From f950c1ea52be062b80af6793d69442a8cc382362 Mon Sep 17 00:00:00 2001 From: Petros Koutsolampros Date: Fri, 12 Jun 2026 08:31:38 +0300 Subject: [PATCH] Add a catchable soft time limit per feed update When a feed is too big it can take a long time to parse. This is not necessarily a problem, but there's currently a 30 second limit on the parsing (so that it doesn't get stuck forever). The problem with set_time_limit is that it's a non-catchable error, so if one of the feeds does trigger it then the rest of the feeds in the update iteration do not get updated and there's no way to recover from the error to continue. Because the failing feed is also not marked as completed it will be retried next time and any feeds after it will keep failing as well. This change adds a catchable soft time limit at 30s and increases the hard php uncatchable limit to 120. The soft limit is tested when fetching every single item in a feed so if we get past it we can gently exit the feed update with a timeout error allowing the other updates to continue. It's still possible to hit the hard limit if an item (not the whole feed) takes too long to parse, but this would require even deeper checking against the soft limit --- server/lib/OPodSync/Feed.php | 13 ++++++++++++- server/lib/OPodSync/GPodder.php | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/server/lib/OPodSync/Feed.php b/server/lib/OPodSync/Feed.php index e54c6ab..fcabfa8 100644 --- a/server/lib/OPodSync/Feed.php +++ b/server/lib/OPodSync/Feed.php @@ -54,7 +54,7 @@ public function sync(): void $db->exec('END'); } - public function fetch(): bool + public function fetch(?int $soft_timeout = null): bool { if (function_exists('curl_exec')) { $ch = curl_init($this->feed_url); @@ -101,7 +101,18 @@ public function fetch(): bool return false; } + // Soft time budget for parsing this feed, started after the feed downloads + // so we measure parse time, not network wait. This is useful in cases + // where this is called in an iteration fetching all feeds - especially + // if that iteration has a process fail limit say using set_time_limit() + $deadline = $soft_timeout !== null ? microtime(true) + $soft_timeout : null; + while (preg_match('!]*>(.*?)!s', $body, $match)) { + if ($deadline !== null && microtime(true) > $deadline) { + error_log(sprintf('oPodSync: parsing feed %s exceeded the %ds budget, skipping', $this->feed_url, $soft_timeout)); + return false; + } + $body = str_replace($match[0], '', $body); $item = $match[1]; $pubdate = $this->getTagValue($item, 'pubDate'); diff --git a/server/lib/OPodSync/GPodder.php b/server/lib/OPodSync/GPodder.php index 1d1cf5b..82055a9 100644 --- a/server/lib/OPodSync/GPodder.php +++ b/server/lib/OPodSync/GPodder.php @@ -261,7 +261,7 @@ public function listEpisodes(int $subscription): array ORDER BY e.pubdate DESC;', $subscription, $this->user->id); } - public function updateFeedForSubscription(int $subscription): ?Feed + public function updateFeedForSubscription(int $subscription, ?int $soft_timeout = null): ?Feed { $db = DB::getInstance(); $url = $db->firstColumn('SELECT url FROM subscriptions WHERE id = ?;', $subscription); @@ -272,7 +272,7 @@ public function updateFeedForSubscription(int $subscription): ?Feed $feed = new Feed($url); - if (!$feed->fetch()) { + if (!$feed->fetch($soft_timeout)) { return null; } @@ -361,8 +361,18 @@ public function updateAllFeeds(bool $cli = false): void $db = DB::getInstance(); + // Two time limits per feed. A soft one allowing 30s per feed and failing + // gracefully allowing the iteration to continue and a hard one to allow + // the process (and the iteration) to completely fail if a feed takes even + // longer to respond (for example for an unbounded feed item). This is + // necessary because it is not possible to catch the failure from + // set_time_limit() thus some feeds after the failure will never get updated. + $soft_timeout = 30; + $hard_time_limit = 120; + foreach ($db->iterate($sql) as $row) { - @set_time_limit(30); // Extend running time; + // set_time_limit() restarts the timer, so reset the hard limit per feed + @set_time_limit($hard_time_limit); if ($cli) { printf("Updating %s\n", $row->url); @@ -373,7 +383,7 @@ public function updateAllFeeds(bool $cli = false): void flush(); } - $this->updateFeedForSubscription($row->subscription); + $this->updateFeedForSubscription($row->subscription, $soft_timeout); $i++; }