diff options
| -rw-r--r-- | classes/Config.php | 2 | ||||
| -rw-r--r-- | classes/PluginHost.php | 33 | ||||
| -rw-r--r-- | classes/Pref_System.php | 3 | ||||
| -rw-r--r-- | classes/RSSUtils.php | 6 | ||||
| -rw-r--r-- | classes/Scheduler.php | 36 | ||||
| -rwxr-xr-x | plugins/cache_starred_images/init.php | 104 | ||||
| -rw-r--r-- | sql/pgsql/migrations/151.sql | 6 | ||||
| -rw-r--r-- | sql/pgsql/schema.sql | 4 |
8 files changed, 128 insertions, 66 deletions
diff --git a/classes/Config.php b/classes/Config.php index c413317be..a4d2eb6b3 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -6,7 +6,7 @@ class Config { const T_STRING = 2; const T_INT = 3; - const SCHEMA_VERSION = 150; + const SCHEMA_VERSION = 151; /** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX: * diff --git a/classes/PluginHost.php b/classes/PluginHost.php index f61a5a9a4..cc7a69a5a 100644 --- a/classes/PluginHost.php +++ b/classes/PluginHost.php @@ -38,6 +38,8 @@ class PluginHost { private static ?PluginHost $instance = null; + private ?Scheduler $scheduler = null; + const API_VERSION = 2; const PUBLIC_METHOD_DELIMITER = "--"; @@ -215,6 +217,7 @@ class PluginHost { function __construct() { $this->pdo = Db::pdo(); + $this->scheduler = new Scheduler('PluginHost Scheduler'); $this->storage = []; } @@ -438,6 +441,10 @@ class PluginHost { $this->owner_uid = (int) $owner_uid; + if ($this->owner_uid) { + $this->set_scheduler_name("PluginHost Scheduler for UID $owner_uid"); + } + foreach ($plugins as $class) { $class = trim($class); $class_file = strtolower(basename(clean($class))); @@ -907,4 +914,30 @@ class PluginHost { return basename(dirname(dirname($ref->getFileName()))) == "plugins.local"; } + /** + * This exposes sheduled tasks functionality to plugins. For system plugins, tasks registered here are + * executed (if due) during housekeeping. For user plugins, tasks are only run after any feeds owned by + * the user have been processed in an update batch (which means user is not inactive). + * + * This behaviour mirrors that of `HOOK_HOUSE_KEEPING` for user plugins. + * + * @see Scheduler::add_scheduled_task() + * @see Plugin::hook_house_keeping() + */ + function add_scheduled_task(Plugin $sender, string $task_name, string $cron_expression, Closure $callback): bool { + if ($this->is_system($sender)) + $task_name = get_class($sender) . ':' . $task_name; + else + $task_name = get_class($sender) . ':' . $task_name . ':' . $this->owner_uid; + + return $this->scheduler->add_scheduled_task($task_name, $cron_expression, $callback); + } + + function run_due_tasks() : void { + $this->scheduler->run_due_tasks(); + } + + private function set_scheduler_name(string $name) : void { + $this->scheduler->set_name($name); + } } diff --git a/classes/Pref_System.php b/classes/Pref_System.php index c5d7ab606..7946dc293 100644 --- a/classes/Pref_System.php +++ b/classes/Pref_System.php @@ -33,6 +33,7 @@ class Pref_System extends Handler_Administrative { <table width='100%' class='event-log'> <tr> <th><?= __("Task name") ?></th> + <th><?= __("Schedule") ?></th> <th><?= __("Last executed") ?></th> <th><?= __("Duration (seconds)") ?></th> <th><?= __("Return code") ?></th> @@ -40,6 +41,7 @@ class Pref_System extends Handler_Administrative { <?php $task_records = ORM::for_table('ttrss_scheduled_tasks') + ->order_by_asc(['last_cron_expression', 'task_name']) ->find_many(); foreach ($task_records as $task) { @@ -48,6 +50,7 @@ class Pref_System extends Handler_Administrative { ?> <tr> <td class="<?= $row_style ?>"><?= $task->task_name ?></td> + <td><?= $task->last_cron_expression ?></td> <td><?= TimeHelper::make_local_datetime($task->last_run) ?></td> <td><?= $task->last_duration ?></td> <td><?= $task->last_rc ?></td> diff --git a/classes/RSSUtils.php b/classes/RSSUtils.php index b0cfc4823..7339a066d 100644 --- a/classes/RSSUtils.php +++ b/classes/RSSUtils.php @@ -1666,6 +1666,7 @@ class RSSUtils { UserHelper::load_user_plugins($owner_uid, $tmph); + $tmph->run_due_tasks(); $tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); } @@ -1794,7 +1795,10 @@ class RSSUtils { static function housekeeping_common(): void { Scheduler::getInstance()->run_due_tasks(); - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); + $pluginhost = PluginHost::getInstance(); + + $pluginhost->run_due_tasks(); + $pluginhost->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); } static function update_favicon(string $site_url, int $feed): false|string { diff --git a/classes/Scheduler.php b/classes/Scheduler.php index 13d8e94ea..b5df8f3fe 100644 --- a/classes/Scheduler.php +++ b/classes/Scheduler.php @@ -3,16 +3,23 @@ class Scheduler { private static ?Scheduler $instance = null; const TASK_RC_EXCEPTION = -100; + const DEFAULT_NAME = 'Default Scheduler'; /** @var array<string, mixed> */ private array $scheduled_tasks = []; - function __construct() { - $this->add_scheduled_task('purge_orphaned_scheduled_tasks', '@weekly', - function() { - return $this->purge_orphaned_tasks(); - } - ); + private string $name; + + function __construct(string $name = self::DEFAULT_NAME) { + $this->set_name($name); + + if ($name === self::DEFAULT_NAME) { + $this->add_scheduled_task('purge_orphaned_scheduled_tasks', '@weekly', + function() { + return $this->purge_orphaned_tasks(); + } + ); + } } public static function getInstance(): Scheduler { @@ -22,6 +29,11 @@ class Scheduler { return self::$instance; } + /** Sets specific identifier for this instance of Scheduler used in debug logging */ + public function set_name(string $name) : void { + $this->name = $name; + } + /** * Adds a backend scheduled task which will be executed by updater (if due) during housekeeping. * @@ -42,13 +54,13 @@ class Scheduler { $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); + user_error("[$this->name] 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); + user_error("[$this->name] Attempt to register scheduled task $task_name failed: " . $e->getMessage(), E_USER_WARNING); return false; } @@ -64,7 +76,7 @@ class Scheduler { * Execute scheduled tasks which are due to run and record last run timestamps. */ function run_due_tasks() : void { - Debug::log('Processing all scheduled tasks...'); + Debug::log("[$this->name] Processing all scheduled tasks..."); $tasks_succeeded = 0; $tasks_failed = 0; @@ -89,7 +101,7 @@ class Scheduler { try { $rc = (int) $task['callback'](); } catch (Exception $e) { - user_error("Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING); + user_error("[$this->name] Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING); $rc = self::TASK_RC_EXCEPTION; } @@ -108,6 +120,7 @@ class Scheduler { $task_record->last_run = Db::NOW(); $task_record->last_duration = $task_duration; $task_record->last_rc = $rc; + $task_record->last_cron_expression = $task['cron']->getExpression(); $task_record->save(); } else { @@ -118,6 +131,7 @@ class Scheduler { 'last_duration' => $task_duration, 'last_rc' => $rc, 'last_run' => Db::NOW(), + 'last_cron_expression' => $task['cron']->getExpression() ]); $task_record->save(); @@ -125,7 +139,7 @@ class Scheduler { } } - Debug::log("Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed."); + Debug::log("[$this->name] Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed."); } /** diff --git a/plugins/cache_starred_images/init.php b/plugins/cache_starred_images/init.php index 527fb21ee..4ca11235a 100755 --- a/plugins/cache_starred_images/init.php +++ b/plugins/cache_starred_images/init.php @@ -31,78 +31,78 @@ class Cache_Starred_Images extends Plugin { $this->cache_status->put(".no-auto-expiry", ""); if ($this->cache->is_writable() && $this->cache_status->is_writable()) { - $host->add_hook($host::HOOK_HOUSE_KEEPING, $this); - $host->add_hook($host::HOOK_ENCLOSURE_ENTRY, $this); - $host->add_hook($host::HOOK_SANITIZE, $this); - } else { - user_error("Starred cache directory ".$this->cache->get_dir()." (or status cache subdir in status-files/) is not writable.", E_USER_WARNING); - } - } - - /** since HOOK_UPDATE_TASK is not available to user plugins, this hook is a next best thing */ - function hook_house_keeping() { - Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "..."); + $host->add_scheduled_task($this, "cache_starred_images", "@hourly", function() { + Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "..."); - $sth = $this->pdo->prepare("SELECT content, ttrss_entries.title, - ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data - FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON - (ttrss_user_entries.feed_id = ttrss_feeds.id) - WHERE ref_id = ttrss_entries.id AND - marked = true AND - site_url != '' AND - ttrss_user_entries.owner_uid = ? AND - plugin_data NOT LIKE '%starred_cache_images%' - ORDER BY RANDOM() LIMIT 100"); + $sth = $this->pdo->prepare("SELECT content, ttrss_entries.title, + ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data + FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON + (ttrss_user_entries.feed_id = ttrss_feeds.id) + WHERE ref_id = ttrss_entries.id AND + marked = true AND + site_url != '' AND + ttrss_user_entries.owner_uid = ? AND + plugin_data NOT LIKE '%starred_cache_images%' + ORDER BY RANDOM() LIMIT 100"); - if ($sth->execute([$this->host->get_owner_uid()])) { + if ($sth->execute([$this->host->get_owner_uid()])) { - $usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?"); + $usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?"); - while ($line = $sth->fetch()) { - Debug::log("processing article " . $line["title"], Debug::LOG_VERBOSE); + while ($line = $sth->fetch()) { + Debug::log("processing article " . $line["title"], Debug::LOG_VERBOSE); - if ($line["site_url"]) { - $success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]); + if ($line["site_url"]) { + $success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]); - if ($success) { - $plugin_data = "starred_cache_images," . $line["owner_uid"] . ":" . $line["plugin_data"]; + if ($success) { + $plugin_data = "starred_cache_images," . $line["owner_uid"] . ":" . $line["plugin_data"]; - $usth->execute([$plugin_data, $line['id']]); + $usth->execute([$plugin_data, $line['id']]); + } + } } } - } - } + }); - /* actual housekeeping */ + $host->add_scheduled_task($this, "expire_caches", "@daily", function() { - Debug::log("expiring {$this->cache->get_dir()} and {$this->cache_status->get_dir()}..."); + Debug::log("expiring {$this->cache->get_dir()} and {$this->cache_status->get_dir()}..."); - $files = [ - ...(glob($this->cache->get_dir() . "/*-*") ?: []), - ...(glob($this->cache_status->get_dir() . "/*.status") ?: []), - ]; + $files = [ + ...(glob($this->cache->get_dir() . "/*-*") ?: []), + ...(glob($this->cache_status->get_dir() . "/*.status") ?: []), + ]; - asort($files); + asort($files); - $last_article_id = 0; - $article_exists = 1; + $last_article_id = 0; + $article_exists = 1; - foreach ($files as $file) { - list ($article_id, $hash) = explode("-", basename($file)); + foreach ($files as $file) { + list ($article_id, $hash) = explode("-", basename($file)); - if ($article_id != $last_article_id) { - $last_article_id = $article_id; + if ($article_id != $last_article_id) { + $last_article_id = $article_id; - $sth = $this->pdo->prepare("SELECT id FROM ttrss_entries WHERE id = ?"); - $sth->execute([$article_id]); + $sth = $this->pdo->prepare("SELECT id FROM ttrss_entries WHERE id = ?"); + $sth->execute([$article_id]); - $article_exists = $sth->fetch(); - } + $article_exists = $sth->fetch(); + } - if (!$article_exists) { - unlink($file); - } + if (!$article_exists) { + unlink($file); + } + } + + }); + + $host->add_hook($host::HOOK_ENCLOSURE_ENTRY, $this); + $host->add_hook($host::HOOK_SANITIZE, $this); + } else { + user_error("Starred cache directory ".$this->cache->get_dir()." (or status cache subdir in status-files/) is not writable.", E_USER_WARNING); } } diff --git a/sql/pgsql/migrations/151.sql b/sql/pgsql/migrations/151.sql new file mode 100644 index 000000000..c3d9c159b --- /dev/null +++ b/sql/pgsql/migrations/151.sql @@ -0,0 +1,6 @@ +alter table ttrss_scheduled_tasks add column owner_uid integer default null references ttrss_users(id) ON DELETE CASCADE; +alter table ttrss_scheduled_tasks add column last_cron_expression varchar(250); + +update ttrss_scheduled_tasks set last_cron_expression = ''; + +alter table ttrss_scheduled_tasks alter column last_cron_expression set not null; diff --git a/sql/pgsql/schema.sql b/sql/pgsql/schema.sql index 3145629fc..21e3fd83a 100644 --- a/sql/pgsql/schema.sql +++ b/sql/pgsql/schema.sql @@ -400,6 +400,8 @@ create table ttrss_scheduled_tasks( task_name varchar(250) unique not null, last_duration integer not null, last_rc integer not null, - last_run timestamp not null default NOW()); + last_run timestamp not null default NOW(), + last_cron_expression varchar(250) not null, + owner_uid integer default null references ttrss_users(id) ON DELETE CASCADE); commit; |