first commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# `mutex-contract`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines `PluginMutex` — the contract guaranteeing at most one lifecycle
|
||||
transition per plugin in flight at any time — together with a reference
|
||||
in-memory implementation (`InMemoryPluginMutex`) that the FSM factory uses
|
||||
by default and that the conformance harness exercises end-to-end. Concurrent
|
||||
`acquire(plugin)` calls queue FIFO and resolve in arrival order, matching
|
||||
the semantics required by design §4's "per-plugin mutex" paragraph.
|
||||
|
||||
## Features
|
||||
|
||||
- Two-method interface: `acquire(plugin) → Promise<MutexRelease>` and
|
||||
`tryAcquire(plugin) → MutexRelease | null`.
|
||||
- Per-plugin scope: locks for distinct plugin names are independent, so
|
||||
`billing` and `inventory` can transition concurrently.
|
||||
- Idempotent `release()` — re-calling is safe.
|
||||
- FIFO queueing semantics so operators see deterministic transition order
|
||||
rather than scheduler-dependent races.
|
||||
- `assertAcquired(plugin, handle)` helper for callers that prefer the
|
||||
`LIFECYCLE.MUTEX_HELD` exception over the null-check pattern.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { InMemoryPluginMutex } from "@suaveplan/api-lifecycle/mutex-contract";
|
||||
|
||||
const mutex = new InMemoryPluginMutex();
|
||||
const handle = await mutex.acquire("billing");
|
||||
try {
|
||||
// … perform the transition …
|
||||
} finally {
|
||||
handle.release();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Pluggable mutex backed by a distributed lock (sketch — the production driver
|
||||
in `api-host` will supply a real implementation):
|
||||
|
||||
```ts
|
||||
import type { MutexRelease, PluginMutex } from "@suaveplan/api-lifecycle/mutex-contract";
|
||||
|
||||
class RedisPluginMutex implements PluginMutex {
|
||||
public async acquire(plugin: string): Promise<MutexRelease> {
|
||||
const lock = await this.client.lock(`plugin:${plugin}`, { ttlMs: 60_000 });
|
||||
return { release: () => lock.unlock() };
|
||||
}
|
||||
public tryAcquire(plugin: string): MutexRelease | null {
|
||||
const lock = this.client.tryLock(`plugin:${plugin}`, { ttlMs: 60_000 });
|
||||
return lock === null ? null : { release: () => lock.unlock() };
|
||||
}
|
||||
private readonly client: { lock: (k: string, o: { ttlMs: number }) => Promise<{ unlock(): void }>; tryLock: (k: string, o: { ttlMs: number }) => { unlock(): void } | null };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface PluginMutex` — `acquire`, `tryAcquire`.
|
||||
- `interface MutexRelease` — `release`.
|
||||
- `class InMemoryPluginMutex` — reference impl; concrete drop-in for the
|
||||
default factory and for conformance tests.
|
||||
- `assertAcquired(plugin, handle)` — throws `LIFECYCLE.MUTEX_HELD` on `null`,
|
||||
returns the handle otherwise.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The reference implementation tracks holders via a `Map<string, "held">` and
|
||||
maintains an array of waiter resolvers per plugin name. On release, if a
|
||||
waiter is queued, the new handle is created against the same plugin name so
|
||||
the next acquirer sees a fresh `released = false` flag — without that, a
|
||||
stale handle's `release()` could re-fire and drain another waiter. The
|
||||
`released` flag is closed-over rather than stored on the handle so the
|
||||
public shape of `MutexRelease` stays minimal.
|
||||
Reference in New Issue
Block a user