Idempotency Key Replay Store

Track in-flight and completed requests by idempotency key, replay stored responses within a TTL, and release abandoned work.

RustResilience

Code

snippet
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

#[derive(Clone)]
struct CachedResponse {
    status: u16,
    body: String,
    stored_at: Instant,
}

enum Entry {
    InProgress,
    Done(CachedResponse),
}

enum Outcome {
    Fresh,
    InProgress,
    Replay(CachedResponse),
}

struct IdempotencyStore {
    entries: Mutex<HashMap<String, Entry>>,
    ttl: Duration,
}

impl IdempotencyStore {
    fn new(ttl: Duration) -> Self {
        Self {
            entries: Mutex::new(HashMap::new()),
            ttl,
        }
    }

    fn begin(&self, key: &str) -> Outcome {
        let mut entries = self.entries.lock().expect("store poisoned");
        let replay = match entries.get(key) {
            Some(Entry::InProgress) => return Outcome::InProgress,
            Some(Entry::Done(cached)) if cached.stored_at.elapsed() < self.ttl => {
                Some((*cached).clone())
            }
            _ => None,
        };
        if let Some(cached) = replay {
            return Outcome::Replay(cached);
        }
        entries.insert(key.to_string(), Entry::InProgress);
        Outcome::Fresh
    }

    fn complete(&self, key: &str, status: u16, body: &str) {
        let mut entries = self.entries.lock().expect("store poisoned");
        entries.insert(
            key.to_string(),
            Entry::Done(CachedResponse {
                status,
                body: body.to_string(),
                stored_at: Instant::now(),
            }),
        );
    }

    fn abandon(&self, key: &str) {
        self.entries
            .lock()
            .expect("store poisoned")
            .remove(key);
    }
}

fn main() {
    let store = IdempotencyStore::new(Duration::from_secs(60));

    match store.begin("order-1") {
        Outcome::Fresh => println!("first request: fresh"),
        _ => println!("unexpected first outcome"),
    }
    match store.begin("order-1") {
        Outcome::InProgress => println!("duplicate while running: in progress"),
        _ => println!("unexpected duplicate outcome"),
    }
    store.complete("order-1", 201, "{\"id\":1}");
    match store.begin("order-1") {
        Outcome::Replay(cached) => println!("replay: {} {}", cached.status, cached.body),
        _ => println!("unexpected replay outcome"),
    }
    store.abandon("order-2");
}