From 670351d436f66b172838b8e28a841fd2e82c62f4 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Mon, 21 Sep 2026 14:38:26 +0800 Subject: [PATCH] fix: let a successful send return a degraded entry to the fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Degrading a link-cache entry (previous commit) closed the "user retries right after a failure" case, but left a new one open: the entry could never regain file ids. `cache_sent_task` skipped *every* cached send — its rule was "the entry already holds the ids the next repeat wants" — which is true for a healthy entry and false for a degraded one. So a degraded entry (pixiv's hotlink-protected media, say) kept sending by URL forever, and every repeat paid a download and an upload through the fallback that the file ids would have avoided. Worse than the re-fetch it replaced. The rule is now the one it always meant: a send whose cache snapshot still carries file ids leaves the entry alone, and a send served from a degraded entry writes back the ids it produced (the URLs in the rewritten entry come from that send's own items, so they stay correct). Verified by a test that drives `cache_sent_task` directly with both task shapes: the degraded one updates the entry to the fresh id, the healthy one leaves it untouched. 125 bot tests + 91 x-media tests, live suite (14) pass. `cargo fmt --check`, `cargo clippy --workspace --all-targets --locked -- -D warnings` and `cargo test --workspace --locked` clean. --- crates/xmedia-bot/src/send/mod.rs | 63 ++++++++++++++++++++++++- crates/xmedia-bot/src/send/post_send.rs | 10 ++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/crates/xmedia-bot/src/send/mod.rs b/crates/xmedia-bot/src/send/mod.rs index 967c224..9a6ca89 100644 --- a/crates/xmedia-bot/src/send/mod.rs +++ b/crates/xmedia-bot/src/send/mod.rs @@ -787,7 +787,7 @@ pub async fn forward_messages(ctx: &AppContext<'_>, task: &Task) -> Result<(), S #[cfg(test)] mod tests { - use super::post_send::build_edit_markup; + use super::post_send::{build_edit_markup, cache_sent_task}; use super::upload::sniff_ext; use super::*; use crate::ctx::test_support::{TestStores, cached_photo}; @@ -1645,6 +1645,67 @@ mod tests { } } + #[tokio::test] + async fn a_degraded_entry_regains_the_file_ids_a_send_produced() { + let sender = MockSender::scripted(vec![], media_fetch_error); + let stores = TestStores::new(); + let ctx = stores.ctx(&sender); + // A degraded entry: no file ids, URL only (what `invalidate_cache` + // leaves behind). + let mut degraded = cached_photo(); + degraded.media[0].file_id.clear(); + stores.link_cache().put("twitter:1", °raded).await; + let mut task = cached_sequence_task(); + if let Task::SendMediaSequence { cache_data, .. } = &mut task { + *cache_data = Some(degraded.clone()); + } + + cache_sent_task( + &ctx, + &task, + vec![CachedMedia { + kind: CachedMediaKind::Photo, + file_id: "fresh-id".into(), + url: "https://pbs.twimg.com/media/photo.jpg".into(), + }], + ) + .await; + + let entry = stores + .link_cache() + .get("twitter:1", Duration::from_secs(3600)) + .await + .expect("the entry must still be there"); + assert_eq!( + entry.media[0].file_id, "fresh-id", + "a degraded entry must take the ids its send produced" + ); + + // A send served from a healthy entry must not rewrite it: the ids it + // already holds are exactly what the next repeat wants. Which of the + // two a send was is the *task's* cache snapshot — a healthy one carries + // file ids. + cache_sent_task( + &ctx, + &cached_sequence_task(), + vec![CachedMedia { + kind: CachedMediaKind::Photo, + file_id: "other-id".into(), + url: String::new(), + }], + ) + .await; + let entry = stores + .link_cache() + .get("twitter:1", Duration::from_secs(3600)) + .await + .unwrap(); + assert_eq!( + entry.media[0].file_id, "fresh-id", + "a healthy entry is left alone" + ); + } + #[tokio::test] async fn settled_sent_keeps_the_cache_entry() { let sender = MockSender::scripted(vec![], media_fetch_error); diff --git a/crates/xmedia-bot/src/send/post_send.rs b/crates/xmedia-bot/src/send/post_send.rs index 174b324..56cf4c2 100644 --- a/crates/xmedia-bot/src/send/post_send.rs +++ b/crates/xmedia-bot/src/send/post_send.rs @@ -15,13 +15,17 @@ use std::collections::HashMap; use std::sync::LazyLock; use teloxide::types::{ChatId, InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId}; -/// Persists a successful send under the post's cache key. Only runs for a -/// fresh (non-resumed) task that carried raw cache data with no file ids yet. +/// Persists a successful send under the post's cache key. Skips a send that was +/// served from the cache — its entry already holds the file ids the next repeat +/// wants — *unless* the entry was degraded (no file ids left, see +/// `invalidate_cache`): then the ids this send just produced are written back, +/// which is what returns a degraded entry to the fast path instead of leaving +/// it to re-upload the media on every repeat. pub(super) async fn cache_sent_task(ctx: &AppContext<'_>, task: &Task, media: Vec) { let Some(cache_data) = task.cache_data() else { return; }; - if !cache_data.media.is_empty() || media.is_empty() { + if cache_data.media.iter().any(|m| !m.file_id.is_empty()) || media.is_empty() { return; } let mut post = cache_data.clone();