Try a feed again in minutes when its host could not be looked up for the moment (#143)

205 of the Directory's 208 feeds without a category had never been read: on 2026-10-05 at 03:24
UTC every one failed with "dns error: ... Temporary failure in name resolution" within a minute,
during an outage on Tower that also stopped iPX's log reaching Loki until Tower rebooted at 11:56.
They are working podcasts, NPR's, Megaphone's, Acast's, which resolve now. A feed nobody
subscribes to is read once a day and a failing one backs off up to a day (#99), so each sat
failing, unread and so uncategorised (#117) until the next day, and explain_failure called every
DNS error "This address no longer resolves; the site is gone.", on its page and since #142 on its
Directory page. A forced check of the 208 read 205; each has its items, its description and its
own category now.

A temporary lookup failure, EAI_AGAIN, is not recorded on the feed at all: no last_error, no
error_since to back off from or call it gone by. The daemon notes it in Ctx::retry and the feed
is due again in ten minutes, which the scan's due check and the sleep until the next scan both
honour; a restart forgets it, which only makes the retry sooner. A name that does not exist is
still the feed's error, explained as "may be gone", and the Directory page, like the feed's own,
names a cause only once a feed has been failing a day; before that its last check failed.

The page also drops "Nobody here subscribes yet", which said nothing worth a line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 00:18:42 +00:00
parent c1b73b3dbe
commit 4f4f2e8d95
6 changed files with 104 additions and 19 deletions

View File

@@ -1029,10 +1029,15 @@ struct Listed {
items: Vec<ListedItem>,
}
/// A feed's last error as its Directory page says it: explained, or that the check failed, never
/// the error as it stands, which can name the feed's address, and the Directory names none.
fn failure_words(err: &str) -> String {
crate::feed::explain_failure(err).map_or_else(|| "Its last check failed.".to_owned(), |f| f.reason.to_owned())
/// A feed's last error as its Directory page says it: explained once it has been failing a day, as
/// the feed's own page does, or else that the check failed; never the error as it stands, which can
/// name the feed's address, and the Directory names none. A day, so that a site down for an hour,
/// or a resolver for a minute, is not called gone (#143).
fn failure_words(err: &str, failing_for: i64) -> String {
Some(err)
.filter(|_| failing_for >= FLAG_AFTER_SECS)
.and_then(crate::feed::explain_failure)
.map_or_else(|| "Its last check failed.".to_owned(), |f| f.reason.to_owned())
}
/// A listed feed's description and latest twenty items. Only a feed the Directory lists, so a
@@ -1049,7 +1054,9 @@ async fn get_listed(
let rows = state.ctx.db.entries_in(user.id, Some(&id), crate::db::Filter::parse("all"), None, 0, 20, &order).await?;
let mut sanitizer = feed_sanitizer();
let description = state.ctx.db.feed_description(&id).await?.map(|d| clean_description(&mut sanitizer, &d, None));
let failing = state.ctx.db.feed_summary(&id).await?.last_error.as_deref().map(failure_words);
let summary = state.ctx.db.feed_summary(&id).await?;
let failing_for = summary.error_since.map_or(0, |since| crate::db::now() - since);
let failing = summary.last_error.as_deref().map(|e| failure_words(e, failing_for));
let items = rows
.into_iter()
.map(|e| ListedItem {
@@ -1157,10 +1164,14 @@ mod tests {
#[test]
fn a_failing_feed_says_why_in_the_directory_without_its_address() {
assert_eq!(failure_words("the feed's address answered with nothing at all"), "This address answers with nothing; the site may be gone.");
let day = FLAG_AFTER_SECS;
let nothing = "the feed's address answered with nothing at all";
assert_eq!(failure_words(nothing, day), "This address answers with nothing; the site may be gone.");
// Not before it has been failing a day, as on the feed's own page (#143).
assert_eq!(failure_words(nothing, 3600), "Its last check failed.");
// reqwest names the URL; the Directory names none.
let raw = "error sending request for url (http://daily-quests.com/comic/?feed=atom): connection closed";
assert_eq!(failure_words(raw), "Its last check failed.");
assert_eq!(failure_words(raw, day), "Its last check failed.");
}
#[test]