| 1 | //! g1t-wide pauses (billing's `platform_pause`; docs/SPEND-GUARDRAILS.md), |
| 2 | //! read cheaply: one call to billing per isolate every 30 seconds at most, |
| 3 | //! never a database read per request. |
| 4 | //! |
| 5 | //! When billing cannot say, nothing is paused: a pause is a brake staff |
| 6 | //! (or billing's usage watcher) pull on purpose, and a billing outage must |
| 7 | //! not stop schedules and indexing everywhere. The failure is kept for the |
| 8 | //! same 30 seconds, so an outage is not asked about on every request. |
| 9 | |
| 10 | use std::cell::RefCell; |
| 11 | |
| 12 | use g1t_contracts::billing::{PauseLevel, PlatformPause}; |
| 13 | use worker::Fetcher; |
| 14 | |
| 15 | /// How long an answer is kept in the isolate. |
| 16 | pub const KEEP_MS: u64 = 30_000; |
| 17 | |
| 18 | thread_local! { |
| 19 | static KEPT: RefCell<Option<(PlatformPause, u64)>> = const { RefCell::new(None) }; |
| 20 | } |
| 21 | |
| 22 | /// A kept answer, while it is fresh. |
| 23 | fn fresh(kept: Option<(PlatformPause, u64)>, now: u64) -> Option<PlatformPause> { |
| 24 | kept.filter(|(_, until)| *until > now).map(|(pause, _)| pause) |
| 25 | } |
| 26 | |
| 27 | /// Whether `level` is paused, through the billing service's binding. |
| 28 | pub async fn paused(billing: &Fetcher, level: PauseLevel) -> bool { |
| 29 | current(billing).await.is(level) |
| 30 | } |
| 31 | |
| 32 | /// Every level, kept for `KEEP_MS`. Nothing paused when billing cannot say. |
| 33 | pub async fn current(billing: &Fetcher) -> PlatformPause { |
| 34 | let now = crate::now_ms(); |
| 35 | if let Some(pause) = KEPT.with(|kept| fresh(*kept.borrow(), now)) { |
| 36 | return pause; |
| 37 | } |
| 38 | let pause = match crate::call::<_, PlatformPause>(billing, "platform_pause", &serde_json::json!({})).await { |
| 39 | Ok(pause) => pause, |
| 40 | Err(error) => { |
| 41 | worker::console_error!("platform pause unreadable, so nothing is paused: {error}"); |
| 42 | PlatformPause::default() |
| 43 | } |
| 44 | }; |
| 45 | KEPT.with(|kept| *kept.borrow_mut() = Some((pause, now + KEEP_MS))); |
| 46 | pause |
| 47 | } |
| 48 | |
| 49 | #[cfg(test)] |
| 50 | mod tests { |
| 51 | use super::*; |
| 52 | |
| 53 | #[test] |
| 54 | fn an_answer_is_kept_thirty_seconds() { |
| 55 | let paused = PlatformPause { schedules: true, ..PlatformPause::default() }; |
| 56 | assert_eq!(fresh(Some((paused, 1_000 + KEEP_MS)), 1_000), Some(paused)); |
| 57 | assert_eq!(fresh(Some((paused, 1_000 + KEEP_MS)), 1_000 + KEEP_MS), None); |
| 58 | assert_eq!(fresh(None, 0), None); |
| 59 | } |
| 60 | |
| 61 | #[test] |
| 62 | fn an_unread_pause_pauses_nothing() { |
| 63 | let none = PlatformPause::default(); |
| 64 | assert!(PauseLevel::ALL.into_iter().all(|level| !none.is(level))); |
| 65 | assert!(!none.any()); |
| 66 | } |
| 67 | } |