pub struct ChallengeMemory { /* private fields */ }Expand description
Capacity-bounded LRU+TTL store of ChallengeMemoryEntrys
keyed by EngineKey.
The store reuses the same LRU+TTL primitive the investigation
cache and the PoW / token-nonce stores use (the shared
crate::cache::LruTtlStore type — not re-exported). That keeps
eviction + expiry semantics consistent across every short-horizon
store in the crate and satisfies the “no new cache store” rule.
§Why engine-keyed?
The primary key is the engine (Akamai, Cloudflare, DataDome,
…), not the URL. A self-healing patch recorded against one
URL on one engine heals every URL on that engine — a captcha
workaround learned on example.com/cloudflare/page1 is
immediately applied to example.com/cloudflare/page2 and to
every other Cloudflare-fronted URL the runner sees. The URL
is kept on each entry only as a secondary debugging index
(see ChallengeMemoryEntry::last_observed_url).
§Example
use stygian_charon::challenge_feedback::{ChallengeMemory, ChallengeOutcome, EngineKey};
use stygian_charon::types::TargetClass;
use stygian_charon::vendor_classifier::VendorId;
use std::num::NonZeroUsize;
use std::time::Duration;
let memory =
ChallengeMemory::new(NonZeroUsize::new(8).expect("non-zero"), Duration::from_mins(5));
let key = EngineKey {
engine: VendorId::Cloudflare,
version: None,
target_class: TargetClass::ContentSite,
tls_profile: None,
};
memory.record(&key, Some("https://example.com/a"), ChallengeOutcome::Captcha);
let entry = memory.lookup(&key).expect("entry");
assert_eq!(entry.last_outcome, ChallengeOutcome::Captcha);
assert_eq!(entry.observation_count, 1);Implementations§
Source§impl ChallengeMemory
impl ChallengeMemory
Sourcepub fn new(capacity: NonZeroUsize, ttl: Duration) -> Self
pub fn new(capacity: NonZeroUsize, ttl: Duration) -> Self
Create a new challenge memory with explicit capacity and TTL.
Sourcepub fn with_default_ttl(capacity: NonZeroUsize) -> Self
pub fn with_default_ttl(capacity: NonZeroUsize) -> Self
Create a new challenge memory with
DEFAULT_CHALLENGE_CAPACITY and DEFAULT_CHALLENGE_TTL.
Sourcepub fn with_defaults() -> Self
pub fn with_defaults() -> Self
Capacity-bounded ChallengeMemory with the default
capacity and TTL.
Sourcepub fn record(
&self,
key: &EngineKey,
observed_url: Option<&str>,
outcome: ChallengeOutcome,
)
pub fn record( &self, key: &EngineKey, observed_url: Option<&str>, outcome: ChallengeOutcome, )
Record a challenge outcome for an EngineKey and
optionally the URL the outcome was observed on. The URL is
stored only as a secondary debugging index — the primary
key is the engine.
The read-modify-write sequence (peek current observation
count → build new entry → put) is atomic under
concurrency: two simultaneous record calls always observe
each other’s prior increments. The underlying LRU+TTL store
exposes a mutate(key, f) primitive that holds the mutex
across the read-modify-write — see the
stygian_charon::cache module for the implementation.
Expired entries start a fresh observation at count=1 (the
underlying mutate evicts expired entries first).
§Example
use stygian_charon::challenge_feedback::{ChallengeMemory, ChallengeOutcome, EngineKey};
use stygian_charon::types::TargetClass;
use stygian_charon::vendor_classifier::VendorId;
let memory = ChallengeMemory::with_defaults();
let key = EngineKey {
engine: VendorId::Cloudflare,
version: None,
target_class: TargetClass::Api,
tls_profile: None,
};
memory.record(&key, None, ChallengeOutcome::Pass);
let entry = memory.lookup(&key).expect("entry");
assert_eq!(entry.last_outcome, ChallengeOutcome::Pass);
assert_eq!(entry.observation_count, 1);Sourcepub fn lookup(&self, key: &EngineKey) -> Option<ChallengeMemoryEntry>
pub fn lookup(&self, key: &EngineKey) -> Option<ChallengeMemoryEntry>
Look up the current entry for an EngineKey. Returns
None if the key is absent or has expired.
§Example
use stygian_charon::challenge_feedback::{ChallengeMemory, EngineKey};
use stygian_charon::types::TargetClass;
use stygian_charon::vendor_classifier::VendorId;
let memory = ChallengeMemory::with_defaults();
let key = EngineKey {
engine: VendorId::DataDome,
version: None,
target_class: TargetClass::HighSecurity,
tls_profile: None,
};
assert!(memory.lookup(&key).is_none());Sourcepub fn invalidate(&self, key: &EngineKey)
pub fn invalidate(&self, key: &EngineKey)
Invalidate a single EngineKey.