Initial Commit
This commit is contained in:
133
openspec/archive/2026-04-19-enterprise-retry/proposal.md
Normal file
133
openspec/archive/2026-04-19-enterprise-retry/proposal.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# OpenSpec Change: enterprise-retry
|
||||
|
||||
- **Change ID**: enterprise-retry
|
||||
- **Package**: `@suaveplan/retry`
|
||||
- **Category**: universal
|
||||
- **Target path**: `packages/universal/retry`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-18
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/retry.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Greenfield implementation of `@suaveplan/retry` for the genesis branch. The existing main-branch retry package (100% test coverage) provides the baseline public-API surface to preserve; this change delivers enterprise-grade retry-with-backoff strategies (constant, linear, exponential + jitter), circuit breaker with half-open probing, sleep/timeout helpers, injectable clock and RNG, `AbortSignal` support throughout, structured errors, Zod schemas, property-based tests, OTel instrumentation, and benchmarks.
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@suaveplan/events` | ✅ |
|
||||
| `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly |
|
||||
| `@suaveplan/feature-flags` | ❌ not yet built — skip |
|
||||
| `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly |
|
||||
|
||||
External deps (via catalog):
|
||||
|
||||
- `zod` → `catalog:forms`
|
||||
- `@opentelemetry/api` → `catalog:observability`
|
||||
- `tslib` → `catalog:build`
|
||||
|
||||
Dev deps (via catalog):
|
||||
|
||||
- `vitest`, `@vitest/coverage-istanbul` → `catalog:testing`
|
||||
- `fast-check` → `catalog:testing`
|
||||
- `mitata` → `catalog:testing`
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
### Core retry
|
||||
|
||||
- `retry(fn, config)` → `Promise<RetryResult<T>>` — core retry primitive
|
||||
- `withRetry(fn, config)` → `Promise<T>` — throws on exhaustion
|
||||
- `withCircuitBreaker(fn, breaker)` → `Promise<T>` — wraps fn with circuit breaker
|
||||
- `withTimeout(fn, ms, signal?)` → `Promise<T>` — wraps fn with deadline
|
||||
- `sleep(ms, signal?)` → `Promise<void>` — cancellable sleep
|
||||
- `createDelayFn(strategy)` → `DelayFunction` — adapts a strategy to a delay function
|
||||
|
||||
### Backoff strategies
|
||||
|
||||
- `createConstantBackoff(opts)` → `RetryStrategy` — fixed delay
|
||||
- `createLinearBackoff(opts)` → `RetryStrategy` — linearly increasing delay
|
||||
- `createExponentialBackoff(opts)` → `RetryStrategy` — exponentially increasing delay
|
||||
- `applyJitter(delay, mode, rng?)` → `number` — full / equal / decorrelated jitter
|
||||
- `clampDelay(delay, min, max)` → `number`
|
||||
|
||||
### Circuit breaker
|
||||
|
||||
- `CircuitBreaker` class — `.call(fn)`, `.reset()`, `.state` getter, event emitters
|
||||
- `CircuitOpenError` extends `SuaveplanError`
|
||||
- `CircuitBreakerConfig` — `{ failureThreshold, successThreshold, timeout, halfOpenProbes }`
|
||||
- `CircuitBreakerEventMap` — `{ stateChange: CircuitState }`
|
||||
- `CircuitState` — `'closed' | 'open' | 'half-open'`
|
||||
|
||||
### Types
|
||||
|
||||
- `RetryConfig` — `{ maxAttempts, delay, shouldRetry?, signal?, clock?, rng? }`
|
||||
- `RetryResult<T>` — `{ value: T; attempts: number; totalMs: number }`
|
||||
- `RetryStrategy` — `(attempt: number) => number` (delay in ms)
|
||||
- `DelayFunction` — alias for `RetryStrategy`
|
||||
- `JitterMode` — `'full' | 'equal' | 'decorrelated' | 'none'`
|
||||
- `ConstantBackoffOptions`, `LinearBackoffOptions`, `ExponentialBackoffOptions`
|
||||
|
||||
### Error types
|
||||
|
||||
- `RetryExhaustedError` extends `SuaveplanError` — `.attempts`, `.lastError`
|
||||
- `AttemptTimeoutError` extends `SuaveplanError` — `.attemptNumber`, `.timeoutMs`
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### Structured errors
|
||||
|
||||
- `RetryError` base extends `SuaveplanError` with codes under `RETRY.*` namespace
|
||||
- Codes: `RETRY.EXHAUSTED`, `RETRY.ABORTED`, `RETRY.TIMEOUT`, `RETRY.CIRCUIT_OPEN`
|
||||
- `RetryExhaustedError`, `AttemptTimeoutError`, `CircuitOpenError` extend `RetryError`
|
||||
|
||||
### Zod schemas
|
||||
|
||||
- `RetryConfigSchema`, `ConstantBackoffOptionsSchema`, `LinearBackoffOptionsSchema`, `ExponentialBackoffOptionsSchema`, `CircuitBreakerConfigSchema`
|
||||
|
||||
### Injectable clock + RNG
|
||||
|
||||
- `RetryConfig.clock?: Clock` — for deterministic sleep (FakeClock in tests)
|
||||
- `RetryConfig.rng?: Rng` — for deterministic jitter computation
|
||||
- `CircuitBreakerConfig.clock?: Clock`
|
||||
|
||||
### AbortSignal throughout
|
||||
|
||||
- `sleep(ms, signal?)` — resolves early on abort
|
||||
- `retry(fn, { signal })` — cancels between attempts on abort
|
||||
- `CircuitBreaker.call(fn, signal?)` — passes signal through to fn
|
||||
|
||||
### OTel instrumentation
|
||||
|
||||
- Counter: `retry.attempt.total` (by outcome label: success / failure / exhausted / aborted)
|
||||
- Histogram: `retry.attempt.delay_ms` (per strategy label)
|
||||
- Counter: `circuit_breaker.state_change.total` (by from/to label)
|
||||
- Histogram: `circuit_breaker.call.duration_ms`
|
||||
|
||||
### Property-based tests (fast-check)
|
||||
|
||||
- Exponential backoff: `delay(n+1) >= delay(n)` for all n (monotone with no jitter)
|
||||
- Jitter bounds: `applyJitter(d, 'full', rng)` always in `[0, d]`
|
||||
- Clamp: `clampDelay(d, min, max)` always in `[min, max]`
|
||||
- Retry count: `withRetry` never exceeds `maxAttempts` calls to the wrapped function
|
||||
- Circuit breaker: after `failureThreshold` consecutive failures, state is `'open'`
|
||||
|
||||
### Benchmarks (mitata)
|
||||
|
||||
- `sleep(0)` call overhead (baseline async cost)
|
||||
- `retry` with always-succeeding fn — 0 retry overhead
|
||||
- `createExponentialBackoff` delay computation — 1 000 iterations
|
||||
- `CircuitBreaker.call` closed-state hot path — 10 000 iterations
|
||||
|
||||
## Impact
|
||||
|
||||
- Adds one new package: `packages/universal/retry`
|
||||
- Adds one new spec capability: `retry`
|
||||
- No existing package consumers on genesis yet
|
||||
Reference in New Issue
Block a user