From 0631c45040a305ae7cf6fcbac5b11454383e6e19 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 30 Jul 2026 17:06:07 -0400 Subject: [PATCH] Fix Cron Schedule and new directory includes created inside cron for the cron lock --- CONTRIBUTING.md | 4 +-- admin/cron.php | 2 +- admin/database_updates/2.6.2.php | 38 +++++++++++++++++++++++ cron/certificate_refresher.php | 2 +- cron/cron.php | 4 +-- cron/domain_refresher.php | 2 +- {includes => cron/includes}/cron_lock.php | 2 +- cron/mail_queue.php | 2 +- cron/nightly_tasks.php | 2 +- cron/ticket_email_parser.php | 4 +-- cron/ticket_sla.php | 2 +- includes/cron_jobs.php | 12 +++++-- setup/index.php | 8 ++++- 13 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 admin/database_updates/2.6.2.php rename {includes => cron/includes}/cron_lock.php (98%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50ffdfd3..7a05e09e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ There is no `composer install` or `npm install` step. All third-party libraries | `client/` | The logged-in client portal (contacts of a client). | | `guest/` | Unauthenticated flows via URL keys (view/pay invoice, view quote/ticket, view shared credentials/files/documents). | | `api/v1/` | Key-authenticated JSON CRUD API, one directory per module. | -| `cron/` | Scheduled jobs. `cron.php` is the dispatcher and the only entry in the crontab; everything else in the directory is a job it runs. See [Cron](#cron). | +| `cron/` | Scheduled jobs. `cron.php` is the dispatcher and the only entry in the crontab; everything else in the directory is a job it runs, with `cron/includes/` for the parts only cron uses. See [Cron](#cron). | | `functions.php` + `functions/` | Shared helper functions, split into topical files (`sanitize.php`, `auth.php`, `logging.php`, …) loaded by `functions.php`. New helpers go in the topical file that matches their concern. | | `includes/` (root) | **Shared** across portals: session/auth bootstrap, DB, layout partials. | | `post/` (root) | **Shared** POST handlers (logout, misc). | @@ -91,7 +91,7 @@ That registry is the only thing that decides **which** scripts can run, and the 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. +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 (`cron/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. Because the jobs share one PHP process, job code has three rules: diff --git a/admin/cron.php b/admin/cron.php index 7a123cac..f57a7536 100644 --- a/admin/cron.php +++ b/admin/cron.php @@ -188,4 +188,4 @@ while ($job_row = mysqli_fetch_assoc($sql)) { - + diff --git a/admin/database_updates/2.6.2.php b/admin/database_updates/2.6.2.php new file mode 100644 index 00000000..b63f960a --- /dev/null +++ b/admin/database_updates/2.6.2.php @@ -0,0 +1,38 @@ + Ticketing > Email-to-ticket parsing. See https://docs.itflow.org/ticket_email_parse -- Quitting.."); } -// Overlapping runs are prevented by includes/cron_lock.php. This script used to keep a +// Overlapping runs are prevented by cron/includes/cron_lock.php. This script used to keep a // lock file of its own alongside that one, which needed a five minute age heuristic to // recover from a killed run and could only end itself with exit() - fatal to a dispatched // job. flock covers the same ground and the kernel drops it however the process ends. diff --git a/cron/ticket_sla.php b/cron/ticket_sla.php index b2b5bcea..ea115422 100644 --- a/cron/ticket_sla.php +++ b/cron/ticket_sla.php @@ -9,7 +9,7 @@ if (php_sapi_name() !== 'cli') { // Prevent overlapping runs of this script $cron_lock_script = __FILE__; -require_once "../includes/cron_lock.php"; +require_once "includes/cron_lock.php"; require_once "../config.php"; require_once "../includes/inc_set_timezone.php"; diff --git a/includes/cron_jobs.php b/includes/cron_jobs.php index 58723022..a349edc1 100644 --- a/includes/cron_jobs.php +++ b/includes/cron_jobs.php @@ -14,6 +14,14 @@ * This file is the only thing that decides which scripts can be run. The database holds * when and whether, never what - a row naming a script that is not listed here is ignored, * so nothing that reaches the database can point the dispatcher at an arbitrary file. + * + * Loaded from both sides: cron/cron.php requires it on the command line under system cron, + * and Settings > Cron requires it in a web request. Nothing in here may touch $_SERVER, + * $_SESSION or any other superglobal - there is no DOCUMENT_ROOT, no session and no request + * when cron runs it. + * + * That shared use is why this sits here rather than in cron/includes/ with the lock, which + * only cron loads: the admin pages would otherwise be reaching into the cron directory. */ function cronJobRegistry(): array @@ -48,8 +56,8 @@ function cronJobRegistry(): array 'label' => 'Domain Refresher', 'script' => 'domain_refresher.php', 'description' => 'Refreshes WHOIS and DNS for the domain that was checked longest ago. One domain per run.', - 'schedule' => 'Interval', - 'interval_minutes' => 5, + 'schedule' => 'Daily', + 'daily_at' => '04:00', ], [ 'name' => 'nightly_tasks', diff --git a/setup/index.php b/setup/index.php index ab3becf2..06a6d0a1 100644 --- a/setup/index.php +++ b/setup/index.php @@ -1555,7 +1555,13 @@ if (isset($_POST['add_telemetry'])) {

A few housekeeping steps are required to ensure everything runs smoothly, namely: