mirror of
https://github.com/itflow-org/itflow
synced 2026-08-16 20:45:12 +00:00
Fix Cron Schedule and new directory includes created inside cron for the cron lock
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
124
cron/includes/cron_lock.php
Normal file
124
cron/includes/cron_lock.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Cron runtime: single-run guard and dispatcher support
|
||||
*
|
||||
* Required by each cron script immediately after its CLI check and before config.php,
|
||||
* with the caller setting $cron_lock_script = __FILE__ first.
|
||||
*
|
||||
* Cron work is not safe to run twice at once: autopay charges cards, the mail queue
|
||||
* sends client email, the parser creates tickets. If a run is still going when the next
|
||||
* one fires, those actions can happen twice. The lock name is derived from the calling
|
||||
* script's full path, so it is unique per script and per install - separate ITFlow
|
||||
* instances on one host never block each other, and neither do two different cron
|
||||
* scripts belonging to the same install.
|
||||
*
|
||||
* flock is used rather than a lock file whose presence is checked, because checking for
|
||||
* a file and then creating it is not atomic - two runs starting together can both find
|
||||
* it absent. flock is also released by the kernel however the process exits, so a killed
|
||||
* run leaves nothing stale behind and no age heuristic is needed to clean up after it.
|
||||
*
|
||||
* Contention is not logged: mail_queue and the email parser run every minute, so hitting
|
||||
* a run that is still going is expected, not a fault worth reporting. A lock file that
|
||||
* cannot be opened at all is a real misconfiguration and does report loudly.
|
||||
*
|
||||
* TWO WAYS A CRON SCRIPT RUNS
|
||||
*
|
||||
* Directly (php cron/mail_queue.php): the guard at the bottom of this file takes the
|
||||
* lock and holds it for the life of the process, exactly as it always has.
|
||||
*
|
||||
* Under the dispatcher (cron/cron.php): the dispatcher takes each job's lock itself,
|
||||
* runs the job, and releases it before moving on, so a long job does not hold up the
|
||||
* short ones on the next minute's dispatch. The guard below is skipped in that case -
|
||||
* the lock is already held for this job, and the jobs share one PHP process, so a lock
|
||||
* held for the life of the process would be a lock held for the whole cycle.
|
||||
*
|
||||
* Because the dispatcher shares one process across jobs, a job must never exit() to end
|
||||
* itself early - that would take the rest of the cycle down with it. cronJobStop() is
|
||||
* the replacement: it exits when the script was run directly and unwinds back to the
|
||||
* dispatcher when it wasn't.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Thrown by cronJobStop() when a job ends itself early under the dispatcher. Carries the
|
||||
* message and exit code the script would have exited with, so the dispatcher can record
|
||||
* why the job stopped.
|
||||
*/
|
||||
class CronJobStopped extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* End the current cron job early. Direct runs exit exactly as they did before; dispatched
|
||||
* runs unwind to the dispatcher, which records the reason and carries on with the next job.
|
||||
*/
|
||||
function cronJobStop(string $message = '', int $exit_code = 0): void
|
||||
{
|
||||
if (defined('ITFLOW_CRON_DISPATCHER')) {
|
||||
throw new CronJobStopped($message, $exit_code);
|
||||
}
|
||||
|
||||
if ($message !== '') {
|
||||
echo $message;
|
||||
}
|
||||
|
||||
exit($exit_code);
|
||||
}
|
||||
|
||||
/*
|
||||
* Take the single-run lock for a cron script. $script_path must be the script's own
|
||||
* __FILE__ (or the same resolved path when the dispatcher takes it on the job's behalf),
|
||||
* because that path is what the lock is named after.
|
||||
*
|
||||
* Returns the open handle on success, or false when another run holds the lock. The
|
||||
* handle must stay open for as long as the lock is wanted - closing it releases the lock.
|
||||
*/
|
||||
function cronLockAcquire(string $script_path)
|
||||
{
|
||||
$lock_file = sys_get_temp_dir() . '/itflow_cron_' . md5($script_path) . '.lock';
|
||||
|
||||
$lock_handle = fopen($lock_file, 'c');
|
||||
if ($lock_handle === false) {
|
||||
die("Cannot open the cron lock file at $lock_file - check permissions and open_basedir.\n");
|
||||
}
|
||||
|
||||
if (!flock($lock_handle, LOCK_EX | LOCK_NB)) {
|
||||
// Closing our own handle does not disturb the lock the other run holds on theirs
|
||||
fclose($lock_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $lock_handle;
|
||||
}
|
||||
|
||||
/*
|
||||
* Release a lock taken by cronLockAcquire(). Only the dispatcher needs this - a direct run
|
||||
* holds its lock until the process ends and the kernel drops it.
|
||||
*/
|
||||
function cronLockRelease($lock_handle): void
|
||||
{
|
||||
if (is_resource($lock_handle)) {
|
||||
flock($lock_handle, LOCK_UN);
|
||||
fclose($lock_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Single-run guard for scripts run directly. Skipped under the dispatcher, which locks
|
||||
// each job itself - see the note above.
|
||||
if (!defined('ITFLOW_CRON_DISPATCHER')) {
|
||||
|
||||
if (!isset($cron_lock_script)) {
|
||||
die("Cron scripts must set \$cron_lock_script = __FILE__ before requiring cron/includes/cron_lock.php.\n");
|
||||
}
|
||||
|
||||
$cron_lock_handle = cronLockAcquire($cron_lock_script);
|
||||
|
||||
if ($cron_lock_handle === false) {
|
||||
// Exit silently. On a per-minute schedule, finding a previous run still going is
|
||||
// normal operation rather than an error, and anything written to stdout here would
|
||||
// be mailed to the crontab owner every single minute for the length of that run.
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// The handle is deliberately left open: the lock is held for the life of the process.
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user