summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rw-r--r--classes/Config.php24
-rw-r--r--classes/Handler_Public.php14
-rw-r--r--classes/PluginHost.php1
-rw-r--r--classes/RPC.php78
-rw-r--r--classes/RSSUtils.php114
-rw-r--r--classes/Scheduler.php124
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