diff options
Diffstat (limited to 'classes')
| -rw-r--r-- | classes/Config.php | 24 | ||||
| -rw-r--r-- | classes/Handler_Public.php | 14 | ||||
| -rw-r--r-- | classes/PluginHost.php | 1 | ||||
| -rw-r--r-- | classes/RPC.php | 78 | ||||
| -rw-r--r-- | classes/RSSUtils.php | 114 | ||||
| -rw-r--r-- | classes/Scheduler.php | 124 |
6 files changed, 215 insertions, 140 deletions
diff --git a/classes/Config.php b/classes/Config.php index c4176b7a8..6e16f269b 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: * @@ -53,13 +53,6 @@ class Config { * your tt-rss directory protected by other means (e.g. http auth). */ const SINGLE_USER_MODE = "SINGLE_USER_MODE"; - /** enables fallback update mode where tt-rss tries to update feeds in - * background while tt-rss is open in your browser. - * if you don't have a lot of feeds and don't want to or can't run - * background processes while not running tt-rss, this method is generally - * viable to keep your feeds up to date. */ - const SIMPLE_UPDATE_MODE = "SIMPLE_UPDATE_MODE"; - /** use this PHP CLI executable to start various tasks */ const PHP_EXECUTABLE = "PHP_EXECUTABLE"; @@ -205,7 +198,6 @@ class Config { Config::DB_PORT => [ "5432", Config::T_STRING ], Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ], Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ], - Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ], Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ], Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ], Config::CACHE_DIR => [ "cache", Config::T_STRING ], @@ -283,6 +275,13 @@ class Config { if (isset(self::_DEFAULTS[$const])) { $override = getenv(self::_ENVVAR_PREFIX . $const); + // cleanup original env var after importing (unless it's a background process) + if (php_sapi_name() != "cli") { + putenv(self::_ENVVAR_PREFIX . $const . '='); + unset($_ENV[self::_ENVVAR_PREFIX . $const]); + unset($_SERVER[self::_ENVVAR_PREFIX . $const]); + } + list ($defval, $deftype) = self::_DEFAULTS[$const]; $this->params[$cvalue] = [ self::cast_to($override !== false ? $override : $defval, $deftype), $deftype ]; @@ -441,6 +440,13 @@ class Config { private function _add(string $param, string $default, int $type_hint): void { $override = getenv(self::_ENVVAR_PREFIX . $param); + // cleanup original env var after importing (unless it's a background process) + if (php_sapi_name() != "cli") { + putenv(self::_ENVVAR_PREFIX . $param . '='); + unset($_ENV[self::_ENVVAR_PREFIX . $param]); + unset($_SERVER[self::_ENVVAR_PREFIX . $param]); + } + $this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ]; } diff --git a/classes/Handler_Public.php b/classes/Handler_Public.php index abff08376..bb3667e2a 100644 --- a/classes/Handler_Public.php +++ b/classes/Handler_Public.php @@ -360,20 +360,6 @@ class Handler_Public extends Handler { header('HTTP/1.1 403 Forbidden'); } - function updateTask(): void { - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK); - } - - function housekeepingTask(): void { - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); - } - - function globalUpdateFeeds(): void { - RPC::updaterandomfeed_real(); - - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK); - } - function login(): void { if (!Config::get(Config::SINGLE_USER_MODE)) { diff --git a/classes/PluginHost.php b/classes/PluginHost.php index 1ab9c0301..f61a5a9a4 100644 --- a/classes/PluginHost.php +++ b/classes/PluginHost.php @@ -906,4 +906,5 @@ class PluginHost { $ref = new ReflectionClass(get_class($plugin)); return basename(dirname(dirname($ref->getFileName()))) == "plugins.local"; } + } diff --git a/classes/RPC.php b/classes/RPC.php index a4a48242e..6ce2c12aa 100644 --- a/classes/RPC.php +++ b/classes/RPC.php @@ -82,8 +82,6 @@ class RPC extends Handler_Protected { WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); $sth->execute([...$ids, $_SESSION['uid']]); - Article::_purge_orphans(); - print json_encode(array("message" => "UPDATE_COUNTERS")); } @@ -252,81 +250,6 @@ class RPC extends Handler_Protected { print json_encode(["wide" => $wide]); } - static function updaterandomfeed_real(): void { - $default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL); - - // Test if the feed need a update (update interval exceded). - $update_limit_qpart = "AND (( - update_interval = 0 - AND (p.value IS NULL OR p.value != '-1') - AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL) - ) OR ( - update_interval > 0 - AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL) - ) OR ( - update_interval >= 0 - AND (p.value IS NULL OR p.value != '-1') - AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL) - ))"; - - // Test if feed is currently being updated by another process. - $updstart_thresh_qpart = 'AND (last_update_started IS NULL OR ' - . Db::past_comparison_qpart('last_update_started', '<', 5, 'minute') . ')'; - - $pdo = Db::pdo(); - - // we could be invoked from public.php with no active session - if (!empty($_SESSION["uid"])) { - $owner_check_qpart = "AND f.owner_uid = ".$pdo->quote($_SESSION["uid"]); - } else { - $owner_check_qpart = ""; - } - - $query = "SELECT f.feed_url,f.id - FROM - ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON - (p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL') - WHERE - f.owner_uid = u.id AND - u.access_level NOT IN (".sprintf("%d, %d", UserHelper::ACCESS_LEVEL_DISABLED, UserHelper::ACCESS_LEVEL_READONLY).") - $owner_check_qpart - $update_limit_qpart - $updstart_thresh_qpart - ORDER BY RANDOM() LIMIT 30"; - - $res = $pdo->query($query); - - $num_updated = 0; - - $tstart = time(); - - while ($line = $res->fetch()) { - $feed_id = $line["id"]; - - if (time() - $tstart < ini_get("max_execution_time") * 0.7) { - RSSUtils::update_rss_feed($feed_id, true); - ++$num_updated; - } else { - break; - } - } - - // Purge orphans and cleanup tags - Article::_purge_orphans(); - //cleanup_tags(14, 50000); - - if ($num_updated > 0) { - print json_encode(array("message" => "UPDATE_COUNTERS", - "num_updated" => $num_updated)); - } else { - print json_encode(array("message" => "NOTHING_TO_UPDATE")); - } - } - - function updaterandomfeed(): void { - self::updaterandomfeed_real(); - } - /** * @param array<int, int> $ids */ @@ -472,7 +395,6 @@ class RPC extends Handler_Protected { $params["num_feeds"] = (int) $num_feeds; $params["hotkeys"] = $this->get_hotkeys_map(); $params["widescreen"] = (int) Prefs::get(Prefs::WIDESCREEN_MODE, $_SESSION['uid'], $profile); - $params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE); $params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif"); $params["icon_oval"] = $this->image_to_base64("images/oval.svg"); $params["icon_three_dots"] = $this->image_to_base64("images/three-dots.svg"); diff --git a/classes/RSSUtils.php b/classes/RSSUtils.php index 20c4bd417..a051a7dc2 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') @@ -132,7 +129,6 @@ class RSSUtils { ))"; // Test if feed is currently being updated by another process. - // TODO: Update RPC::updaterandomfeed_real() to also use 10 minutes? $updstart_thresh_qpart = 'AND (last_update_started IS NULL OR ' . Db::past_comparison_qpart('last_update_started', '<', 10, 'minute') . ')'; @@ -288,9 +284,6 @@ class RSSUtils { self::housekeeping_user($owner_uid); } - // Send feed digests by email if needed. - Digest::send_headlines_digests(); - return $nf; } @@ -1432,15 +1425,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 +1643,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,20 +1711,80 @@ class RSSUtils { } } + /** 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...'); + + $scheduler = Scheduler::getInstance(); + + $scheduler->add_scheduled_task('purge_orphans', '@daily', + function() { + Article::_purge_orphans(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('disk_cache_expire_all', '@daily', + function() { + $cache = DiskCache::instance(""); + $cache->expire_all(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('disable_failed_feeds', '@daily', + function() { + self::disable_failed_feeds(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('migrate_feed_icons', '@daily', + function() { + self::migrate_feed_icons(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('cleanup_feed_icons', '@daily', + function() { + self::cleanup_feed_icons(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('expire_error_log', '@hourly', + function() { + self::expire_error_log(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('expire_lock_files', '@hourly', + function() { + self::expire_lock_files(); + + return 0; + } + ); + + $scheduler->add_scheduled_task('send_headlines_digests', '@hourly', + function() { + Digest::send_headlines_digests(); + + return 0; + } + ); + } + static function housekeeping_common(): void { - $cache = DiskCache::instance(""); - $cache->expire_all(); - - 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(); - - Article::_purge_orphans(); - self::cleanup_counters_cache(); + Scheduler::getInstance()->run_due_tasks(); PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING); } diff --git a/classes/Scheduler.php b/classes/Scheduler.php new file mode 100644 index 000000000..fd6301641 --- /dev/null +++ b/classes/Scheduler.php @@ -0,0 +1,124 @@ +<?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."); + } + + // TODO implement some sort of automatic cleanup for orphan task execution records +}
\ No newline at end of file |