rust 80 lines · 3 tabs

Thread-Safe Request Metrics with Arc, Mutex, and a Poison-Recovery Guard

Shared by codesnips Jul 2026
3 tabs
use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};

#[derive(Default)]
struct Metrics {
    per_endpoint: HashMap<String, u64>,
    total: u64,
}

#[derive(Clone)]
pub struct SharedMetrics {
    inner: Arc<Mutex<Metrics>>,
}

impl SharedMetrics {
    pub fn new() -> Self {
        SharedMetrics {
            inner: Arc::new(Mutex::new(Metrics::default())),
        }
    }

    pub fn record(&self, endpoint: &str) {
        let mut guard = self
            .inner
            .lock()
            .unwrap_or_else(PoisonError::into_inner);
        *guard.per_endpoint.entry(endpoint.to_string()).or_insert(0) += 1;
        guard.total += 1;
    }

    pub fn snapshot(&self) -> (Vec<(String, u64)>, u64) {
        let guard = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
        let mut rows: Vec<(String, u64)> =
            guard.per_endpoint.iter().map(|(k, v)| (k.clone(), *v)).collect();
        rows.sort_by(|a, b| b.1.cmp(&a.1));
        (rows, guard.total)
    }
}
3 files · rust Explain with highlit

This snippet builds a small in-memory metrics collector that several worker threads update concurrently. The central concept is Rust's shared-ownership-plus-locking idiom: Arc<Mutex<T>>. Arc (atomically reference-counted) lets multiple threads own the same allocation, and Mutex guarantees that only one thread mutates the inner data at a time. Neither alone is enough — Arc<T> gives shared reads but not mutation, and a bare Mutex cannot be sent to threads that outlive the current stack frame, so they are composed.

In metrics.rs, Metrics wraps a HashMap counting requests per endpoint plus a running total. The public API is a cheap-to-clone SharedMetrics handle: cloning it only bumps the Arc refcount, so every thread shares one underlying map. The record method locks, mutates, and releases in a tight scope. Notably lock() returns a Result because a Mutex becomes poisoned if a thread panics while holding it; the code calls unwrap_or_else with PoisonError::into_inner to recover the data rather than propagate the panic, which is the pragmatic choice for a metrics counter where a slightly-inconsistent read is acceptable.

The snapshot method demonstrates minimizing lock hold time: it clones the inner state under the lock and returns immediately, so formatting and I/O happen outside the critical section. Holding a lock across expensive work is the classic contention pitfall this avoids.

In worker.rs, spawn_workers clones the SharedMetrics handle once per thread before thread::spawn, because each closure must own its own handle with a 'static lifetime. The JoinHandles are collected and joined so the program waits for all recorded work.

main.rs wires it together: it constructs one SharedMetrics, hands clones to the workers, joins them, and prints the aggregated snapshot. The trade-off of a Mutex versus per-field atomics is deliberate — a Mutex keeps the multi-field update (map entry plus total) atomic as a unit, which atomics cannot easily do without racing between two separate operations. When contention becomes a bottleneck, sharding the map or switching to atomics per counter is the natural next step.


Related snips

Share this code

Here's the card — post it anywhere.

Thread-Safe Request Metrics with Arc, Mutex, and a Poison-Recovery Guard — share card
Link copied