distkit
Instance-aware counters

StrictInstanceAwareCounter

Immediately consistent per-instance counting with epoch-based correctness.

StrictInstanceAwareCounter is immediately consistent with Redis on every call. set and del bump a per-key epoch, which causes any stale instance to reset its stored count on its next operation - this is what prevents double-counting when the global total is reset out from under an instance.

Construct

use distkit::DistkitRedisKey;
use distkit::icounter::{
    InstanceAwareCounterTrait,
    StrictInstanceAwareCounter, StrictInstanceAwareCounterOptions,
};

let prefix = DistkitRedisKey::try_from("my_app".to_string())?;
let counter = StrictInstanceAwareCounter::new(
    StrictInstanceAwareCounterOptions::new(prefix, conn),
);

StrictInstanceAwareCounterOptions::new defaults dead_instance_threshold_ms to 30_000 (30 s). Set the field directly to change it.

Operations

Every call returns (cumulative, instance_count):

let key = DistkitRedisKey::try_from("connections".to_string())?;

// Add to / subtract from this instance's contribution.
let (total, mine) = counter.inc(&key, 5).await?;
let (total, mine) = counter.dec(&key, 2).await?;

// Read without modifying.
let (total, mine) = counter.get(&key).await?;

// Set just this instance's slice (no epoch bump).
let (total, mine) = counter.set_on_instance(&key, 10).await?;

// Set the global total and bump the epoch.
let (total, mine) = counter.set(&key, 100).await?;

// Remove only this instance's contribution.
let (total, removed) = counter.del_on_instance(&key).await?;

// Delete the key globally and bump the epoch.
let (old_total, _) = counter.del(&key).await?;

Dead-instance cleanup

Each instance sends a heartbeat on every operation. If a process silently dies, surviving instances remove its contribution the next time any of them touches the same key:

let opts = |conn| StrictInstanceAwareCounterOptions {
    prefix: prefix.clone(),
    connection_manager: conn,
    dead_instance_threshold_ms: 30_000, // 30 s
};
let server_a = StrictInstanceAwareCounter::new(opts(conn1));
let server_b = StrictInstanceAwareCounter::new(opts(conn2));

server_a.inc(&key, 10).await?; // cumulative = 10
server_b.inc(&key,  5).await?; // cumulative = 15

// server_a goes offline. After 30 s, server_b's next call removes
// server_a's slice automatically.
let (total, _) = server_b.get(&key).await?; // total = 5 once cleaned up

Cleanup is lazy: it happens on the next operation, not on a timer. A key nobody touches keeps its stale contributions until someone reads or writes it.