mirror of
https://git.tt-rss.org/fox/tt-rss.git
synced 2025-08-06 14:17:27 +02:00
150 lines
4.5 KiB
PHP
150 lines
4.5 KiB
PHP
<?php
|
|
class Scheduler {
|
|
private static ?Scheduler $instance = null;
|
|
|
|
const TASK_RC_EXCEPTION = -100;
|
|
|
|
/** @var array<string, mixed> */
|
|
private array $scheduled_tasks = [];
|
|
|
|
public static function getInstance(): Scheduler {
|
|
if (self::$instance == null)
|
|
self::$instance = new self();
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Adds a backend scheduled task which will be executed by updater (if due) during housekeeping.
|
|
*
|
|
* The granularity is not strictly guaranteed, housekeeping is invoked several times per hour
|
|
* depending on how fast feed batch was processed, but no more than once per minute.
|
|
*
|
|
* Tasks do not run in user context. Task names may not overlap. Plugins should register tasks
|
|
* via PluginHost methods (to be implemented later).
|
|
*
|
|
* Tasks should return an integer value (return code) which is stored in the database, a value of
|
|
* 0 is considered successful.
|
|
*
|
|
* @param string $task_name unique name for this task, plugins should prefix this with plugin name
|
|
* @param string $cron_expression schedule for this task in cron format
|
|
* @param Closure $callback task code that gets executed
|
|
*/
|
|
function add_scheduled_task(string $task_name, string $cron_expression, Closure $callback) : bool {
|
|
$task_name = strtolower($task_name);
|
|
|
|
if (isset($this->scheduled_tasks[$task_name])) {
|
|
user_error("Attempted to override already registered scheduled task $task_name", E_USER_WARNING);
|
|
return false;
|
|
} else {
|
|
try {
|
|
$cron = new Cron\CronExpression($cron_expression);
|
|
} catch (InvalidArgumentException $e) {
|
|
user_error("Attempt to register scheduled task $task_name failed: " . $e->getMessage(), E_USER_WARNING);
|
|
return false;
|
|
}
|
|
|
|
$this->scheduled_tasks[$task_name] = [
|
|
"cron" => $cron,
|
|
"callback" => $callback,
|
|
];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute scheduled tasks which are due to run and record last run timestamps.
|
|
*/
|
|
function run_due_tasks() : void {
|
|
Debug::log('Processing all scheduled tasks...');
|
|
|
|
$tasks_succeeded = 0;
|
|
$tasks_failed = 0;
|
|
|
|
foreach ($this->scheduled_tasks as $task_name => $task) {
|
|
$task_record = ORM::for_table('ttrss_scheduled_tasks')
|
|
->where('task_name', $task_name)
|
|
->find_one();
|
|
|
|
if ($task_record)
|
|
$last_run = $task_record->last_run;
|
|
else
|
|
$last_run = '1970-01-01 00:00';
|
|
|
|
// because we don't schedule tasks every minute, we assume that task is due if its
|
|
// next estimated run based on previous timestamp is in the past
|
|
if ($task['cron']->getNextRunDate($last_run)->getTimestamp() - time() < 0) {
|
|
Debug::log("=> Scheduled task $task_name is due, executing...");
|
|
|
|
$task_started = time();
|
|
|
|
try {
|
|
$rc = (int) $task['callback']();
|
|
} catch (Exception $e) {
|
|
user_error("Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING);
|
|
|
|
$rc = self::TASK_RC_EXCEPTION;
|
|
}
|
|
|
|
$task_duration = time() - $task_started;
|
|
|
|
if ($rc === 0) {
|
|
++$tasks_succeeded;
|
|
Debug::log("<= Scheduled task $task_name has finished in $task_duration seconds.");
|
|
} else {
|
|
$tasks_failed++;
|
|
Debug::log("!! Scheduled task $task_name has failed with RC: $rc after $task_duration seconds.");
|
|
}
|
|
|
|
if ($task_record) {
|
|
$task_record->last_run = Db::NOW();
|
|
$task_record->last_duration = $task_duration;
|
|
$task_record->last_rc = $rc;
|
|
|
|
$task_record->save();
|
|
} else {
|
|
$task_record = ORM::for_table('ttrss_scheduled_tasks')->create();
|
|
|
|
$task_record->set([
|
|
'task_name' => $task_name,
|
|
'last_duration' => $task_duration,
|
|
'last_rc' => $rc,
|
|
'last_run' => Db::NOW(),
|
|
]);
|
|
|
|
$task_record->save();
|
|
}
|
|
}
|
|
}
|
|
|
|
Debug::log("Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed.");
|
|
}
|
|
|
|
/**
|
|
* Purge records of scheduled tasks that aren't currently registered
|
|
* and haven't ran for a long time.
|
|
*
|
|
* @return int 0 if successful, 1 on failure
|
|
*/
|
|
function purge_orphaned_tasks(): int {
|
|
if (!$this->scheduled_tasks) {
|
|
Debug::log(__METHOD__ . ' was invoked before scheduled tasks have been registered. This should never happen.');
|
|
return 1;
|
|
}
|
|
|
|
$result = ORM::for_table('ttrss_scheduled_tasks')
|
|
->where_not_in('task_name', array_keys($this->scheduled_tasks))
|
|
->where_raw("last_run < NOW() - INTERVAL '5 weeks'")
|
|
->delete_many();
|
|
|
|
if ($result) {
|
|
$deleted_count = ORM::get_last_statement()->rowCount();
|
|
|
|
if ($deleted_count)
|
|
Debug::log("Purged {$deleted_count} orphaned scheduled tasks.");
|
|
}
|
|
|
|
return $result ? 0 : 1;
|
|
}
|
|
}
|