The Tick
How the once-a-minute tick schedules and runs jobs
The tick is the heart of the module: once a minute it looks at every enabled job, decides what is due, and runs it. Neither the webtrees core nor the operating system does any scheduling itself — they only start tick.php
.
flowchart TD
subgraph Triggers["`⏱ Triggers *(once per minute)*`"]
CRON["`🕑 OS cron / systemd<br/>*every minute*`"]
WATCH["`👁 watch daemon<br/>*(in-module)*`"]
end
subgraph Guard["🛡 Guard"]
TP["`📄 cli/tick.php · **cron:tick**<br/>*guard(): 403 over HTTP*`"]
OFF{"`⚠ site offline?<br/>*data/offline.txt*`"}
BOOT["`🚀 boot()<br/>*webtrees + DB*`"]
end
subgraph Selection["🔎 Select work"]
TIME["`📅 time-based jobs<br/>**next_run_at due** *(UTC)*`"]
CE[("`📦 cj_event<br/>*event queue*`")]
DRAIN["`🧮 drain queue<br/>*coalesced · 60 s budget*`"]
end
subgraph Execution["`⚙ Execution *(isolated child)*`"]
RUN["`🧪 JobRunner<br/>**proc_open**`"]
LIM["`⏲ timeout 30–3600 s<br/>last **64 KB** · exit code`"]
end
subgraph Storage["💾 Storage"]
CJRUN[("`cj_run<br/>*run history*`")]
MARK["`🏷 tick.last<br/>*stale > 300 s → tick_stale*`"]
end
CRON --> Guard
WATCH --> Guard
TP --> OFF
OFF -->|"offline"| SKIP["⏭ skip tick"]
OFF -->|"online"| BOOT
BOOT --> TIME
BOOT --> DRAIN
CE --> DRAIN
TIME --> Execution
DRAIN --> Execution
RUN --> LIM
LIM --> CJRUN
BOOT --> MARK
classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px
class CRON,WATCH sourceStyle
class OFF,CE queueStyle
class TP,TIME,DRAIN tickStyle
class RUN,LIM execStyle
class CJRUN,MARK,SKIP storageStyle
What starts a tick
Pick one trigger (once):
- OS cron —
* * * * *from the webtrees root (see Installation). - systemd timer — the generated unit from the module’s admin page.
- watch daemon — the in-module resident process that re-launches
tick.phpevery 60 s and survives FPM worker recycling (Installation, which also carries the daemon’s lifecycle diagram).
All three run the same command: php modules_v4/cronjob/cli/tick.php cron:tick.
What one tick does
- Guard —
CliBootstrap::guard()answers403if the script is ever reached over HTTP; thenCliBootstrap::boot()loads webtrees + the database. - Offline check — if
data/offline.txtexists the tick is skipped entirely, before any database access — e.g. during a webtrees update, when schema and code are in flux. - Select the work — two sources, combined in one pass:
- time-based jobs whose
next_run_athas passed (cron schedules, UTC); the due triggers are computed against the last run (or the job’s creation, for never-run jobs). - event jobs — the
cj_eventqueue is drained oldest-first, coalesced to one run per event name, within a 60-second wall-clock budget. Event jobs receive the event name and payload via theCRONJOB_EVENT/CRONJOB_EVENT_PAYLOADenvironment. See the event pipeline.
- time-based jobs whose
- Run each job in isolation —
JobRunnervalidates the command (amodules_v4/<module>/cli/<script>.phppath, or one of the four allowlisted core commands; at most 10 argument tokens) and launches it as a child process with a hard timeout (30–3600 s). Only the last 64 KB of output are captured; a job that hits its timeout is killed (proc_terminate) and reported as exit-9. - Record — every run is written to
cj_run(status, exit code, duration, the trigger that fired it, captured output) — this is the run history. Event-triggered runs are additionally linked viacj_event_run. - Marker — the tick writes
data/cronjob/tick.last. If that marker is older than 300 s, the module reports itself astick_stalein the admin status.
Manual runs
You can also run the tick by hand for debugging — --dry-run, --job=<name>, --strict and --full-output are described on the CLI scripts page. Note that --full-output may expose personal data, so only use it where the output destination is trusted.