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; + } +} + +?> + +
+
+

Cron

+
+
+ + +
+
Cron is not running
+ ITFlow last heard from cron . + Nothing below will run - no mail is being sent, no email is being turned into tickets, and invoices are not being generated. + Add this line to the crontab of the user that owns the ITFlow files: +
+
+ +
+ Cron last checked in . + +
+ + + +
+ Cron is switched off in + Settings > Notifications. The dispatcher is running, but most jobs + stop themselves immediately while this is off. +
+ + +
+ + + + + + + + + + + + + + Queued'; + } elseif ($last_status === 'Running') { + $status_badge = 'Running'; + } elseif ($last_status === 'Completed') { + $status_badge = 'Completed'; + } elseif ($last_status === 'Failed') { + $status_badge = 'Failed'; + } elseif ($last_status !== null) { + $status_badge = 'Stopped'; + } else { + $status_badge = 'Never run'; + } + + ?> + + + + + + + + + + + + + + + + +
JobScheduleLast RunDurationStatusNext RunAction
+ + Disabled +
+
cron/ +
+ +
+
+ + 9) { ?> +
+ +
+ +
+
+
+ + + +
+ 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. + +

+ +
+
+ + diff --git a/admin/database_updates/2.6.1.php b/admin/database_updates/2.6.1.php new file mode 100644 index 00000000..ebda3ead --- /dev/null +++ b/admin/database_updates/2.6.1.php @@ -0,0 +1,32 @@ + Cron. The registry in includes/cron_jobs.php still decides + // which scripts exist and seeds these columns the first time it meets a job; from then + // on the row is what runs. Nothing here can name a script - a row whose job is not in + // the registry is ignored. + mysqli_query($mysqli, "ALTER TABLE `cron_jobs` + ADD COLUMN `cron_job_enabled` tinyint(1) NOT NULL DEFAULT 1 AFTER `cron_job_name`, + ADD COLUMN `cron_job_schedule` varchar(200) NOT NULL DEFAULT 'Interval' AFTER `cron_job_enabled`, + ADD COLUMN `cron_job_interval_minutes` int(11) NOT NULL DEFAULT 1 AFTER `cron_job_schedule`, + ADD COLUMN `cron_job_daily_at` time DEFAULT NULL AFTER `cron_job_interval_minutes`, + ADD COLUMN `cron_job_run_now` tinyint(1) NOT NULL DEFAULT 0 AFTER `cron_job_daily_at`"); + + // Duration is here to make a job that is quietly getting slower visible before it starts + // overrunning its own interval. + mysqli_query($mysqli, "ALTER TABLE `cron_jobs` + ADD COLUMN `cron_job_last_duration` decimal(10,2) DEFAULT NULL AFTER `cron_job_last_finished_at`, + ADD COLUMN `cron_job_last_error` text DEFAULT NULL AFTER `cron_job_last_status`, + ADD COLUMN `cron_job_last_error_at` datetime DEFAULT NULL AFTER `cron_job_last_error`"); + + // Written by the dispatcher every minute before it runs anything, so the admin page can + // tell "no job happened to be due" apart from "the crontab entry is missing". + mysqli_query($mysqli, "ALTER TABLE `settings` + ADD COLUMN `config_cron_last_dispatch_at` datetime DEFAULT NULL"); diff --git a/admin/includes/side_nav.php b/admin/includes/side_nav.php index f2c46108..fe3156ad 100644 --- a/admin/includes/side_nav.php +++ b/admin/includes/side_nav.php @@ -161,6 +161,12 @@ +