Cron Tick Hooks

2 views Markdown

The hooks over work nobody is watching: run starts, task registration, the queue, periodic rounds and worker management.

Overview

Scheduled work runs in a tick: the system wakes each minute, takes jobs off the queue, opens helper workers where needed, and finishes.

One thing is true of every hook here: nobody is watching. An error you throw is shown to no one and output you print reaches no one; the only trace is in the records.

Reference

Registering a scheduled task of your own

registercronjobs
CronJobQueue registration is a side effect

Runs while the task registry is gathered. This is where you register your own.

Parameters 0
It takes no parameters. You register by calling CronJobQueue::register(), not by returning.
Return 1
voidThe return is ignored; registration happens as a side effect.
Listener
Hook::add('register:cronjobs', 10, function () {
    // You register with a call, not with a return.
    CronJobQueue::register('acme.sync', AcmeSyncHandler::class);
});

Stopping a tick from running

gatecron.worker.run
cron.php the run environment

Runs before a tick starts. Stopping it means no job is processed in that tick.

Parameters 2
$isCliboolWhether it runs from the command line. False means it was triggered over a request.
$sapistringThe name of the interface running it. Read this where you want it to run in one environment only.
Return 1
stringA non-empty string blocks the tick; the text comes back as the reason. No error is thrown and the tick is quietly skipped.
Listener
Hook::add('gate:cron.worker.run', 10, function ($isCli, $sapi) {
    // No task runs inside a maintenance window.
    if (Acme::maintenanceWindow()) return 'maintenance window';

    return null;
});

Following a tick starting

actioncron.tick.started
cron.php it can be a helper

Runs where a tick starts.

Parameters 3
$workerIdstringThis tick's unique worker id. Several ticks can run at once; keep your records apart by this id.
$isChildboolWhether this is a helper worker. True means it is not the main tick but a helper opened under load; both run this hook.
$startTsintThe time the tick started.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:cron.tick.started', 10, function ($workerId, $isChild, $startTs) {
    // Helper workers run this hook too: tell the two apart.
    if ($isChild) return;

    Ops::heartbeat($workerId, $startTs);
});

Following a tick finishing

actioncron.tick.completed
cron.php it can be partial

Runs after a tick finished.

Parameters 2
$payloadarrayThe tick summary: status (ok or partial), the worker id, how many jobs ran, the duration, the errors, plus scheduler and helper details. partial means the tick did not finish its work.
$isChildboolWhether this was a helper worker.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:cron.tick.completed', 10, function ($payload, $isChild) {
    // partial means the tick ran out of room; a run of them is a capacity problem.
    if (($payload['status'] ?? '') === 'partial')
        Ops::warn('cron-partial', (int) ($payload['processed'] ?? 0));
});

Following a processed job

actioncron.job.processed
CronJobQueue once per job

Runs after a queued job was processed.

Parameters 2
$jobarrayThe processed queue row: id, type, payload, attempts. An attempt count above one means the job failed before.
$workerIdstringThe id of the worker that ran it.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:cron.job.processed', 10, function ($job, $workerId) {
    // An attempt count above one means this job failed before.
    if ((int) ($job['attempts'] ?? 0) > 1)
        Ops::note('cron-retry', $job['type'] ?? '', (int) $job['attempts']);
});

Joining the daily work

actioncron.day.run
cronjobs/Daily your return is recorded

Runs in the daily task round. Unlike the other hooks your return is recorded.

Parameters 0
It takes no parameters.
Return 1
bool|arrayYour return is gathered and shown in the panel as success, duration and errors. A wrong return breaks nothing; it only makes your entry look bad.
Listener
Hook::add('action:cron.day.run', 10, function () {
    // Your return shows in the panel: true on success, a note otherwise.
    $done = Acme::nightlyReport();

    return $done ? true : ['status' => false, 'message' => 'the report failed'];
});

Joining the hourly work

actioncron.hour.run
cronjobs/HourlyExecute your return is recorded

Runs once an hour. Attach hourly work here instead of opening a task file: cache clearing, summaries, outside syncing.

Parameters 0
It takes no parameters.
Return 1
bool|arrayYour return is gathered and shown in the panel as success, duration and errors. A wrong return breaks nothing; it only makes your entry look bad.
Listener
Hook::add('action:cron.hour.run', 10, function () {
    $n = Acme::syncPartnerCatalog();

    return ['status' => true, 'message' => $n . ' records synced'];
});

