Recipe 45: Distributed Counter
What You Build
A counter shared across cells. Two tasklets running on different machines acquire the same grafos_sync::FabricMutex<u64> in leased memory, increment the protected value, and release it. Lease expiry is the synchronization point: if a holder disappears, the fabric lease expires and the counter can be acquired again by a healthy holder.
Source
cookbook/recipe-45-distributed-counter/ in the source tree.
The recipe includes the deterministic tasklet core and a real FabricMutex helper. The tasklet takes a { "current": N } JSON document and returns { "previous": N, "new_value": N+1 }. The fabric helper increments a FabricMutex<u64> directly:
Core grafOS API Path
The counter is not a special counter service. It is a value in leased memory
protected by grafos_sync::FabricMutex:
use grafos_std::mem::MemBuilder;use grafos_sync::FabricMutex;
let lease = MemBuilder::new() .min_bytes(4096) .lease_secs(300) .acquire()?;
let counter = FabricMutex::new(lease, 0, 41u64)?;let mut guard = counter.lock(7, 100)?;let previous = *guard;*guard = guard.saturating_add(1);let new_value = *guard;guard.unlock()?;
assert_eq!((previous, new_value), (41, 42));assert_eq!(counter.generation()?, 1);# Ok::<(), grafos_std::FabricError>(())The helper in the recipe crate keeps that path reusable:
pub fn compute(input: &[u8]) -> Result<CounterOutput, &'static str> { let parsed: CounterInput = serde_json::from_slice(input).map_err(|_| "invalid_input")?; let new_value = parsed.current.checked_add(1).ok_or("counter_overflow")?; Ok(CounterOutput { previous: parsed.current, new_value })}
pub fn increment_fabric_counter( counter: &FabricMutex<u64>, holder_id: u128, max_attempts: u32,) -> FabricResult<CounterOutput> { let mut guard = counter.lock(holder_id, max_attempts)?; let previous = *guard; *guard = guard.saturating_add(1); let new_value = *guard; guard.unlock()?; Ok(CounterOutput { previous, new_value })}What’s interesting
- Typed errors.
invalid_inputandcounter_overfloware returned as typed strings rather thanBox<dyn Error>. The runtime trap path lets you map anyErr(_)to a non-zero return code. - The
compute()separation. The host-testable function does no I/O; the wasm32 entry point handles pointer marshaling. Same pattern as Recipe 44. - Real fabric path.
increment_fabric_counterusesgrafos_sync::FabricMutex<u64>instead of a prose-only counter API.
Failure Behavior
- Invalid JSON returns
invalid_input. u64::MAXreturnscounter_overflowfrom the tasklet core.- A busy or expired fabric mutex returns the underlying
grafos_std::FabricError; callers should retry with a new holder id only after they know the previous holder lost or released authority. - Saturating arithmetic in the fabric helper prevents wraparound if a long-lived fabric counter reaches
u64::MAX.
Run And Verify
cargo test -p cookbook-recipe-45-distributed-counterExpected: the tests cover increment from zero, overflow rejection, invalid input rejection, and the real FabricMutex helper path.
Adapt It
Change the initial mutex value, holder id convention, and retry budget to match the workload. Keep holder ids stable enough to debug ownership transitions, and expose the mutex generation counter in logs when the counter is used for coordination.