Skip to main content

weixin_agent/api/
config_cache.rs

1//! In-memory config cache for `typing_ticket` with TTL and exponential backoff retry.
2
3use std::sync::Arc;
4
5use dashmap::DashMap;
6
7use crate::api::client::HttpApiClient;
8use crate::types::CONFIG_CACHE_TTL_MS;
9
10const INITIAL_RETRY_MS: u64 = 2_000;
11const MAX_RETRY_MS: u64 = 3_600_000;
12
13struct CacheEntry {
14    typing_ticket: String,
15    next_fetch_at: u64,
16    retry_delay_ms: u64,
17}
18
19/// Per-user `getConfig` cache with periodic refresh and exponential-backoff retry.
20pub(crate) struct ConfigCache {
21    api: Arc<HttpApiClient>,
22    cache: DashMap<String, CacheEntry>,
23}
24
25impl ConfigCache {
26    /// Create a new cache backed by the given API client.
27    pub fn new(api: Arc<HttpApiClient>) -> Self {
28        Self {
29            api,
30            cache: DashMap::new(),
31        }
32    }
33
34    /// Get the cached `typing_ticket` for a user, refreshing if stale.
35    pub async fn get_typing_ticket(
36        &self,
37        user_id: &str,
38        context_token: Option<&str>,
39    ) -> Option<String> {
40        let now = now_ms();
41        let should_fetch = self
42            .cache
43            .get(user_id)
44            .is_none_or(|e| now >= e.next_fetch_at);
45
46        if should_fetch {
47            match self.api.get_config(user_id, context_token).await {
48                Ok(resp) if resp.ret.unwrap_or(-1) == 0 => {
49                    let ticket = resp.typing_ticket.unwrap_or_default();
50                    // Jitter within TTL for staggered refresh
51                    #[allow(
52                        clippy::cast_possible_truncation,
53                        clippy::cast_sign_loss,
54                        clippy::cast_precision_loss
55                    )]
56                    let jitter = (rand::random::<f64>() * CONFIG_CACHE_TTL_MS as f64) as u64;
57                    self.cache.insert(
58                        user_id.to_owned(),
59                        CacheEntry {
60                            typing_ticket: ticket,
61                            next_fetch_at: now + jitter,
62                            retry_delay_ms: INITIAL_RETRY_MS,
63                        },
64                    );
65                }
66                _ => {
67                    // On failure, apply exponential backoff
68                    let mut entry = self.cache.entry(user_id.to_owned()).or_insert(CacheEntry {
69                        typing_ticket: String::new(),
70                        next_fetch_at: now + INITIAL_RETRY_MS,
71                        retry_delay_ms: INITIAL_RETRY_MS,
72                    });
73                    let next_delay = (entry.retry_delay_ms * 2).min(MAX_RETRY_MS);
74                    entry.next_fetch_at = now + next_delay;
75                    entry.retry_delay_ms = next_delay;
76                }
77            }
78        }
79
80        self.cache.get(user_id).map(|e| e.typing_ticket.clone())
81    }
82}
83
84use crate::util::now_ms;