diff options
| -rw-r--r-- | classes/Config.php | 2 | ||||
| -rw-r--r-- | classes/PluginHost.php | 90 | ||||
| -rw-r--r-- | classes/RSSUtils.php | 95 | ||||
| -rw-r--r-- | sql/pgsql/migrations/150.sql | 5 | ||||
| -rw-r--r-- | sql/pgsql/schema.sql | 7 |
5 files changed, 164 insertions, 35 deletions
diff --git a/classes/Config.php b/classes/Config.php index c4176b7a8..1afa4f043 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 = 149; + const SCHEMA_VERSION = 150; /** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX: * diff --git a/classes/PluginHost.php b/classes/PluginHost.php index bfc02318b..484364f26 100644 --- a/classes/PluginHost.php +++ b/classes/PluginHost.php @@ -32,6 +32,9 @@ class PluginHost { /** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */ private array $plugin_actions = []; + /** @var array<string, mixed> */ + private array $scheduled_tasks = []; + private ?int $owner_uid = null; private bool $data_loaded = false; @@ -906,4 +909,91 @@ class PluginHost { $ref = new ReflectionClass(get_class($plugin)); return basename(dirname(dirname($ref->getFileName()))) == "plugins.local"; } + + /** + * Adds a backend scheduled task which will be executed by updater (if due) when idle during + * RSSUtils::housekeeping_common(). + * + * 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 are not run in user context and are available to system plugins only. Task names may not + * overlap. + * + * 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 { + $cron = new Cron\CronExpression($cron_expression); + + $this->scheduled_tasks[$task_name] = [ + "cron" => $cron, + "callback" => $callback, + ]; + return true; + } + } + + /** + * Execute scheduled tasks which are due to run and record last run timestamps. + * @return void + */ + function run_due_tasks() { + Debug::log('Processing all scheduled tasks...'); + + $tasks_run = 0; + + foreach ($this->scheduled_tasks as $task_name => $task) { + $last_run = '1970-01-01 00:00'; + + $task_record = ORM::for_table('ttrss_scheduled_tasks') + ->where('task_name', $task_name) + ->find_one(); + + if ($task_record) + $last_run = $task_record->last_run; + + Debug::log("Checking scheduled task: $task_name, last run: $last_run"); + + if ($task['cron']->isDue($last_run)) { + Debug::log("Task $task_name is due, executing..."); + + $rc = (int) $task['callback'](); + + Debug::log("Task $task_name has finished with RC=$rc, recording timestamp..."); + + if ($task_record) { + $task_record->last_run = time(); + $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_rc' => $rc, + 'last_run' => Db::NOW(), + ]); + + $task_record->save(); + } + } + } + + Debug::log("Finished with $tasks_run tasks executed."); + } + + // TODO implement some sort of automatic cleanup for orphan task execution records + } diff --git a/classes/RSSUtils.php b/classes/RSSUtils.php index acf1d14e5..d46d73793 100644 --- a/classes/RSSUtils.php +++ b/classes/RSSUtils.php @@ -35,11 +35,6 @@ class RSSUtils { return sha1(implode(",", $pluginhost->get_plugin_names()) . $tmp); } - static function cleanup_feed_browser(): void { - $pdo = Db::pdo(); - $pdo->query("DELETE FROM ttrss_feedbrowser_cache"); - } - static function cleanup_feed_icons(): void { $pdo = Db::pdo(); $sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?"); @@ -81,6 +76,8 @@ class RSSUtils { die("Schema version is wrong, please upgrade the database.\n"); } + self::init_housekeeping_tasks(); + $pdo = Db::pdo(); $feeds_in_the_future = ORM::for_table('ttrss_feeds') @@ -1432,15 +1429,6 @@ class RSSUtils { WHERE ' . Db::past_comparison_qpart('created_at', '<', 7, 'day')); } - /** - * @deprecated table not used - */ - static function expire_feed_archive(): void { - $pdo = Db::pdo(); - - $pdo->query("DELETE FROM ttrss_archived_feeds"); - } - static function expire_lock_files(): void { Debug::log("Removing old lock files...", Debug::LOG_VERBOSE); @@ -1659,14 +1647,6 @@ class RSSUtils { mb_strtolower(strip_tags($title), 'utf-8')); } - /* counter cache is no longer used, if called truncate leftover data */ - static function cleanup_counters_cache(): void { - $pdo = Db::pdo(); - - $pdo->query("DELETE FROM ttrss_counters_cache"); - $pdo->query("DELETE FROM ttrss_cat_counters_cache"); - } - static function disable_failed_feeds(): void { if (Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT) > 0) { @@ -1735,21 +1715,68 @@ class RSSUtils { } } - static function housekeeping_common(): void { - $cache = DiskCache::instance(""); - $cache->expire_all(); + /** Init all system tasks which are run periodically by updater in housekeeping_common() */ + static function init_housekeeping_tasks() : void { + Debug::log('Registering scheduled tasks for housekeeping...'); + + PluginHost::getInstance()->add_scheduled_task('purge_orphans', '@daily', + function() { + Article::_purge_orphans(); return 0; + } + ); + + PluginHost::getInstance()->add_scheduled_task('disk_cache_expire_all', '@daily', + function() { + $cache = DiskCache::instance(""); + $cache->expire_all(); + + return 0; + } + ); + + PluginHost::getInstance()->add_scheduled_task('expire_error_log', '@hourly', + function() { + self::expire_error_log(); + + return 0; + } + ); - self::migrate_feed_icons(); - self::expire_lock_files(); - self::expire_error_log(); - self::expire_feed_archive(); - self::cleanup_feed_browser(); - self::cleanup_feed_icons(); - self::disable_failed_feeds(); + PluginHost::getInstance()->add_scheduled_task('expire_lock_files', '@hourly', + function() { + self::expire_lock_files(); - Article::_purge_orphans(); - self::cleanup_counters_cache(); + return 0; + } + ); + + PluginHost::getInstance()->add_scheduled_task('disable_failed_feeds', '@daily', + function() { + self::disable_failed_feeds(); + + return 0; + } + ); + PluginHost::getInstance()->add_scheduled_task('migrate_feed_icons', '@daily', + function() { + self::migrate_feed_icons(); + + return 0; + } + ); + + PluginHost::getInstance()->add_scheduled_task('cleanup_feed_icons', '@daily', + function() { + self::cleanup_feed_icons(); + + return 0; + } + ); + } + + static function housekeeping_common(): void { + PluginHost::getInstance()->run_due_tasks(); PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); } diff --git a/sql/pgsql/migrations/150.sql b/sql/pgsql/migrations/150.sql new file mode 100644 index 000000000..55d9609e9 --- /dev/null +++ b/sql/pgsql/migrations/150.sql @@ -0,0 +1,5 @@ +create table ttrss_scheduled_tasks( + id serial not null primary key, + task_name varchar(250) unique not null, + last_rc integer not null, + last_run timestamp not null default NOW()); diff --git a/sql/pgsql/schema.sql b/sql/pgsql/schema.sql index acfa619c9..3ff9a6674 100644 --- a/sql/pgsql/schema.sql +++ b/sql/pgsql/schema.sql @@ -1,3 +1,4 @@ +drop table if exists ttrss_scheduled_tasks; drop table if exists ttrss_error_log; drop table if exists ttrss_plugin_storage; drop table if exists ttrss_linked_feeds; @@ -394,4 +395,10 @@ create table ttrss_error_log( context text not null, created_at timestamp not null); +create table ttrss_scheduled_tasks( + id serial not null primary key, + task_name varchar(250) unique not null, + last_rc integer not null, + last_run timestamp not null default NOW()); + commit; |