diff --git a/CHANGELOG.md b/CHANGELOG.md index ba62c07a..9421d480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,12 +75,20 @@ This file documents all notable changes made to ITFlow. plural filenames. Bookmarks or external links pointing at the old filenames will 404. ### Major Changes -- **One cron entry instead of five.** `cron/cron.php` is now a dispatcher that runs every minute - and decides which jobs are due, so scheduling lives in ITFlow rather than in the crontab and new - jobs arrive with an update instead of an install note. Jobs are tracked in a new `cron_jobs` - table, which means a job whose slot was missed runs at the next opportunity rather than waiting - a day, and each job is locked for its own run so a slow mailbox or a long nightly run no longer - delays anything else. The nightly work itself moved to `cron/nightly_tasks.php`. +- **One cron entry instead of five, and a page to manage it.** `cron/cron.php` is now a dispatcher + that runs every minute and decides which jobs are due, so scheduling lives in ITFlow rather than + in the crontab and new jobs arrive with an update instead of an install note. Jobs are tracked in + a new `cron_jobs` table, which means a job whose slot was missed runs at the next opportunity + rather than waiting a day, and each job is locked for its own run so a slow mailbox or a long + nightly run no longer delays anything else. The nightly work itself moved to + `cron/nightly_tasks.php`. +- **Settings > Cron.** A new admin page lists every job with its schedule, when it last ran, how + long it took, how it ended, and when it is next due. Each job can be turned off, given a + different frequency or time of day, and run on demand — Run Now hands the job to the next + dispatch rather than running it in the browser, so it starts within a minute and still runs on + the command line. The last error a job hit is kept until it is dismissed, rather than + disappearing behind the next success, and the page says plainly when the crontab entry itself is + missing — the dispatcher records a heartbeat every minute whether or not any job was due. - **Ticket SLAs (optional).** SLAs define a response target and an optional resolution target, and are assigned per client and priority, with a global default and an explicit "no SLA" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95a95aff..50ffdfd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,11 @@ One crontab entry runs everything: * * * * * php /path/to/itflow/cron/cron.php >/dev/null ``` -`cron/cron.php` is a dispatcher. It wakes every minute, works out which scripts in `cron/` are due, and requires them into its own process. Adding a job is a new script in `cron/` plus a line in the job table at the top of the dispatcher — `'every' => n` for interval jobs, `'daily_at' => 'HH:MM'` for daily ones. The crontab never changes again. +`cron/cron.php` is a dispatcher. It wakes every minute, works out which scripts in `cron/` are due, and requires them into its own process. Adding a job is a new script in `cron/` plus an entry in `includes/cron_jobs.php`. The crontab never changes again. + +That registry is the only thing that decides **which** scripts can run, and the schedule in it is only a default: it seeds the job's `cron_jobs` row the first time the dispatcher meets the job, and from then on the row is what runs, because Settings > Cron writes to it. The database therefore holds **when and whether**, never **what** — a row naming a script that is not in the registry is ignored, so nothing that reaches the database can point the dispatcher at an arbitrary file. Keep it that way. + +Run Now in the admin UI does not execute anything in the web request: these scripts are CLI-only and some take minutes, so the button sets `cron_job_run_now` and the next dispatch picks it up, through the same lock and claim as a scheduled run. Due-ness is recorded in the `cron_jobs` table rather than matched against the clock, so a job whose minute was missed — machine down, previous run still going — runs at the next opportunity instead of being skipped for the day. A job is claimed *before* it runs, not after: a run that dies half way through is not repeated, which matters because `nightly_tasks.php` generates invoices and charges cards. Each job is also locked individually for the length of its own run (`includes/cron_lock.php`), so a long or hung job holds up only itself — the next minute's dispatch picks up everything else in a second process. diff --git a/admin/cron.php b/admin/cron.php new file mode 100644 index 00000000..7a123cac --- /dev/null +++ b/admin/cron.php @@ -0,0 +1,191 @@ +/dev/null"; + +// Registry order is dispatch order, so the table reads the way the cycle runs +$cron_jobs = []; +foreach (cronJobRegistry() as $job) { + $cron_jobs[$job['name']] = $job; + $cron_jobs[$job['name']]['row'] = null; +} + +$sql = mysqli_query($mysqli, "SELECT * FROM cron_jobs"); +while ($job_row = mysqli_fetch_assoc($sql)) { + if (isset($cron_jobs[$job_row['cron_job_name']])) { + $cron_jobs[$job_row['cron_job_name']]['row'] = $job_row; + } +} + +?> + +
= escapeHtml($cron_command) ?>+
| Job | +Schedule | +Last Run | +Duration | +Status | +Next Run | +Action | +
|---|---|---|---|---|---|---|
|
+ = escapeHtml($job['label']) ?>
+ Disabled
+ = escapeHtml($job['description']) ?> + cron/= escapeHtml($job['script']) ?>
+ |
+ = escapeHtml(cronJobScheduleDescription($schedule, $interval_minutes, $daily_at)) ?> | +
+ = escapeHtml(cronJobTimeAgo($last_run_at)) ?>
+ = escapeHtml(date('M j, g:i A', strtotime($last_run_at))) ?> + |
+ = $last_duration === null ? '-' : escapeHtml($last_duration) . 's' ?> | +
+ = $status_badge ?>
+ 9) { ?>
+ = escapeHtml($last_status) ?> + + |
+ = $next_run === null ? '-' : escapeHtml(cronJobTimeAgo($next_run)) ?> | +
+
+
+
+
+ |
+
|
+
+
+
+
+
+
+
+ Last error
+ = escapeHtml(cronJobTimeAgo($last_error_at)) ?>
+ = escapeHtml($last_error) ?>
+ |
+ ||||||
+ + Run Now does not start the job in your browser - it asks the + dispatcher to pick it up on its next pass, so a job starts within a minute and still runs on the command + line with the same locking as a scheduled run. Detailed per-job output is in + App Logs. + +
+ +Cron
+ +