Skip to main content

decdn_cache/
probe_hold.rs

1//! Probe-triggered eviction-hold constants (ADR 005 §Probe-triggered
2//! eviction hold, §Hold budget; ADR 040 §Pinning, durable operator-evict, and
3//! the probe-hold stay engine-enforced).
4//!
5//! When a node signs `has_blob: true` in a `ProbeResponse` it tries to keep the
6//! blob eviction-exempt for [`PROBE_HOLD_DURATION`] so it is still resident when
7//! the probing client's follow-up pull arrives. The hold is **best-effort**, not
8//! a guarantee: presence governs the advertisement, and a hold is placed only if
9//! a slot is free (see [`ProbeHoldOutcome::BudgetExhausted`]). All three timing
10//! parameters derive from a single base value [`PROBE_SLASH_WINDOW`], which
11//! remains the on-chain rate-manipulation probe↔stream window (ADR 014).
12
13use std::time::Duration;
14
15/// The on-chain probe↔stream window `SlashJudge` enforces for rate
16/// manipulation (ADR 014; surfaced in ADR 005 §Derived constants). The single
17/// base value the other timing parameters derive from.
18pub const PROBE_SLASH_WINDOW: Duration = Duration::from_secs(30);
19
20/// Extra margin over [`PROBE_SLASH_WINDOW`] absorbing network latency between
21/// signing and the requester opening a stream.
22pub const PROBE_HOLD_MARGIN: Duration = Duration::from_secs(5);
23
24/// How long a blob stays eviction-exempt after the node signs
25/// `has_blob: true` for it: [`PROBE_SLASH_WINDOW`] plus [`PROBE_HOLD_MARGIN`]
26/// (35s) — long enough to cover the probe→pull round trip. Derived from the
27/// slashing window because the two share a timescale, not because a hold has
28/// any slashing consequence (ADR 005 §Derived constants).
29pub const PROBE_HOLD_DURATION: Duration = PROBE_SLASH_WINDOW.saturating_add(PROBE_HOLD_MARGIN);
30
31/// Default maximum number of concurrently held (eviction-exempt) blobs (ADR
32/// 005 §Hold budget). Holds are per-blob: many peers probing one hash share a
33/// single slot. The canonical definition lives in the
34/// `decdn-config-types` leaf crate (#578) so the config default
35/// (`decdn_common::config`) and the cache engine default cannot drift apart;
36/// re-exported here as `crate::probe_hold::DEFAULT_MAX_PROBE_HOLDS`.
37pub use decdn_config_types::DEFAULT_MAX_PROBE_HOLDS;
38
39/// Outcome of [`crate::CacheEngine::try_probe_hold`], carrying two independent
40/// facts (ADR 005 §Probe-triggered eviction hold):
41///
42/// - **whether to advertise** the blob — [`ProbeHoldOutcome::advertises`];
43/// - **whether a hold was actually placed** — [`ProbeHoldOutcome::hold_placed`].
44///
45/// Holds are best-effort, so these are two independent bits:
46/// [`Self::BudgetExhausted`] advertises *without* holding. Use the two
47/// predicates rather than matching on variants, so the mapping stays in one
48/// place; the variants themselves exist so the probe handler can emit the right
49/// metric without a second cache lookup.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ProbeHoldOutcome {
52    /// Blob present (and not operator-evicted) **and** an eviction hold was
53    /// placed, so the blob stays resident for the follow-up pull — sign
54    /// `has_blob: true`.
55    Held,
56    /// Blob absent or operator-evicted — sign `has_blob: false`. Not a
57    /// degradation: the node simply does not have the content.
58    ///
59    /// Deliberately the one outcome with **no** metric. The
60    /// `probe_hold_unavailable` counter records hold *refusals* for content the
61    /// node has; a true negative is not a refusal, and counting it would swamp
62    /// the signal every alert on that metric depends on. Despite the shared
63    /// word, this is not `ProbeHoldUnavailableReason` — that enum's values all
64    /// mean "we have it but would not hold it".
65    Unavailable,
66    /// Blob present but **all hold slots are in use** (`max_probe_holds > 0`
67    /// and the live-hold count has reached it). The caller still advertises
68    /// `has_blob: true` but places no hold, so the blob may be LRU-evicted
69    /// before the pull — costing one wasted round trip, never a slash. Counted
70    /// as `probe_hold_unavailable{reason="exhausted"}` (ADR 005 §Hold budget);
71    /// the operator-actionable remedy for the lost holds is to raise
72    /// `max_probe_holds`.
73    BudgetExhausted,
74    /// Blob present but the eviction-hold path is **disabled by config**
75    /// (`max_probe_holds == 0`) — the operator opt-out from advertising
76    /// store-backed content, so the caller signs `has_blob: false`.
77    /// Operationally distinct from [`Self::BudgetExhausted`] (#739): a
78    /// deliberate choice, not load. Counted as
79    /// `probe_hold_unavailable{reason="disabled"}`, never as
80    /// `{reason="exhausted"}` (whose alert remedy is "increase
81    /// `max_probe_holds`", nonsensical when holds are deliberately off).
82    ///
83    /// Scope note: this silences *store-backed* advertisements only. Content
84    /// servable from a configured origin takes no hold and is advertised
85    /// regardless — see the origin-held fallback in the probe handler.
86    HoldsDisabled,
87}
88
89impl ProbeHoldOutcome {
90    /// Whether this outcome authorises signing `has_blob: true` for the
91    /// **store-backed** answer (the origin-held fallback may still promote a
92    /// `false` afterwards).
93    ///
94    /// Presence governs the answer, not the hold: [`Self::BudgetExhausted`]
95    /// advertises despite placing no hold, because refusing there would let a
96    /// probe flood that fills the hold cache suppress truthful answers.
97    #[must_use]
98    pub const fn advertises(self) -> bool {
99        matches!(self, Self::Held | Self::BudgetExhausted)
100    }
101
102    /// Whether an eviction hold was actually placed, making the blob
103    /// LRU-exempt for [`PROBE_HOLD_DURATION`].
104    ///
105    /// Distinct from [`Self::advertises`]: holds are best-effort, and only
106    /// [`Self::Held`] consumes a slot.
107    #[must_use]
108    pub const fn hold_placed(self) -> bool {
109        matches!(self, Self::Held)
110    }
111}