Files
skills/docs/foundation/flow/idempotency/src/distributed/distributed-guard.md
2026-05-29 08:20:56 +01:00

3.7 KiB

distributed-guard

Purpose

Provides distributed idempotency protection via a pluggable LockProvider that serializes concurrent access across multiple processes or nodes before consulting the idempotency store. The DistributedIdempotencyGuard guarantees exactly-once execution even when multiple service instances race to handle the same idempotency key simultaneously, preventing double-charge or double-write scenarios.

Features

  • DistributedIdempotencyGuard — wraps any store + lock provider for multi-node safety
  • MemoryLockProvider — in-process lock map for unit tests and single-node deployments
  • Exponential back-off with jitter on lock contention
  • Automatic lock release in a finally block — never leaks locks on exception
  • Configurable retry count, base delay, and lock TTL
  • Fingerprint mismatch detection under the lock (same key, different payload)
  • AbortSignal support propagated through lock acquisition retries
  • Injectable clock for deterministic testing

Usage: Basic Example

import { DistributedIdempotencyGuard, MemoryLockProvider } from "@suaveplan/idempotency/distributed";
import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store";

const guard = new DistributedIdempotencyGuard({
  store: new MemoryIdempotencyStore(),
  lockProvider: new MemoryLockProvider(),
  ttlMs: 3_600_000,
  lockTtlMs: 5_000,
});

const result = await guard.execute("payment-42", () => chargeCard());

Usage: Advanced Example

import { DistributedIdempotencyGuard } from "@suaveplan/idempotency/distributed";
import { RedisIdempotencyStore } from "@suaveplan/idempotency/store";
import { RedisLockProvider } from "@suaveplan/idempotency/distributed";
import { LockAcquisitionError } from "@suaveplan/idempotency";

const guard = new DistributedIdempotencyGuard({
  store: new RedisIdempotencyStore({ client: redisAdapter }),
  lockProvider: new RedisLockProvider({ client: redisAdapter, ownerId: "worker-1" }),
  retryCount: 5,
  retryBaseDelayMs: 50,
  lockTtlMs: 10_000,
});

try {
  await guard.execute("order-99", () => fulfillOrder(order), { input: order });
} catch (err) {
  if (err instanceof LockAcquisitionError) {
    console.error(`Lock failed after ${err.attempts} attempts`);
  }
}

API Reference

new DistributedIdempotencyGuard(config: DistributedGuardConfig)

Option Type Default Description
store IdempotencyStoreAdapter required Record persistence
lockProvider LockProvider required Distributed lock backend
ttlMs number 86_400_000 Record TTL (24 h)
lockTtlMs number 10_000 Lock TTL (10 s)
retryCount number 3 Max lock acquisition retries
retryBaseDelayMs number 100 Base back-off delay (ms)
fingerprintFn fn fingerprintPayload Payload hasher
clock () => number Date.now Monotonic clock

execute<T>(key, fn, opts?): Promise<T>

Acquires the distributed lock, checks the store, and executes fn if no cached result exists.

Throws: LockAcquisitionError | IdempotencyMismatchError

MemoryLockProvider

In-memory lock map with TTL-based auto-expiration. Suitable for testing and single-process deployments.

Methods: acquire(resource, ttlMs), release(resource), isLocked(resource), clear()

Implementation Notes

  • The lock key is prefixed idempotency:lock:<key> to avoid collisions with store keys.
  • Exponential back-off delay formula: baseDelay * 2^attempt + random(0, baseDelay).
  • The lock is always released in finally — even when fn throws or the AbortSignal fires.
  • MemoryLockProvider.clock is injectable to enable deterministic TTL expiration in tests.