perf: degrade a link-cache entry instead of dropping it on a failed send

`link_cache` exists so a repeat link costs nothing: no source request, no
download, no upload. It was written only on a *successful* send, and a send
that failed permanently deleted the entry — so the user's immediate retry, the
one case where they are most likely to try again, re-fetched everything:
site requests, a download, and for a ugoira or a bsky video a full ffmpeg
encode. Invalidation is right about the cause (the cached Telegram file id is
what went stale) and wrong about the cure (the media and its URLs are usually
fine).

Cached media now carries the source URL it was sent from, and a permanent
failure *degrades* the entry: the file ids are cleared, the URLs and the
caption fields stay, and the next request sends from those URLs — Telegram
fetches the media (or the upload fallback does) with no source round trip.
That is the same media a fresh fetch would have produced (site CDN URLs are
stable per post), and it is bounded: an entry that is already degraded, or one
from before this field existed, is removed instead, so a dead post still ends
up re-fetched and reported rather than retried forever.

Verified: a cached send that fails permanently leaves the entry with its URL
and no file id, a second failure drops it, and a degraded entry sends the
media with no fetch at all (the mock records no reply, which is what the
fetch-error path would have produced). 124 bot tests + 91 x-media tests pass,
including a direct test of the two payload shapes.

`cargo fmt --check`, `cargo clippy --workspace --all-targets --locked -- -D
warnings` and `cargo test --workspace --locked` clean.
This commit is contained in:
2026-09-21 14:35:57 +08:00
parent 62507d3a01
commit 64cf43dc01
6 changed files with 199 additions and 49 deletions
+8
View File
@@ -26,6 +26,12 @@ pub enum CachedMediaKind {
pub struct CachedMedia {
pub kind: CachedMediaKind,
pub file_id: String,
/// The media URL the send used, kept so an entry whose file ids stopped
/// working can still be re-sent without touching the source site (see the
/// bot's `invalidate_cache`). Empty for entries written before this field
/// existed — those can only be dropped and re-fetched.
#[serde(default)]
pub url: String,
}
/// Everything needed to re-send a post without touching the source site:
@@ -237,6 +243,8 @@ mod tests {
let got = got.unwrap();
assert_eq!(got.url, "https://x.com/u/status/1");
assert_eq!(got.media[0].file_id, "AgAC-file-id");
// The source URL rides along: it is what a degraded entry falls back to.
assert_eq!(got.media[0].url, "https://pbs.twimg.com/media/photo.jpg");
}
#[tokio::test]