Fix Cron Schedule and new directory includes created inside cron for the cron lock

This commit is contained in:
johnnyq
2026-07-30 17:06:07 -04:00
parent 119334ab71
commit 0631c45040
13 changed files with 68 additions and 16 deletions

View File

@@ -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:

View File

@@ -188,4 +188,4 @@ while ($job_row = mysqli_fetch_assoc($sql)) {
</div>
</div>
<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/footer.php"; ?>
<?php require_once "../includes/footer.php"; ?>

View File

@@ -0,0 +1,38 @@
<?php
/*
* ITFlow - Database update to version 2.6.2 (from 2.6.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Two things, both about rows that already existed before 2.6.1 ran.
//
// 2.6.1 added the schedule columns but could not set them, so those rows took the column
// defaults - every minute - which is wrong for three jobs and harmful for the nightly run,
// whose overdue invoice reminders re-send on every pass. The registry in
// includes/cron_jobs.php only ever seeds a row it is creating, so it cannot fix them.
//
// The domain refresher also moves onto the nightly schedule here, which is why it is
// matched at either of the intervals it may be sitting on: 1 from the column default, or 5
// if it was already put back by hand.
//
// Only rows still carrying one of those shipped values are touched, so a schedule someone
// has deliberately changed is left alone. Deliberately hardcoded rather than read from the
// registry: a migration has to keep meaning the same thing years from now, whatever that
// file says by then.
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:00:00'
WHERE `cron_job_name` = 'nightly_tasks'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:30:00'
WHERE `cron_job_name` = 'certificate_refresher'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '04:00:00'
WHERE `cron_job_name` = 'domain_refresher'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` IN (1, 5)");

View File

@@ -10,7 +10,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";

View File

@@ -44,12 +44,12 @@ if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.\n");
}
// Tells includes/cron_lock.php and the jobs themselves that they are running under the
// Tells cron/includes/cron_lock.php and the jobs themselves that they are running under the
// dispatcher rather than being executed directly. Must be defined before anything else
// is loaded.
define('ITFLOW_CRON_DISPATCHER', true);
require_once "../includes/cron_lock.php";
require_once "includes/cron_lock.php";
require_once "../config.php";
// Set Timezone

View File

@@ -10,7 +10,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";

View File

@@ -108,7 +108,7 @@ function cronLockRelease($lock_handle): void
if (!defined('ITFLOW_CRON_DISPATCHER')) {
if (!isset($cron_lock_script)) {
die("Cron scripts must set \$cron_lock_script = __FILE__ before requiring includes/cron_lock.php.\n");
die("Cron scripts must set \$cron_lock_script = __FILE__ before requiring cron/includes/cron_lock.php.\n");
}
$cron_lock_handle = cronLockAcquire($cron_lock_script);

View File

@@ -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";

View File

@@ -18,7 +18,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";

View File

@@ -17,7 +17,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";
// Autoload (Webklex & any composer deps)
require_once "../libs/vendor/autoload.php";
@@ -48,7 +48,7 @@ if ($config_ticket_email_parse == 0) {
cronJobStop("Email Parser: Feature is not enabled - check Settings > 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.

View File

@@ -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";

View File

@@ -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',

View File

@@ -1555,7 +1555,13 @@ if (isset($_POST['add_telemetry'])) {
<p>A few <a href="https://docs.itflow.org/installation#post-installation_essential_housekeeping">housekeeping steps</a> are required to ensure everything runs smoothly, namely:</p>
<ul>
<li><a href="https://docs.itflow.org/backups">Setup backups</a></li>
<li><a href="https://docs.itflow.org/cron">Setup cron</a> *If Installing via script cron jobs will be automatically setup for you.</li>
<li>
<a href="https://docs.itflow.org/cron">Setup cron</a> - ITFlow needs one entry, which runs
every job on the schedule set in Settings &gt; Cron. Add it to the crontab of the user that
owns the ITFlow files:
<pre class="bg-dark text-white p-2 mt-2"><?= escapeHtml("* * * * * php " . dirname(__DIR__) . "/cron/cron.php >/dev/null") ?></pre>
*If installing via the script this is set up for you.
</li>
<li>Star ITFlow on <a href="https://github.com/itflow-org/itflow">Github</a> :)</li>
</ul>