Joining the per-minute work

actioncron.minute.run
cronjobs/PerMinuteExecute your return is recorded

Runs every minute, the tightest of the rounds. It suits queue draining, outside polling and health checks.

Parameters 0
It takes no parameters.
Return 1
bool|arrayYour return is gathered and shown in the panel. Time spent here is paid again every minute: if your work is long, hand it to the queue instead of finishing it here.
Listener
Hook::add('action:cron.minute.run', 10, function () {
    // Keep it short: this block runs every minute.
    $sent = Acme::drainOutbox(50);

    return ['status' => true, 'message' => $sent . ' messages sent'];
});

Joining the monthly work

actioncron.month.run
cronjobs/MonthlyExecute your return is recorded

Runs once a month: month-end summaries, reconciliation, archiving.

Parameters 0
It takes no parameters.
Return 1
bool|arrayYour return is gathered and shown in the panel. Because it runs once a month, a fault can sit unnoticed for weeks; put a clear note in your return.
Listener
Hook::add('action:cron.month.run', 10, function () {
    $rows = Acme::archiveLastMonth();

    return ['status' => true, 'message' => $rows . ' rows archived'];
});

Changing how many helpers open

filtercron.spawn.count
cron.php server load

Runs after it was decided how many helper workers to open under load.

Parameters 1
$decisionarrayrefThe decision: count (how many to open), pending jobs, active workers, the threshold, the ceiling, the reason. The field that acts is count; the rest explain the decision.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:cron.spawn.count', 10, function (&$decision) {
    // Only count acts; open no helpers while the server is under load.
    if (Acme::loadHigh()) $decision['count'] = 0;
});

Changing the chart task list

filtercron.dashboard_chart_tasks
automation/dashboard passed by link

Runs while the task picker of the activity chart is being prepared on the automation board. You can add a task type of your own or hide the internal ones.

Parameters 1
$optionsarrayby linkWhat the picker holds: the key is the task name, the value is the title on screen. It is passed by link, so you make your change by writing over the array, not by returning it.
Return 1
voidThe return is ignored. Sorting happens before you, so anything you add stays at the end of the list.
Listener
Hook::add('filter:cron.dashboard_chart_tasks', 10, function (&$options) {
    // Add a task of your own to the list.
    $options['acme-sync'] = 'Acme sync';

    // Hide an internal one.
    unset($options['queue-cleanup']);
});

Stopping a run by hand

gatecron.task_run_now
AdminAutomation an operator triggers it

Runs before an operator runs a task by hand.

Parameters 2
$taskstringThe key of the task about to run.
$user_idintThe id of the staff member acting.
Return 1
stringA non-empty string stops the run; the text is shown to the operator. Triggered from the panel, this hook is one whose message somebody actually reads.
Listener
Hook::add('gate:cron.task_run_now', 10, function ($task, $user_id) {
    // Triggered from the panel: a person reads your message.
    if ($task === 'invoice.generate' && Acme::billingFrozen())
        return 'Billing is frozen; this task cannot run now.';

    return null;
});

Following the kill switch

actioncron.kill_switch_toggled
AdminAutomation everything stops

Runs where all scheduled work is switched on or off.

Parameters 2
$enabledboolThe new state. Switched off, no task runs: no invoices, no suspensions, no renewals.
$user_idintThe staff member who acted.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:cron.kill_switch_toggled', 10, function ($enabled, $user_id) {
    // Switching off stops every automation: forgotten, not even invoices go out.
    if (!$enabled) Ops::alert('cron-disabled', (int) $user_id);
});

Pitfalls

An error thrown here reaches nobody

On scheduled work hooks nobody is watching. An error you throw is shown to neither customer nor operator and only lands in the records. To make a problem known, do it yourself: a notification, a record, an alert.

Helper workers run the same hooks

Under load the system opens helper workers and each one runs the tick hooks. A notification on "tick started" sends dozens in a busy minute. Read the helper mark in the second parameter.

One job can be processed more than once

A failed job is tried again and the processed hook runs on each attempt. A listener reacting without reading the attempt count produces a run of records or messages for one job. Check the counter in your first line.

On the daily hook your return is recorded

Unlike other event hooks, the periodic ones gather your return and show it in the panel as success, duration and errors. Returning nothing leaves your entry blank. Return true on success and an array with a note on failure.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.