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++; }