@gkoos/caracal 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +3 -3
- package/dist/chunk-FD7IZJKY.js +84 -0
- package/dist/chunk-FD7IZJKY.js.map +1 -0
- package/dist/{chunk-5CXDW7W6.js → chunk-JV2OLYOF.js} +5 -82
- package/dist/chunk-JV2OLYOF.js.map +1 -0
- package/dist/{circuit-breaker-DHEz0YLV.d.ts → circuit-breaker-xZ8uenT8.d.ts} +1 -1
- package/dist/fetch.d.ts +6 -3
- package/dist/fetch.js +4 -2
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/redis.d.ts +10 -4
- package/dist/redis.js +4 -1
- package/dist/redis.js.map +1 -1
- package/dist/{retry-BFP_k3Hg.d.ts → retry-DD85oXL9.d.ts} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-Tf9T76C7.d.ts → types-C-Ml-MKp.d.ts} +7 -0
- package/package.json +1 -1
- package/src/adapters/fetch/retry-after.ts +11 -3
- package/src/coordination/redis/client.ts +15 -1
- package/src/core/bulkhead.ts +16 -2
- package/src/core/operation.ts +2 -5
- package/src/core/retry.ts +46 -2
- package/src/core/runtime.ts +26 -5
- package/src/core/timeout.ts +11 -1
- package/src/core/types.ts +8 -0
- package/dist/chunk-5CXDW7W6.js.map +0 -1
package/dist/redis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/coordination/redis/client.ts","../src/coordination/redis/eval-script.ts","../src/coordination/redis/keys.ts","../src/coordination/redis/scripts.ts","../src/coordination/redis/bulkhead.ts","../src/coordination/redis/circuit-breaker.ts"],"names":["createHash"],"mappings":"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","file":"redis.js","sourcesContent":["import { Cluster, Redis } from \"ioredis\"\n\nexport class CoordinatorUnavailableError extends Error {\n readonly coordination = \"distributed\"\n constructor(cause: unknown) {\n super(\"Redis coordination unavailable; command outcome may be unknown\", {\n cause,\n })\n this.name = \"CoordinatorUnavailableError\"\n }\n}\n\nfunction attachErrorSink(emitter: {\n on(event: \"error\", listener: () => void): void\n}): void {\n emitter.on(\"error\", () => {\n /* Commands surface errors; never crash on an unhandled EventEmitter error. */\n })\n}\n\n/** Standalone Redis client. Caller owns it and must disconnect at shutdown. */\nexport function createCoordinationClient(\n url: string,\n commandTimeout = 1000,\n): Redis {\n if (!Number.isSafeInteger(commandTimeout) || commandTimeout < 1)\n throw new RangeError(\"commandTimeout must be a positive integer\")\n const client = new Redis(url, {\n lazyConnect: true,\n enableOfflineQueue: false,\n maxRetriesPerRequest: 0,\n autoResendUnfulfilledCommands: false,\n commandTimeout,\n connectTimeout: commandTimeout,\n retryStrategy: (attempt) => Math.min(attempt * 50, 1000),\n })\n attachErrorSink(client)\n return client\n}\n\nexport interface ClusterNode {\n readonly host: string\n readonly port: number\n}\n\n/**\n * Redis Cluster client. Caller owns it and must disconnect at shutdown.\n *\n * All coordination keys use a hash-tag so that every key for a given\n * policy+operation+scope lands on the same cluster slot. Multi-key Lua\n * scripts (circuit breaker) are therefore cluster-safe without cross-slot\n * concerns.\n *\n * Call `connect()` before use and `disconnect()` at shutdown, matching the\n * standalone client contract.\n */\nexport function createCoordinationClusterClient(\n nodes: ReadonlyArray<ClusterNode>,\n commandTimeout = 1000,\n): Cluster {\n if (!Number.isSafeInteger(commandTimeout) || commandTimeout < 1)\n throw new RangeError(\"commandTimeout must be a positive integer\")\n if (!Array.isArray(nodes) || nodes.length === 0)\n throw new RangeError(\"nodes must be a non-empty array of { host, port }\")\n const cluster = new Cluster([...nodes], {\n lazyConnect: true,\n enableOfflineQueue: false,\n clusterRetryStrategy: (attempt) => Math.min(attempt * 50, 1000),\n redisOptions: {\n commandTimeout,\n connectTimeout: commandTimeout,\n maxRetriesPerRequest: 0,\n autoResendUnfulfilledCommands: false,\n },\n })\n attachErrorSink(cluster)\n return cluster\n}\n","import { createHash } from \"node:crypto\"\n\n/**\n * Minimal Lua-script capability required by the Redis coordinators.\n *\n * `eval` is always required. `evalsha` is optional so structural test doubles\n * and clients that only expose `eval` keep working; when it is missing the\n * script body is sent with `eval`.\n */\nexport interface ScriptClient {\n eval(\n script: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n ): Promise<unknown>\n evalsha?(\n sha: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n ): Promise<unknown>\n}\n\nconst scriptHashes = new Map<string, string>()\n\n/** SHA1 of a script body. Redis keys its script cache by exactly this value. */\nexport function scriptSha(script: string): string {\n const cached = scriptHashes.get(script)\n if (cached !== undefined) return cached\n const sha = createHash(\"sha1\").update(script).digest(\"hex\")\n scriptHashes.set(script, sha)\n return sha\n}\n\nfunction errorMessage(error: unknown): string {\n if (typeof error === \"string\") return error\n if (error instanceof Error) return error.message\n return \"\"\n}\n\n/**\n * Executes a Lua script, sending the body only when it has to.\n *\n * The script cache is keyed by SHA1, so the steady state is `EVALSHA` - a\n * 40-byte hash instead of kilobytes of Lua on every call. The full body goes\n * out with `EVAL` only when the client cannot send `EVALSHA`, or when the\n * server answers `NOSCRIPT`: the script cache lost the script to a restart, a\n * `SCRIPT FLUSH`, or (Redis 7.4 and later) LRU eviction under memory pressure.\n *\n * Retrying through `EVAL` is safe because Redis rejects an unknown SHA1 before\n * executing anything, so the failed command is known not to have run. This is\n * deliberately narrower than the coordinator's command-timeout rule, where the\n * outcome is unknown and a replay is never allowed. A timeout error is not a\n * `NOSCRIPT` error and propagates unchanged.\n */\nexport async function evalScript(\n client: ScriptClient,\n script: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n): Promise<unknown> {\n const evalsha = client.evalsha\n if (typeof evalsha !== \"function\") {\n return client.eval(script, numberOfKeys, ...args)\n }\n try {\n return await evalsha.call(client, scriptSha(script), numberOfKeys, ...args)\n } catch (error) {\n if (!errorMessage(error).includes(\"NOSCRIPT\")) throw error\n return client.eval(script, numberOfKeys, ...args)\n }\n}\n","import { createHash } from \"node:crypto\"\n\n/**\n * Build a namespaced Redis key for a policy-scoped coordination slot.\n *\n * The SHA-256 hash of `[namespace, policy, operation, scope]` forms the\n * stable identity; `suffix` distinguishes multiple keys that share the\n * same identity (e.g. `:leases`, `:breaker`, `:observations`, `:probes`).\n * Default suffix is `\"leases\"` for backward compatibility with the bulkhead.\n */\nexport function coordinationKey(\n namespace: string,\n policy: string,\n operation: string,\n scope: string,\n suffix = \"leases\",\n): string {\n for (const value of [namespace, policy, operation, scope, suffix]) {\n if (\n typeof value !== \"string\" ||\n !value.trim() ||\n Buffer.byteLength(value) > 1024\n )\n throw new TypeError(\n \"Coordination identities must be nonempty strings of at most 1024 UTF-8 bytes\",\n )\n }\n const identity = createHash(\"sha256\")\n .update(JSON.stringify([namespace, policy, operation, scope]))\n .digest(\"hex\")\n return `caracal:v1:{${identity}}:${suffix}`\n}\n","// ---------------------------------------------------------------------------\n// Versioned atomic operations, internal to the Redis policy capabilities.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Bulkhead scripts\n// ---------------------------------------------------------------------------\n\nexport const leaseV1 = `\nlocal action = ARGV[1]\nlocal token = ARGV[2]\nlocal ttl = tonumber(ARGV[3])\nlocal limit = tonumber(ARGV[4])\nlocal time = redis.call('TIME')\nlocal now = time[1] * 1000 + math.floor(time[2] / 1000)\nredis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', now)\nlocal existing = redis.call('ZSCORE', KEYS[1], token)\nif action == 'release' then\n return redis.call('ZREM', KEYS[1], token)\nend\nif action == 'renew' and not existing then return 0 end\nif action == 'acquire' and existing then return 1 end\nif action == 'acquire' and redis.call('ZCARD', KEYS[1]) >= limit then return 0 end\nredis.call('ZADD', KEYS[1], now + ttl, token)\nlocal latest = redis.call('ZREVRANGE', KEYS[1], 0, 0, 'WITHSCORES')\nredis.call('PEXPIREAT', KEYS[1], math.ceil(tonumber(latest[2])))\nreturn 1\n`\n\n/** Returns the decision and occupancy from the same atomic transition. */\nexport const bulkheadLeaseV1 = `local function transition()\\n${leaseV1}\\nend\\nlocal allowed = transition()\\nreturn {allowed, redis.call('ZCARD', KEYS[1])}`\n\n// ---------------------------------------------------------------------------\n// Circuit-breaker scripts\n//\n// Return arrays use fixed positions so the TypeScript caller can validate\n// and decode without field names (which Lua/Redis cannot return).\n//\n// stateCode encoding: 0=closed 1=open 2=half-open\n// ---------------------------------------------------------------------------\n\n/**\n * Record one attempt outcome into the distributed sliding window.\n * Atomically opens the breaker when the failure ratio meets the threshold.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = observations sorted set (:observations)\n *\n * ARGV[1] = outcome \"success\" | \"failure\"\n * ARGV[2] = expectedGeneration integer\n * ARGV[3] = windowTtlMs observation retention window in ms\n * ARGV[4] = minimumThroughput min observations before opening\n * ARGV[5] = failureThresholdNum failure threshold × 1000 (e.g. 500 = 0.5).\n * The policy rejects thresholds that would round\n * to 0 or to 1000 before they reach this script\n * (see assertResolvableThreshold in core).\n * ARGV[6] = windowSize max observations retained by count\n * ARGV[7] = openMs how long to stay OPEN; closed hash TTL = openMs×2\n * ARGV[8] = uuid unique string for member deduplication\n *\n * Returns: {status, stateCode, generation, windowTotal, windowFailures}\n * status 0 = stale (dropped; generation or state mismatch)\n * status 1 = observed, no transition (remained closed)\n * status 2 = observed, breaker opened (stateCode=1, generation incremented)\n *\n * TTL policy:\n * OPEN state → PERSIST (no expiry). A missing key is treated as closed by\n * all scripts, so expiring an open breaker would silently admit\n * unrestricted traffic and reset the generation counter.\n * CLOSED state → max(openMs×2, windowTtlMs) TTL for eventual cleanup of idle\n * scopes. Expiring a closed key is safe *only* because the TTL\n * outlives the observation window: the window is scoped by an\n * epoch, and an epoch that outlives its members is what keeps\n * cleanup from resurrecting them.\n *\n * Epochs:\n * `generation` increments on every transition, and a brand new value is minted\n * whenever the state hash has to be recreated while observation members from a\n * previous epoch are still present (state lost to eviction or admin cleanup).\n * Window membership is decided by comparing the stored epoch, so members from\n * a superseded epoch can never be counted again, and an attempt holding a\n * pre-loss generation can never pass the staleness check. A scope that has\n * never been observed keeps generation 0.\n */\nexport const breakerObserveV1 = `\nlocal outcome = ARGV[1]\nlocal expectGen = tonumber(ARGV[2])\nlocal windowTtl = tonumber(ARGV[3])\nlocal minTP = tonumber(ARGV[4])\nlocal threshNum = tonumber(ARGV[5])\nlocal windowSize = tonumber(ARGV[6])\nlocal openMs = tonumber(ARGV[7])\nlocal uuid = ARGV[8]\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nif state ~= 'closed' then\n local sc = (state == 'open' and 1) or (state == 'half-open' and 2) or 0\n return {0, sc, gen, 0, 0}\nend\nif redis.call('EXISTS', KEYS[1]) == 0 then\n -- No live state hash: a scope we have never seen, or a hash that was lost\n -- while its window survived.\n if expectGen ~= 0 then\n -- The caller holds a generation this key cannot confirm.\n return {0, 0, 0, 0, 0}\n end\n if redis.call('EXISTS', KEYS[2]) == 1 then\n -- Members from a superseded epoch are still here. Mint an epoch that has\n -- never been used for this key (derived from the observation's own uuid, so\n -- no clock is involved) instead of restarting at a value those members\n -- would match.\n --\n -- Bounded to 11 hex digits (44 bits, <= 14 decimal digits) so the value\n -- prints exactly with stock Lua number formatting: the epoch is read back\n -- from the hash as a decimal string by both this script and the client, and\n -- a larger value could be written in scientific notation and break the\n -- round-trip.\n gen = tonumber(string.sub(redis.sha1hex(uuid), 1, 11), 16)\n if not gen or gen == 0 then gen = 1 end\n else\n gen = 0\n end\n redis.call('HSET', KEYS[1], 'state', 'closed', 'generation', gen,\n 'openedAt', 0, 'probeCount', 0, 'probeSuccesses', 0)\nelseif gen ~= expectGen then\n return {0, 0, gen, 0, 0}\nend\nredis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now - windowTtl)\nlocal member = tostring(gen) .. ':' .. uuid .. ':' .. outcome\nredis.call('ZADD', KEYS[2], 'NX', now, member)\nlocal cnt = redis.call('ZCARD', KEYS[2])\nif cnt > windowSize then\n redis.call('ZREMRANGEBYRANK', KEYS[2], 0, cnt - windowSize - 1)\nend\nredis.call('PEXPIREAT', KEYS[2], now + windowTtl)\nlocal all = redis.call('ZRANGE', KEYS[2], 0, -1)\nlocal prefix = tostring(gen) .. ':'\nlocal wTotal = 0\nlocal wFail = 0\nfor _, m in ipairs(all) do\n if string.sub(m, 1, #prefix) == prefix then\n wTotal = wTotal + 1\n if string.sub(m, -8) == ':failure' then wFail = wFail + 1 end\n end\nend\nif wTotal >= minTP and wFail * 1000 >= threshNum * wTotal then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'open', 'generation', gen,\n 'openedAt', now, 'probeCount', 0, 'probeSuccesses', 0)\n -- OPEN state must never expire: a missing key is treated as closed,\n -- which would silently admit unrestricted traffic and reset the generation.\n redis.call('PERSIST', KEYS[1])\n return {2, 1, gen, wTotal, wFail}\nend\nif redis.call('EXISTS', KEYS[1]) == 1 then\n -- Still CLOSED. The cleanup TTL must not expire the state before the window\n -- it governs, otherwise retained members would survive their epoch.\n redis.call('PEXPIRE', KEYS[1], math.max(openMs * 2, windowTtl))\nend\nreturn {1, 0, gen, wTotal, wFail}\n`\n\n/**\n * Admit a probe attempt. Transitions OPEN→HALF_OPEN when openMs has elapsed,\n * then atomically allocates a probe token slot.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = probe tokens sorted set (:probes)\n *\n * ARGV[1] = probeToken unique UUID for this probe attempt\n * ARGV[2] = openMs determines OPEN→HALF_OPEN transition and TTL\n * ARGV[3] = halfOpenProbes maximum concurrent probe tokens\n * ARGV[4] = probeLeaseTtlMs probe token TTL in ms\n *\n * Returns: {status, stateCode, generation, probeCount, transitioned}\n * status 0 = rejected\n * stateCode 0 = breaker already CLOSED (no probe needed)\n * stateCode 1 = still OPEN (openMs not yet elapsed)\n * stateCode 2 = HALF_OPEN but probe limit reached\n * status 1 = admitted (probe token stored)\n * transitioned 1 = OPEN→HALF_OPEN transition happened in this call\n * transitioned 0 = was already HALF_OPEN\n */\nexport const breakerAdmitProbeV1 = `\nlocal probeToken = ARGV[1]\nlocal openMs = tonumber(ARGV[2])\nlocal maxProbes = tonumber(ARGV[3])\nlocal probeLeaseTtl = tonumber(ARGV[4])\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation', 'openedAt',\n 'probeCount', 'probeSuccesses')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nlocal openedAt = tonumber(f[3]) or 0\nlocal transitioned = 0\nif state == 'closed' then\n return {0, 0, gen, 0, 0}\nend\nif state == 'open' then\n if now - openedAt < openMs then\n return {0, 1, gen, 0, 0}\n end\n state = 'half-open'\n transitioned = 1\n redis.call('HSET', KEYS[1], 'state', 'half-open', 'probeSuccesses', 0, 'probeCount', 0)\n -- OPEN/HALF_OPEN must never expire; PERSIST removes any prior cleanup TTL.\n redis.call('PERSIST', KEYS[1])\n -- Tokens from the superseded recovery window must not consume slots in this\n -- one; their settle is dropped by the generation check anyway.\n redis.call('DEL', KEYS[2])\nend\nredis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)\nlocal activeProbes = redis.call('ZCARD', KEYS[2])\nif activeProbes >= maxProbes then\n return {0, 2, gen, activeProbes, transitioned}\nend\nredis.call('ZADD', KEYS[2], 'NX', now + probeLeaseTtl, probeToken)\nactiveProbes = activeProbes + 1\nredis.call('HSET', KEYS[1], 'probeCount', activeProbes)\n-- State hash must not expire while recovery probes are in flight.\nredis.call('PERSIST', KEYS[1])\n-- Probes sorted set expires naturally once the last lease elapses.\nredis.call('PEXPIREAT', KEYS[2], now + probeLeaseTtl + 1000)\nreturn {1, 2, gen, activeProbes, transitioned}\n`\n\n/**\n * Record a probe attempt outcome. Removes the probe token and applies the\n * result: failure re-opens; sufficient successes close the breaker.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = probe tokens sorted set (:probes)\n *\n * ARGV[1] = probeToken the token issued by admitProbe\n * ARGV[2] = outcome \"success\" | \"failure\" | \"ignored\", where\n * \"ignored\" releases the slot without recording\n * an outcome or advancing recovery\n * ARGV[3] = expectedGeneration generation captured at admission time\n * ARGV[4] = halfOpenSuccesses consecutive probe successes needed to close\n * ARGV[5] = openMs how long the breaker stays OPEN (TTL floor)\n * ARGV[6] = windowTtlMs observation retention (TTL floor): the CLOSED\n * hash must outlive the window it governs\n *\n * Returns: {status, stateCode, generation}\n * status 0 = stale (token missing, lease elapsed, or generation mismatch;\n * the result is dropped and any consumed token is released)\n * status 1 = settled, no state transition (still HALF_OPEN; an \"ignored\"\n * outcome only releases the probe slot)\n * status 2 = settled, state transition occurred\n * stateCode 0 = transitioned to CLOSED\n * stateCode 1 = transitioned back to OPEN (probe failure)\n *\n * A token carries its own deadline as its sorted-set score. When the lease has\n * elapsed the slot is already recoverable - admitProbe prunes expired tokens\n * and re-issues the slot - so the result must not count even while the member is\n * still present. The deadline is therefore checked under the same atomic call.\n */\nexport const breakerSettleProbeV1 = `\nlocal probeToken = ARGV[1]\nlocal outcome = ARGV[2]\nlocal expectGen = tonumber(ARGV[3])\nlocal halfOpenSucc = tonumber(ARGV[4])\nlocal openMs = tonumber(ARGV[5])\nlocal windowTtl = tonumber(ARGV[6])\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation', 'probeSuccesses', 'probeCount')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nlocal probeSucc = tonumber(f[3]) or 0\nlocal probeCnt = tonumber(f[4]) or 0\nlocal sc = (state == 'open' and 1) or (state == 'half-open' and 2) or 0\nlocal deadline = redis.call('ZSCORE', KEYS[2], probeToken)\nif not deadline then\n -- Never admitted, already settled, or dropped with its recovery window.\n return {0, sc, gen}\nend\nredis.call('ZREM', KEYS[2], probeToken)\nif tonumber(deadline) <= now then\n -- The lease elapsed while the probe was still running: its slot was\n -- recoverable, so the result cannot be counted. Release the slot it still\n -- occupied so the probe count keeps matching the token set.\n probeCnt = math.max(0, probeCnt - 1)\n redis.call('HSET', KEYS[1], 'probeCount', probeCnt)\n return {0, sc, gen}\nend\nif state ~= 'half-open' or gen ~= expectGen then\n return {0, sc, gen}\nend\nprobeCnt = math.max(0, probeCnt - 1)\nif outcome == 'ignored' then\n -- The result was not recorded (the classifier ignored it), but the slot the\n -- probe held must still be released: otherwise the recovery window stalls\n -- until the probe lease elapses instead of letting the next probe through.\n -- The local breaker releases its slot immediately, and this matches it: no\n -- success counter advances and the state does not transition.\n redis.call('HSET', KEYS[1], 'probeCount', probeCnt)\n -- Still HALF_OPEN: must not expire while recovery is in progress.\n redis.call('PERSIST', KEYS[1])\n return {1, 2, gen}\nend\nif outcome == 'failure' then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'open', 'generation', gen,\n 'openedAt', now, 'probeCount', 0, 'probeSuccesses', 0)\n -- Re-opened: OPEN must not expire (see breakerObserveV1 TTL policy).\n redis.call('PERSIST', KEYS[1])\n -- In-flight probes belong to the dead generation; free their slots so the\n -- next recovery window starts with full capacity.\n redis.call('DEL', KEYS[2])\n return {2, 1, gen}\nend\nprobeSucc = probeSucc + 1\nif probeSucc >= halfOpenSucc then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'closed', 'generation', gen,\n 'probeCount', 0, 'probeSuccesses', 0)\n -- Newly CLOSED: expire for cleanup, but never before the window it governs.\n redis.call('PEXPIRE', KEYS[1], math.max(openMs * 2, windowTtl))\n redis.call('DEL', KEYS[2])\n return {2, 0, gen}\nend\nredis.call('HSET', KEYS[1], 'probeSuccesses', probeSucc, 'probeCount', probeCnt)\n-- Still HALF_OPEN: must not expire while recovery is in progress.\nredis.call('PERSIST', KEYS[1])\nreturn {1, 2, gen}\n`\n","import type { BulkheadCoordinator } from \"../../core/bulkhead.js\"\nimport { CoordinatorUnavailableError } from \"./client.js\"\nimport { evalScript } from \"./eval-script.js\"\nimport { coordinationKey } from \"./keys.js\"\nimport type { RedisScriptClient } from \"./leases.js\"\nimport { bulkheadLeaseV1 } from \"./scripts.js\"\nexport function redisCoordinator(\n client: RedisScriptClient,\n options: { namespace: string },\n): BulkheadCoordinator {\n const namespace = options.namespace\n coordinationKey(namespace, \"bulkhead\", \"validate\", \"validate\")\n const coordinator: BulkheadCoordinator = {\n async command(identity, action, token, leaseMs, limit) {\n if (\n ![\"acquire\", \"renew\", \"release\"].includes(action) ||\n typeof token !== \"string\" ||\n !token ||\n token.length > 256\n )\n throw new TypeError(\"Invalid bulkhead lease action or token\")\n if (\n ![leaseMs, limit].every(\n (value) => Number.isSafeInteger(value) && value > 0,\n ) ||\n leaseMs > 86400000\n )\n throw new RangeError(\"Invalid bulkhead lease duration or limit\")\n const key = coordinationKey(\n namespace,\n `bulkhead:${identity.name}`,\n identity.operation,\n identity.scope,\n )\n try {\n const result = await evalScript(\n client,\n bulkheadLeaseV1,\n 1,\n key,\n action,\n token,\n leaseMs,\n limit,\n )\n if (\n !Array.isArray(result) ||\n result.length !== 2 ||\n ![0, 1].includes(result[0]) ||\n !Number.isSafeInteger(result[1]) ||\n result[1] < 0\n )\n throw new Error(\"Invalid bulkhead reply\")\n return { allowed: result[0] === 1, occupancy: result[1] as number }\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n }\n return Object.freeze(coordinator)\n}\n","import type {\n AdmitProbeResult,\n BreakerCoordinator,\n BreakerIdentity,\n BreakerState,\n ObserveResult,\n SettleProbeResult,\n} from \"../../core/circuit-breaker.js\"\nimport { CoordinatorUnavailableError } from \"./client.js\"\nimport { evalScript } from \"./eval-script.js\"\nimport { coordinationKey } from \"./keys.js\"\nimport type { RedisScriptClient } from \"./leases.js\"\nimport {\n breakerAdmitProbeV1,\n breakerObserveV1,\n breakerSettleProbeV1,\n} from \"./scripts.js\"\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\nfunction breakerKeys(\n namespace: string,\n identity: BreakerIdentity,\n): [hashKey: string, obsKey: string, probeKey: string] {\n const policy = `breaker:${identity.name}`\n const hash = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"breaker\",\n )\n const obs = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"observations\",\n )\n const prob = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"probes\",\n )\n return [hash, obs, prob]\n}\n\n// ---------------------------------------------------------------------------\n// Reply validation helpers\n// ---------------------------------------------------------------------------\n\nfunction assertArray(reply: unknown, minLen: number, label: string): unknown[] {\n if (\n !Array.isArray(reply) ||\n reply.length < minLen ||\n reply.some((v) => typeof v !== \"number\" || !Number.isSafeInteger(v))\n ) {\n throw new Error(\n `Invalid ${label} reply from Redis: ${JSON.stringify(reply)}`,\n )\n }\n return reply as unknown[]\n}\n\nconst STATE_CODES: BreakerState[] = [\"closed\", \"open\", \"half-open\"]\n\nfunction decodeState(code: number): BreakerState {\n const s = STATE_CODES[code]\n if (!s) throw new Error(`Unknown breaker state code: ${code}`)\n return s\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Policy-specific Redis coordinator for `circuitBreaker.distributed()`.\n *\n * The caller owns `client` and must connect/disconnect it independently.\n * `namespace` is prepended to every key; use a per-service, per-environment\n * value to avoid cross-deployment state collisions.\n */\nexport function redisCircuitBreakerCoordinator(\n client: RedisScriptClient,\n options: { readonly namespace: string },\n): BreakerCoordinator {\n const { namespace } = options\n // Validate namespace eagerly (coordinationKey throws on bad input).\n coordinationKey(\n namespace,\n \"breaker:validate\",\n \"validate\",\n \"validate\",\n \"breaker\",\n )\n\n const coordinator: BreakerCoordinator = {\n async readState(identity) {\n const [hashKey] = breakerKeys(namespace, identity)\n try {\n const fields = await client.hmget(hashKey, \"state\", \"generation\")\n const rawState = fields[0]\n if (!rawState) return null\n const state = rawState as BreakerState\n if (!STATE_CODES.includes(state))\n throw new Error(`Unknown breaker state: ${rawState}`)\n const generation = parseInt(fields[1] ?? \"0\", 10)\n return { state, generation }\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async observe(identity, params) {\n const [hashKey, obsKey] = breakerKeys(namespace, identity)\n const {\n generation,\n outcome,\n uuid,\n windowTtlMs,\n minimumThroughput,\n failureThresholdNumerator,\n windowSize,\n openMs,\n } = params\n try {\n const reply = await evalScript(\n client,\n breakerObserveV1,\n 2,\n hashKey,\n obsKey,\n outcome,\n generation,\n windowTtlMs,\n minimumThroughput,\n failureThresholdNumerator,\n windowSize,\n openMs,\n uuid,\n )\n const r = assertArray(reply, 5, \"breakerObserveV1\") as number[]\n const [status, , newGen, windowTotal, windowFailures] = r\n if (status === 0) {\n return {\n type: \"stale\",\n currentGeneration: newGen,\n } satisfies ObserveResult\n }\n if (status === 2) {\n return {\n type: \"opened\",\n newGeneration: newGen,\n windowTotal,\n windowFailures,\n } satisfies ObserveResult\n }\n return {\n type: \"observed\",\n generation: newGen,\n windowTotal,\n windowFailures,\n } satisfies ObserveResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async admitProbe(identity, params) {\n const [hashKey, , probeKey] = breakerKeys(namespace, identity)\n const { probeToken, openMs, halfOpenProbes, probeLeaseTtlMs } = params\n try {\n const reply = await evalScript(\n client,\n breakerAdmitProbeV1,\n 2,\n hashKey,\n probeKey,\n probeToken,\n openMs,\n halfOpenProbes,\n probeLeaseTtlMs,\n )\n const r = assertArray(reply, 5, \"breakerAdmitProbeV1\") as number[]\n const [status, stateCode, gen, probeCount, transitioned] = r\n if (status === 0) {\n const reason =\n stateCode === 0\n ? \"closed\"\n : stateCode === 1\n ? \"open\"\n : \"probe-limit\"\n return {\n type: \"rejected\",\n reason,\n generation: gen,\n } satisfies AdmitProbeResult\n }\n return {\n type: \"admitted\",\n generation: gen,\n probeCount,\n stateChanged: transitioned === 1,\n } satisfies AdmitProbeResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async settleProbe(identity, params) {\n const [hashKey, , probeKey] = breakerKeys(namespace, identity)\n const {\n probeToken,\n outcome,\n generation,\n halfOpenSuccesses,\n openMs,\n windowTtlMs,\n } = params\n try {\n const reply = await evalScript(\n client,\n breakerSettleProbeV1,\n 2,\n hashKey,\n probeKey,\n probeToken,\n outcome,\n generation,\n halfOpenSuccesses,\n openMs,\n // Floor for the CLOSED cleanup TTL; older callers that do not pass it\n // keep the historical openMs x 2 behaviour.\n windowTtlMs ?? openMs * 2,\n )\n const r = assertArray(reply, 3, \"breakerSettleProbeV1\") as number[]\n const [status, stateCode, newGen] = r\n if (status === 0) {\n return {\n type: \"stale\",\n generation: newGen,\n } satisfies SettleProbeResult\n }\n const state = decodeState(stateCode)\n if (status === 2) {\n return {\n type: \"transitioned\",\n newState: state,\n newGeneration: newGen,\n previousState: \"half-open\",\n } satisfies SettleProbeResult\n }\n return {\n type: \"settled\",\n state,\n generation: newGen,\n } satisfies SettleProbeResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n }\n\n return Object.freeze(coordinator)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/coordination/redis/client.ts","../src/coordination/redis/eval-script.ts","../src/coordination/redis/keys.ts","../src/coordination/redis/scripts.ts","../src/coordination/redis/bulkhead.ts","../src/coordination/redis/circuit-breaker.ts"],"names":["createHash"],"mappings":"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","file":"redis.js","sourcesContent":["import { Cluster, Redis, type RedisOptions } from \"ioredis\"\n\nexport class CoordinatorUnavailableError extends Error {\n readonly coordination = \"distributed\"\n constructor(cause: unknown) {\n super(\"Redis coordination unavailable; command outcome may be unknown\", {\n cause,\n })\n this.name = \"CoordinatorUnavailableError\"\n }\n}\n\nfunction attachErrorSink(emitter: {\n on(event: \"error\", listener: () => void): void\n}): void {\n emitter.on(\"error\", () => {\n /* Commands surface errors; never crash on an unhandled EventEmitter error. */\n })\n}\n\n/** Standalone Redis client. Caller owns it and must disconnect at shutdown. */\nexport function createCoordinationClient(\n url: string,\n commandTimeout = 1000,\n): Redis {\n if (!Number.isSafeInteger(commandTimeout) || commandTimeout < 1)\n throw new RangeError(\"commandTimeout must be a positive integer\")\n const client = new Redis(url, {\n lazyConnect: true,\n enableOfflineQueue: false,\n maxRetriesPerRequest: 0,\n autoResendUnfulfilledCommands: false,\n commandTimeout,\n connectTimeout: commandTimeout,\n retryStrategy: (attempt) => Math.min(attempt * 50, 1000),\n })\n attachErrorSink(client)\n return client\n}\n\nexport interface ClusterNode {\n readonly host: string\n readonly port: number\n}\n\n/**\n * Redis Cluster client. Caller owns it and must disconnect at shutdown.\n *\n * All coordination keys use a hash-tag so that every key for a given\n * policy+operation+scope lands on the same cluster slot. Multi-key Lua\n * scripts (circuit breaker) are therefore cluster-safe without cross-slot\n * concerns.\n *\n * Call `connect()` before use and `disconnect()` at shutdown, matching the\n * standalone client contract.\n */\n/**\n * Redis Cluster client for a secured cluster: pass credentials and TLS via\n * `connectionOptions`. The coordination safeguards (`lazyConnect`, no offline\n * queue, no command replay, no per-request retries) are applied after them and\n * cannot be overridden, because the coordinators depend on those semantics.\n */\nexport function createCoordinationClusterClient(\n nodes: ReadonlyArray<ClusterNode>,\n commandTimeout = 1000,\n connectionOptions: RedisOptions = {},\n): Cluster {\n if (!Number.isSafeInteger(commandTimeout) || commandTimeout < 1)\n throw new RangeError(\"commandTimeout must be a positive integer\")\n if (!Array.isArray(nodes) || nodes.length === 0)\n throw new RangeError(\"nodes must be a non-empty array of { host, port }\")\n if (\n typeof connectionOptions !== \"object\" ||\n connectionOptions === null ||\n Array.isArray(connectionOptions)\n )\n throw new TypeError(\"connectionOptions must be an ioredis options object\")\n const cluster = new Cluster([...nodes], {\n lazyConnect: true,\n enableOfflineQueue: false,\n clusterRetryStrategy: (attempt) => Math.min(attempt * 50, 1000),\n redisOptions: {\n ...connectionOptions,\n commandTimeout,\n connectTimeout: commandTimeout,\n maxRetriesPerRequest: 0,\n autoResendUnfulfilledCommands: false,\n },\n })\n attachErrorSink(cluster)\n return cluster\n}\n","import { createHash } from \"node:crypto\"\n\n/**\n * Minimal Lua-script capability required by the Redis coordinators.\n *\n * `eval` is always required. `evalsha` is optional so structural test doubles\n * and clients that only expose `eval` keep working; when it is missing the\n * script body is sent with `eval`.\n */\nexport interface ScriptClient {\n eval(\n script: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n ): Promise<unknown>\n evalsha?(\n sha: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n ): Promise<unknown>\n}\n\nconst scriptHashes = new Map<string, string>()\n\n/** SHA1 of a script body. Redis keys its script cache by exactly this value. */\nexport function scriptSha(script: string): string {\n const cached = scriptHashes.get(script)\n if (cached !== undefined) return cached\n const sha = createHash(\"sha1\").update(script).digest(\"hex\")\n scriptHashes.set(script, sha)\n return sha\n}\n\nfunction errorMessage(error: unknown): string {\n if (typeof error === \"string\") return error\n if (error instanceof Error) return error.message\n return \"\"\n}\n\n/**\n * Executes a Lua script, sending the body only when it has to.\n *\n * The script cache is keyed by SHA1, so the steady state is `EVALSHA` - a\n * 40-byte hash instead of kilobytes of Lua on every call. The full body goes\n * out with `EVAL` only when the client cannot send `EVALSHA`, or when the\n * server answers `NOSCRIPT`: the script cache lost the script to a restart, a\n * `SCRIPT FLUSH`, or (Redis 7.4 and later) LRU eviction under memory pressure.\n *\n * Retrying through `EVAL` is safe because Redis rejects an unknown SHA1 before\n * executing anything, so the failed command is known not to have run. This is\n * deliberately narrower than the coordinator's command-timeout rule, where the\n * outcome is unknown and a replay is never allowed. A timeout error is not a\n * `NOSCRIPT` error and propagates unchanged.\n */\nexport async function evalScript(\n client: ScriptClient,\n script: string,\n numberOfKeys: number,\n ...args: (string | number)[]\n): Promise<unknown> {\n const evalsha = client.evalsha\n if (typeof evalsha !== \"function\") {\n return client.eval(script, numberOfKeys, ...args)\n }\n try {\n return await evalsha.call(client, scriptSha(script), numberOfKeys, ...args)\n } catch (error) {\n if (!errorMessage(error).includes(\"NOSCRIPT\")) throw error\n return client.eval(script, numberOfKeys, ...args)\n }\n}\n","import { createHash } from \"node:crypto\"\n\n/**\n * Build a namespaced Redis key for a policy-scoped coordination slot.\n *\n * The SHA-256 hash of `[namespace, policy, operation, scope]` forms the\n * stable identity; `suffix` distinguishes multiple keys that share the\n * same identity (e.g. `:leases`, `:breaker`, `:observations`, `:probes`).\n * Default suffix is `\"leases\"` for backward compatibility with the bulkhead.\n */\nexport function coordinationKey(\n namespace: string,\n policy: string,\n operation: string,\n scope: string,\n suffix = \"leases\",\n): string {\n for (const value of [namespace, policy, operation, scope, suffix]) {\n if (\n typeof value !== \"string\" ||\n !value.trim() ||\n Buffer.byteLength(value) > 1024\n )\n throw new TypeError(\n \"Coordination identities must be nonempty strings of at most 1024 UTF-8 bytes\",\n )\n }\n const identity = createHash(\"sha256\")\n .update(JSON.stringify([namespace, policy, operation, scope]))\n .digest(\"hex\")\n return `caracal:v1:{${identity}}:${suffix}`\n}\n","// ---------------------------------------------------------------------------\n// Versioned atomic operations, internal to the Redis policy capabilities.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Bulkhead scripts\n// ---------------------------------------------------------------------------\n\nexport const leaseV1 = `\nlocal action = ARGV[1]\nlocal token = ARGV[2]\nlocal ttl = tonumber(ARGV[3])\nlocal limit = tonumber(ARGV[4])\nlocal time = redis.call('TIME')\nlocal now = time[1] * 1000 + math.floor(time[2] / 1000)\nredis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', now)\nlocal existing = redis.call('ZSCORE', KEYS[1], token)\nif action == 'release' then\n return redis.call('ZREM', KEYS[1], token)\nend\nif action == 'renew' and not existing then return 0 end\nif action == 'acquire' and existing then return 1 end\nif action == 'acquire' and redis.call('ZCARD', KEYS[1]) >= limit then return 0 end\nredis.call('ZADD', KEYS[1], now + ttl, token)\nlocal latest = redis.call('ZREVRANGE', KEYS[1], 0, 0, 'WITHSCORES')\nredis.call('PEXPIREAT', KEYS[1], math.ceil(tonumber(latest[2])))\nreturn 1\n`\n\n/** Returns the decision and occupancy from the same atomic transition. */\nexport const bulkheadLeaseV1 = `local function transition()\\n${leaseV1}\\nend\\nlocal allowed = transition()\\nreturn {allowed, redis.call('ZCARD', KEYS[1])}`\n\n// ---------------------------------------------------------------------------\n// Circuit-breaker scripts\n//\n// Return arrays use fixed positions so the TypeScript caller can validate\n// and decode without field names (which Lua/Redis cannot return).\n//\n// stateCode encoding: 0=closed 1=open 2=half-open\n// ---------------------------------------------------------------------------\n\n/**\n * Record one attempt outcome into the distributed sliding window.\n * Atomically opens the breaker when the failure ratio meets the threshold.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = observations sorted set (:observations)\n *\n * ARGV[1] = outcome \"success\" | \"failure\"\n * ARGV[2] = expectedGeneration integer\n * ARGV[3] = windowTtlMs observation retention window in ms\n * ARGV[4] = minimumThroughput min observations before opening\n * ARGV[5] = failureThresholdNum failure threshold × 1000 (e.g. 500 = 0.5).\n * The policy rejects thresholds that would round\n * to 0 or to 1000 before they reach this script\n * (see assertResolvableThreshold in core).\n * ARGV[6] = windowSize max observations retained by count\n * ARGV[7] = openMs how long to stay OPEN; closed hash TTL = openMs×2\n * ARGV[8] = uuid unique string for member deduplication\n *\n * Returns: {status, stateCode, generation, windowTotal, windowFailures}\n * status 0 = stale (dropped; generation or state mismatch)\n * status 1 = observed, no transition (remained closed)\n * status 2 = observed, breaker opened (stateCode=1, generation incremented)\n *\n * TTL policy:\n * OPEN state → PERSIST (no expiry). A missing key is treated as closed by\n * all scripts, so expiring an open breaker would silently admit\n * unrestricted traffic and reset the generation counter.\n * CLOSED state → max(openMs×2, windowTtlMs) TTL for eventual cleanup of idle\n * scopes. Expiring a closed key is safe *only* because the TTL\n * outlives the observation window: the window is scoped by an\n * epoch, and an epoch that outlives its members is what keeps\n * cleanup from resurrecting them.\n *\n * Epochs:\n * `generation` increments on every transition, and a brand new value is minted\n * whenever the state hash has to be recreated while observation members from a\n * previous epoch are still present (state lost to eviction or admin cleanup).\n * Window membership is decided by comparing the stored epoch, so members from\n * a superseded epoch can never be counted again, and an attempt holding a\n * pre-loss generation can never pass the staleness check. A scope that has\n * never been observed keeps generation 0.\n */\nexport const breakerObserveV1 = `\nlocal outcome = ARGV[1]\nlocal expectGen = tonumber(ARGV[2])\nlocal windowTtl = tonumber(ARGV[3])\nlocal minTP = tonumber(ARGV[4])\nlocal threshNum = tonumber(ARGV[5])\nlocal windowSize = tonumber(ARGV[6])\nlocal openMs = tonumber(ARGV[7])\nlocal uuid = ARGV[8]\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nif state ~= 'closed' then\n local sc = (state == 'open' and 1) or (state == 'half-open' and 2) or 0\n return {0, sc, gen, 0, 0}\nend\nif redis.call('EXISTS', KEYS[1]) == 0 then\n -- No live state hash: a scope we have never seen, or a hash that was lost\n -- while its window survived.\n if expectGen ~= 0 then\n -- The caller holds a generation this key cannot confirm.\n return {0, 0, 0, 0, 0}\n end\n if redis.call('EXISTS', KEYS[2]) == 1 then\n -- Members from a superseded epoch are still here. Mint an epoch that has\n -- never been used for this key (derived from the observation's own uuid, so\n -- no clock is involved) instead of restarting at a value those members\n -- would match.\n --\n -- Bounded to 11 hex digits (44 bits, <= 14 decimal digits) so the value\n -- prints exactly with stock Lua number formatting: the epoch is read back\n -- from the hash as a decimal string by both this script and the client, and\n -- a larger value could be written in scientific notation and break the\n -- round-trip.\n gen = tonumber(string.sub(redis.sha1hex(uuid), 1, 11), 16)\n if not gen or gen == 0 then gen = 1 end\n else\n gen = 0\n end\n redis.call('HSET', KEYS[1], 'state', 'closed', 'generation', gen,\n 'openedAt', 0, 'probeCount', 0, 'probeSuccesses', 0)\nelseif gen ~= expectGen then\n return {0, 0, gen, 0, 0}\nend\nredis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now - windowTtl)\nlocal member = tostring(gen) .. ':' .. uuid .. ':' .. outcome\nredis.call('ZADD', KEYS[2], 'NX', now, member)\nlocal cnt = redis.call('ZCARD', KEYS[2])\nif cnt > windowSize then\n redis.call('ZREMRANGEBYRANK', KEYS[2], 0, cnt - windowSize - 1)\nend\nredis.call('PEXPIREAT', KEYS[2], now + windowTtl)\nlocal all = redis.call('ZRANGE', KEYS[2], 0, -1)\nlocal prefix = tostring(gen) .. ':'\nlocal wTotal = 0\nlocal wFail = 0\nfor _, m in ipairs(all) do\n if string.sub(m, 1, #prefix) == prefix then\n wTotal = wTotal + 1\n if string.sub(m, -8) == ':failure' then wFail = wFail + 1 end\n end\nend\nif wTotal >= minTP and wFail * 1000 >= threshNum * wTotal then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'open', 'generation', gen,\n 'openedAt', now, 'probeCount', 0, 'probeSuccesses', 0)\n -- OPEN state must never expire: a missing key is treated as closed,\n -- which would silently admit unrestricted traffic and reset the generation.\n redis.call('PERSIST', KEYS[1])\n return {2, 1, gen, wTotal, wFail}\nend\nif redis.call('EXISTS', KEYS[1]) == 1 then\n -- Still CLOSED. The cleanup TTL must not expire the state before the window\n -- it governs, otherwise retained members would survive their epoch.\n redis.call('PEXPIRE', KEYS[1], math.max(openMs * 2, windowTtl))\nend\nreturn {1, 0, gen, wTotal, wFail}\n`\n\n/**\n * Admit a probe attempt. Transitions OPEN→HALF_OPEN when openMs has elapsed,\n * then atomically allocates a probe token slot.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = probe tokens sorted set (:probes)\n *\n * ARGV[1] = probeToken unique UUID for this probe attempt\n * ARGV[2] = openMs determines OPEN→HALF_OPEN transition and TTL\n * ARGV[3] = halfOpenProbes maximum concurrent probe tokens\n * ARGV[4] = probeLeaseTtlMs probe token TTL in ms\n *\n * Returns: {status, stateCode, generation, probeCount, transitioned}\n * status 0 = rejected\n * stateCode 0 = breaker already CLOSED (no probe needed)\n * stateCode 1 = still OPEN (openMs not yet elapsed)\n * stateCode 2 = HALF_OPEN but probe limit reached\n * status 1 = admitted (probe token stored)\n * transitioned 1 = OPEN→HALF_OPEN transition happened in this call\n * transitioned 0 = was already HALF_OPEN\n */\nexport const breakerAdmitProbeV1 = `\nlocal probeToken = ARGV[1]\nlocal openMs = tonumber(ARGV[2])\nlocal maxProbes = tonumber(ARGV[3])\nlocal probeLeaseTtl = tonumber(ARGV[4])\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation', 'openedAt',\n 'probeCount', 'probeSuccesses')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nlocal openedAt = tonumber(f[3]) or 0\nlocal transitioned = 0\nif state == 'closed' then\n return {0, 0, gen, 0, 0}\nend\nif state == 'open' then\n if now - openedAt < openMs then\n return {0, 1, gen, 0, 0}\n end\n state = 'half-open'\n transitioned = 1\n redis.call('HSET', KEYS[1], 'state', 'half-open', 'probeSuccesses', 0, 'probeCount', 0)\n -- OPEN/HALF_OPEN must never expire; PERSIST removes any prior cleanup TTL.\n redis.call('PERSIST', KEYS[1])\n -- Tokens from the superseded recovery window must not consume slots in this\n -- one; their settle is dropped by the generation check anyway.\n redis.call('DEL', KEYS[2])\nend\nredis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)\nlocal activeProbes = redis.call('ZCARD', KEYS[2])\nif activeProbes >= maxProbes then\n return {0, 2, gen, activeProbes, transitioned}\nend\nredis.call('ZADD', KEYS[2], 'NX', now + probeLeaseTtl, probeToken)\nactiveProbes = activeProbes + 1\nredis.call('HSET', KEYS[1], 'probeCount', activeProbes)\n-- State hash must not expire while recovery probes are in flight.\nredis.call('PERSIST', KEYS[1])\n-- Probes sorted set expires naturally once the last lease elapses.\nredis.call('PEXPIREAT', KEYS[2], now + probeLeaseTtl + 1000)\nreturn {1, 2, gen, activeProbes, transitioned}\n`\n\n/**\n * Record a probe attempt outcome. Removes the probe token and applies the\n * result: failure re-opens; sufficient successes close the breaker.\n *\n * KEYS[1] = breaker state hash (:breaker)\n * KEYS[2] = probe tokens sorted set (:probes)\n *\n * ARGV[1] = probeToken the token issued by admitProbe\n * ARGV[2] = outcome \"success\" | \"failure\" | \"ignored\", where\n * \"ignored\" releases the slot without recording\n * an outcome or advancing recovery\n * ARGV[3] = expectedGeneration generation captured at admission time\n * ARGV[4] = halfOpenSuccesses consecutive probe successes needed to close\n * ARGV[5] = openMs how long the breaker stays OPEN (TTL floor)\n * ARGV[6] = windowTtlMs observation retention (TTL floor): the CLOSED\n * hash must outlive the window it governs\n *\n * Returns: {status, stateCode, generation}\n * status 0 = stale (token missing, lease elapsed, or generation mismatch;\n * the result is dropped and any consumed token is released)\n * status 1 = settled, no state transition (still HALF_OPEN; an \"ignored\"\n * outcome only releases the probe slot)\n * status 2 = settled, state transition occurred\n * stateCode 0 = transitioned to CLOSED\n * stateCode 1 = transitioned back to OPEN (probe failure)\n *\n * A token carries its own deadline as its sorted-set score. When the lease has\n * elapsed the slot is already recoverable - admitProbe prunes expired tokens\n * and re-issues the slot - so the result must not count even while the member is\n * still present. The deadline is therefore checked under the same atomic call.\n */\nexport const breakerSettleProbeV1 = `\nlocal probeToken = ARGV[1]\nlocal outcome = ARGV[2]\nlocal expectGen = tonumber(ARGV[3])\nlocal halfOpenSucc = tonumber(ARGV[4])\nlocal openMs = tonumber(ARGV[5])\nlocal windowTtl = tonumber(ARGV[6])\nlocal t = redis.call('TIME')\nlocal now = t[1] * 1000 + math.floor(t[2] / 1000)\nlocal f = redis.call('HMGET', KEYS[1], 'state', 'generation', 'probeSuccesses', 'probeCount')\nlocal state = f[1] or 'closed'\nlocal gen = tonumber(f[2]) or 0\nlocal probeSucc = tonumber(f[3]) or 0\nlocal probeCnt = tonumber(f[4]) or 0\nlocal sc = (state == 'open' and 1) or (state == 'half-open' and 2) or 0\nlocal deadline = redis.call('ZSCORE', KEYS[2], probeToken)\nif not deadline then\n -- Never admitted, already settled, or dropped with its recovery window.\n return {0, sc, gen}\nend\nredis.call('ZREM', KEYS[2], probeToken)\nif tonumber(deadline) <= now then\n -- The lease elapsed while the probe was still running: its slot was\n -- recoverable, so the result cannot be counted. Release the slot it still\n -- occupied so the probe count keeps matching the token set.\n probeCnt = math.max(0, probeCnt - 1)\n redis.call('HSET', KEYS[1], 'probeCount', probeCnt)\n return {0, sc, gen}\nend\nif state ~= 'half-open' or gen ~= expectGen then\n return {0, sc, gen}\nend\nprobeCnt = math.max(0, probeCnt - 1)\nif outcome == 'ignored' then\n -- The result was not recorded (the classifier ignored it), but the slot the\n -- probe held must still be released: otherwise the recovery window stalls\n -- until the probe lease elapses instead of letting the next probe through.\n -- The local breaker releases its slot immediately, and this matches it: no\n -- success counter advances and the state does not transition.\n redis.call('HSET', KEYS[1], 'probeCount', probeCnt)\n -- Still HALF_OPEN: must not expire while recovery is in progress.\n redis.call('PERSIST', KEYS[1])\n return {1, 2, gen}\nend\nif outcome == 'failure' then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'open', 'generation', gen,\n 'openedAt', now, 'probeCount', 0, 'probeSuccesses', 0)\n -- Re-opened: OPEN must not expire (see breakerObserveV1 TTL policy).\n redis.call('PERSIST', KEYS[1])\n -- In-flight probes belong to the dead generation; free their slots so the\n -- next recovery window starts with full capacity.\n redis.call('DEL', KEYS[2])\n return {2, 1, gen}\nend\nprobeSucc = probeSucc + 1\nif probeSucc >= halfOpenSucc then\n gen = gen + 1\n redis.call('HSET', KEYS[1], 'state', 'closed', 'generation', gen,\n 'probeCount', 0, 'probeSuccesses', 0)\n -- Newly CLOSED: expire for cleanup, but never before the window it governs.\n redis.call('PEXPIRE', KEYS[1], math.max(openMs * 2, windowTtl))\n redis.call('DEL', KEYS[2])\n return {2, 0, gen}\nend\nredis.call('HSET', KEYS[1], 'probeSuccesses', probeSucc, 'probeCount', probeCnt)\n-- Still HALF_OPEN: must not expire while recovery is in progress.\nredis.call('PERSIST', KEYS[1])\nreturn {1, 2, gen}\n`\n","import type { BulkheadCoordinator } from \"../../core/bulkhead.js\"\nimport { CoordinatorUnavailableError } from \"./client.js\"\nimport { evalScript } from \"./eval-script.js\"\nimport { coordinationKey } from \"./keys.js\"\nimport type { RedisScriptClient } from \"./leases.js\"\nimport { bulkheadLeaseV1 } from \"./scripts.js\"\nexport function redisCoordinator(\n client: RedisScriptClient,\n options: { namespace: string },\n): BulkheadCoordinator {\n const namespace = options.namespace\n coordinationKey(namespace, \"bulkhead\", \"validate\", \"validate\")\n const coordinator: BulkheadCoordinator = {\n async command(identity, action, token, leaseMs, limit) {\n if (\n ![\"acquire\", \"renew\", \"release\"].includes(action) ||\n typeof token !== \"string\" ||\n !token ||\n token.length > 256\n )\n throw new TypeError(\"Invalid bulkhead lease action or token\")\n if (\n ![leaseMs, limit].every(\n (value) => Number.isSafeInteger(value) && value > 0,\n ) ||\n leaseMs > 86400000\n )\n throw new RangeError(\"Invalid bulkhead lease duration or limit\")\n const key = coordinationKey(\n namespace,\n `bulkhead:${identity.name}`,\n identity.operation,\n identity.scope,\n )\n try {\n const result = await evalScript(\n client,\n bulkheadLeaseV1,\n 1,\n key,\n action,\n token,\n leaseMs,\n limit,\n )\n if (\n !Array.isArray(result) ||\n result.length !== 2 ||\n ![0, 1].includes(result[0]) ||\n !Number.isSafeInteger(result[1]) ||\n result[1] < 0\n )\n throw new Error(\"Invalid bulkhead reply\")\n return { allowed: result[0] === 1, occupancy: result[1] as number }\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n }\n return Object.freeze(coordinator)\n}\n","import type {\n AdmitProbeResult,\n BreakerCoordinator,\n BreakerIdentity,\n BreakerState,\n ObserveResult,\n SettleProbeResult,\n} from \"../../core/circuit-breaker.js\"\nimport { CoordinatorUnavailableError } from \"./client.js\"\nimport { evalScript } from \"./eval-script.js\"\nimport { coordinationKey } from \"./keys.js\"\nimport type { RedisScriptClient } from \"./leases.js\"\nimport {\n breakerAdmitProbeV1,\n breakerObserveV1,\n breakerSettleProbeV1,\n} from \"./scripts.js\"\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\nfunction breakerKeys(\n namespace: string,\n identity: BreakerIdentity,\n): [hashKey: string, obsKey: string, probeKey: string] {\n const policy = `breaker:${identity.name}`\n const hash = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"breaker\",\n )\n const obs = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"observations\",\n )\n const prob = coordinationKey(\n namespace,\n policy,\n identity.operation,\n identity.scope,\n \"probes\",\n )\n return [hash, obs, prob]\n}\n\n// ---------------------------------------------------------------------------\n// Reply validation helpers\n// ---------------------------------------------------------------------------\n\nfunction assertArray(reply: unknown, minLen: number, label: string): unknown[] {\n if (\n !Array.isArray(reply) ||\n reply.length < minLen ||\n reply.some((v) => typeof v !== \"number\" || !Number.isSafeInteger(v))\n ) {\n throw new Error(\n `Invalid ${label} reply from Redis: ${JSON.stringify(reply)}`,\n )\n }\n return reply as unknown[]\n}\n\nconst STATE_CODES: BreakerState[] = [\"closed\", \"open\", \"half-open\"]\n\nfunction decodeState(code: number): BreakerState {\n const s = STATE_CODES[code]\n if (!s) throw new Error(`Unknown breaker state code: ${code}`)\n return s\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Policy-specific Redis coordinator for `circuitBreaker.distributed()`.\n *\n * The caller owns `client` and must connect/disconnect it independently.\n * `namespace` is prepended to every key; use a per-service, per-environment\n * value to avoid cross-deployment state collisions.\n */\nexport function redisCircuitBreakerCoordinator(\n client: RedisScriptClient,\n options: { readonly namespace: string },\n): BreakerCoordinator {\n const { namespace } = options\n // Validate namespace eagerly (coordinationKey throws on bad input).\n coordinationKey(\n namespace,\n \"breaker:validate\",\n \"validate\",\n \"validate\",\n \"breaker\",\n )\n\n const coordinator: BreakerCoordinator = {\n async readState(identity) {\n const [hashKey] = breakerKeys(namespace, identity)\n try {\n const fields = await client.hmget(hashKey, \"state\", \"generation\")\n const rawState = fields[0]\n if (!rawState) return null\n const state = rawState as BreakerState\n if (!STATE_CODES.includes(state))\n throw new Error(`Unknown breaker state: ${rawState}`)\n const generation = parseInt(fields[1] ?? \"0\", 10)\n return { state, generation }\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async observe(identity, params) {\n const [hashKey, obsKey] = breakerKeys(namespace, identity)\n const {\n generation,\n outcome,\n uuid,\n windowTtlMs,\n minimumThroughput,\n failureThresholdNumerator,\n windowSize,\n openMs,\n } = params\n try {\n const reply = await evalScript(\n client,\n breakerObserveV1,\n 2,\n hashKey,\n obsKey,\n outcome,\n generation,\n windowTtlMs,\n minimumThroughput,\n failureThresholdNumerator,\n windowSize,\n openMs,\n uuid,\n )\n const r = assertArray(reply, 5, \"breakerObserveV1\") as number[]\n const [status, , newGen, windowTotal, windowFailures] = r\n if (status === 0) {\n return {\n type: \"stale\",\n currentGeneration: newGen,\n } satisfies ObserveResult\n }\n if (status === 2) {\n return {\n type: \"opened\",\n newGeneration: newGen,\n windowTotal,\n windowFailures,\n } satisfies ObserveResult\n }\n return {\n type: \"observed\",\n generation: newGen,\n windowTotal,\n windowFailures,\n } satisfies ObserveResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async admitProbe(identity, params) {\n const [hashKey, , probeKey] = breakerKeys(namespace, identity)\n const { probeToken, openMs, halfOpenProbes, probeLeaseTtlMs } = params\n try {\n const reply = await evalScript(\n client,\n breakerAdmitProbeV1,\n 2,\n hashKey,\n probeKey,\n probeToken,\n openMs,\n halfOpenProbes,\n probeLeaseTtlMs,\n )\n const r = assertArray(reply, 5, \"breakerAdmitProbeV1\") as number[]\n const [status, stateCode, gen, probeCount, transitioned] = r\n if (status === 0) {\n const reason =\n stateCode === 0\n ? \"closed\"\n : stateCode === 1\n ? \"open\"\n : \"probe-limit\"\n return {\n type: \"rejected\",\n reason,\n generation: gen,\n } satisfies AdmitProbeResult\n }\n return {\n type: \"admitted\",\n generation: gen,\n probeCount,\n stateChanged: transitioned === 1,\n } satisfies AdmitProbeResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n\n async settleProbe(identity, params) {\n const [hashKey, , probeKey] = breakerKeys(namespace, identity)\n const {\n probeToken,\n outcome,\n generation,\n halfOpenSuccesses,\n openMs,\n windowTtlMs,\n } = params\n try {\n const reply = await evalScript(\n client,\n breakerSettleProbeV1,\n 2,\n hashKey,\n probeKey,\n probeToken,\n outcome,\n generation,\n halfOpenSuccesses,\n openMs,\n // Floor for the CLOSED cleanup TTL; older callers that do not pass it\n // keep the historical openMs x 2 behaviour.\n windowTtlMs ?? openMs * 2,\n )\n const r = assertArray(reply, 3, \"breakerSettleProbeV1\") as number[]\n const [status, stateCode, newGen] = r\n if (status === 0) {\n return {\n type: \"stale\",\n generation: newGen,\n } satisfies SettleProbeResult\n }\n const state = decodeState(stateCode)\n if (status === 2) {\n return {\n type: \"transitioned\",\n newState: state,\n newGeneration: newGen,\n previousState: \"half-open\",\n } satisfies SettleProbeResult\n }\n return {\n type: \"settled\",\n state,\n generation: newGen,\n } satisfies SettleProbeResult\n } catch (error) {\n throw new CoordinatorUnavailableError(error)\n }\n },\n }\n\n return Object.freeze(coordinator)\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as Outcome, O as OperationCapabilities, e as ExecutionMetadata, P as Policy } from './types-
|
|
1
|
+
import { h as Outcome, O as OperationCapabilities, e as ExecutionMetadata, P as Policy } from './types-C-Ml-MKp.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Settled-attempt information passed to a custom `delay` function.
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as OperationCapabilities, h as Outcome, C as Classification, A as Adapter } from '../types-
|
|
1
|
+
import { O as OperationCapabilities, h as Outcome, C as Classification, A as Adapter } from '../types-C-Ml-MKp.js';
|
|
2
2
|
|
|
3
3
|
interface AdapterContractSuccess<Args, Result> {
|
|
4
4
|
readonly args: Args;
|
package/dist/testing/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../test/harness/adapter-contract.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../test/harness/adapter-contract.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { operation } from \"../../src/core/operation.js\"\nimport type {\n Adapter,\n Classification,\n OperationCapabilities,\n OperationEvent,\n Outcome,\n} from \"../../src/core/types.js\"\n\nexport interface AdapterContractSuccess<Args, Result> {\n readonly args: Args\n readonly assertResult?: (result: Result) => void | Promise<void>\n}\n\nexport interface AdapterContractCapabilityCase<Args> {\n readonly args: Args\n readonly expected: OperationCapabilities\n}\n\nexport interface AdapterContractClassificationCase<Result> {\n readonly outcome: Outcome<Result>\n readonly expected: Classification\n}\n\nexport interface AdapterContractAbortCase<Args> {\n readonly args: Args\n readonly verify: (controls: {\n readonly controller: AbortController\n readonly execute: () => Promise<unknown>\n }) => void | Promise<void>\n}\n\nexport interface AdapterContractOptions<Args, Result> {\n readonly name: string\n readonly adapter: Adapter<Args, Result>\n readonly success: AdapterContractSuccess<Args, Result>\n readonly capabilities: readonly AdapterContractCapabilityCase<Args>[]\n readonly classifications?: readonly AdapterContractClassificationCase<Result>[]\n readonly abort?: AdapterContractAbortCase<Args>\n}\n\nexport interface AdapterContractCheck {\n readonly name: string\n run(): Promise<void>\n}\n\nexport interface AdapterContractSuite {\n readonly name: string\n readonly checks: readonly AdapterContractCheck[]\n}\n\nfunction assertEqual<T>(actual: T, expected: T, message: string): void {\n if (!Object.is(actual, expected)) {\n throw new Error(\n `${message}: expected ${String(expected)}, received ${String(actual)}`,\n )\n }\n}\n\nfunction assertLifecycle(events: readonly OperationEvent[]): void {\n const types = events.map((event) => event.type)\n const expected = [\n \"execution.started\",\n \"attempt.started\",\n \"attempt.settled\",\n \"execution.settled\",\n ]\n if (\n types.length !== expected.length ||\n types.some((type, index) => type !== expected[index])\n ) {\n throw new Error(\n `expected operation lifecycle ${expected.join(\" -> \")}; received ${types.join(\" -> \")}`,\n )\n }\n}\n\n/**\n * Returns runner-agnostic checks for a third-party adapter. Register each\n * check with the application's test runner; this module imports no test runner.\n */\nexport function defineAdapterContractSuite<Args, Result>(\n options: AdapterContractOptions<Args, Result>,\n): AdapterContractSuite {\n const checks: AdapterContractCheck[] = options.capabilities.map(\n (capabilityCase, index) => ({\n name: `${options.name}: capabilities ${index + 1}`,\n async run(): Promise<void> {\n const actual = options.adapter.capabilities(capabilityCase.args)\n assertEqual(\n actual.abort,\n capabilityCase.expected.abort,\n \"abort capability\",\n )\n assertEqual(\n actual.replay,\n capabilityCase.expected.replay,\n \"replay capability\",\n )\n },\n }),\n )\n\n checks.push({\n name: `${options.name}: successful operation lifecycle`,\n async run(): Promise<void> {\n const events: OperationEvent[] = []\n const subject = operation({\n name: `adapter-contract:${options.name}`,\n adapter: options.adapter,\n events: { emit: (event) => events.push(event) },\n })\n const result = await subject.execute(options.success.args, {\n executionId: \"adapter-contract\",\n })\n await options.success.assertResult?.(result)\n assertLifecycle(events)\n },\n })\n\n for (const [index, classificationCase] of (\n options.classifications ?? []\n ).entries()) {\n checks.push({\n name: `${options.name}: classification ${index + 1}`,\n async run(): Promise<void> {\n const actual =\n options.adapter.classify?.(classificationCase.outcome) ??\n (classificationCase.outcome.status === \"success\"\n ? \"success\"\n : \"failure\")\n assertEqual(\n actual,\n classificationCase.expected,\n \"outcome classification\",\n )\n },\n })\n }\n\n if (options.abort !== undefined) {\n checks.push({\n name: `${options.name}: abort behavior`,\n async run(): Promise<void> {\n const controller = new AbortController()\n const subject = operation({\n name: `adapter-contract:${options.name}`,\n adapter: options.adapter,\n })\n await options.abort?.verify({\n controller,\n execute: () =>\n subject.execute(options.abort?.args as Args, {\n signal: controller.signal,\n }),\n })\n },\n })\n }\n\n return Object.freeze({ name: options.name, checks: Object.freeze(checks) })\n}\n\nexport async function runAdapterContractSuite(\n suite: AdapterContractSuite,\n): Promise<void> {\n for (const check of suite.checks) {\n await check.run()\n }\n}\n"]}
|
|
@@ -154,6 +154,13 @@ type OperationEvent = BreakerStateChangedEvent | BreakerRejectedEvent | BreakerO
|
|
|
154
154
|
context: ExecutionContext;
|
|
155
155
|
outcome: Outcome<undefined>;
|
|
156
156
|
classification: Classification;
|
|
157
|
+
}> | Readonly<{
|
|
158
|
+
type: "retry.declined";
|
|
159
|
+
at: number;
|
|
160
|
+
context: ExecutionContext;
|
|
161
|
+
outcome: Outcome<undefined>;
|
|
162
|
+
classification: Classification;
|
|
163
|
+
reason: "replay-unsafe" | "not-retryable";
|
|
157
164
|
}>;
|
|
158
165
|
/** Output-only observability contract. Sinks cannot alter policy execution. */
|
|
159
166
|
interface EventSink {
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_TIMER_MS } from "../../core/runtime.js"
|
|
1
2
|
import type { RetryContext } from "../../core/retry.js"
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -34,7 +35,10 @@ function headersOf(value: unknown): Headers | undefined {
|
|
|
34
35
|
* response-bearing error.
|
|
35
36
|
*
|
|
36
37
|
* Returns `undefined` when no usable header is present. Malformed values
|
|
37
|
-
* are ignored rather than thrown.
|
|
38
|
+
* are ignored rather than thrown. Values above `MAX_TIMER_MS` are clamped,
|
|
39
|
+
* because `setTimeout` cannot schedule them and would silently fire almost
|
|
40
|
+
* immediately; `createRetryAfterDelay` applies the same ceiling to the wait it
|
|
41
|
+
* returns, so a delay composed from this parser stays schedulable.
|
|
38
42
|
*/
|
|
39
43
|
export function retryAfterMs(
|
|
40
44
|
context: RetryContext,
|
|
@@ -48,11 +52,15 @@ export function retryAfterMs(
|
|
|
48
52
|
|
|
49
53
|
if (deltaSecondsPattern.test(value)) {
|
|
50
54
|
const seconds = Number(value)
|
|
51
|
-
return Number.isSafeInteger(seconds)
|
|
55
|
+
return Number.isSafeInteger(seconds)
|
|
56
|
+
? Math.min(seconds * 1000, MAX_TIMER_MS)
|
|
57
|
+
: undefined
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
const timestamp = Date.parse(value)
|
|
55
|
-
return Number.isNaN(timestamp)
|
|
61
|
+
return Number.isNaN(timestamp)
|
|
62
|
+
? undefined
|
|
63
|
+
: Math.min(Math.max(0, timestamp - now), MAX_TIMER_MS)
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/** Options for `createRetryAfterDelay`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cluster, Redis } from "ioredis"
|
|
1
|
+
import { Cluster, Redis, type RedisOptions } from "ioredis"
|
|
2
2
|
|
|
3
3
|
export class CoordinatorUnavailableError extends Error {
|
|
4
4
|
readonly coordination = "distributed"
|
|
@@ -54,19 +54,33 @@ export interface ClusterNode {
|
|
|
54
54
|
* Call `connect()` before use and `disconnect()` at shutdown, matching the
|
|
55
55
|
* standalone client contract.
|
|
56
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* Redis Cluster client for a secured cluster: pass credentials and TLS via
|
|
59
|
+
* `connectionOptions`. The coordination safeguards (`lazyConnect`, no offline
|
|
60
|
+
* queue, no command replay, no per-request retries) are applied after them and
|
|
61
|
+
* cannot be overridden, because the coordinators depend on those semantics.
|
|
62
|
+
*/
|
|
57
63
|
export function createCoordinationClusterClient(
|
|
58
64
|
nodes: ReadonlyArray<ClusterNode>,
|
|
59
65
|
commandTimeout = 1000,
|
|
66
|
+
connectionOptions: RedisOptions = {},
|
|
60
67
|
): Cluster {
|
|
61
68
|
if (!Number.isSafeInteger(commandTimeout) || commandTimeout < 1)
|
|
62
69
|
throw new RangeError("commandTimeout must be a positive integer")
|
|
63
70
|
if (!Array.isArray(nodes) || nodes.length === 0)
|
|
64
71
|
throw new RangeError("nodes must be a non-empty array of { host, port }")
|
|
72
|
+
if (
|
|
73
|
+
typeof connectionOptions !== "object" ||
|
|
74
|
+
connectionOptions === null ||
|
|
75
|
+
Array.isArray(connectionOptions)
|
|
76
|
+
)
|
|
77
|
+
throw new TypeError("connectionOptions must be an ioredis options object")
|
|
65
78
|
const cluster = new Cluster([...nodes], {
|
|
66
79
|
lazyConnect: true,
|
|
67
80
|
enableOfflineQueue: false,
|
|
68
81
|
clusterRetryStrategy: (attempt) => Math.min(attempt * 50, 1000),
|
|
69
82
|
redisOptions: {
|
|
83
|
+
...connectionOptions,
|
|
70
84
|
commandTimeout,
|
|
71
85
|
connectTimeout: commandTimeout,
|
|
72
86
|
maxRetriesPerRequest: 0,
|
package/src/core/bulkhead.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto"
|
|
2
2
|
import {
|
|
3
|
+
MAX_TIMER_MS,
|
|
3
4
|
admissionSignal,
|
|
4
5
|
emitRuntimeEvent,
|
|
5
6
|
withAdmissionSignal,
|
|
@@ -81,7 +82,7 @@ function local(options: LocalBulkheadOptions): Policy & {
|
|
|
81
82
|
queue.limit < 1 ||
|
|
82
83
|
!Number.isSafeInteger(queue.timeoutMs) ||
|
|
83
84
|
queue.timeoutMs < 1 ||
|
|
84
|
-
queue.timeoutMs >
|
|
85
|
+
queue.timeoutMs > MAX_TIMER_MS)
|
|
85
86
|
)
|
|
86
87
|
throw new RangeError("Invalid bounded queue")
|
|
87
88
|
let occupancy = 0
|
|
@@ -275,13 +276,26 @@ function distributed(
|
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
try {
|
|
278
|
-
if (performance.now() >= deadline)
|
|
279
|
+
if (performance.now() >= deadline) {
|
|
280
|
+
// The permit was acquired, but its lease deadline passed before the
|
|
281
|
+
// call could start. Report it like any other rejection, so a sink
|
|
282
|
+
// sees the attempt instead of only the caller seeing the error.
|
|
283
|
+
event(
|
|
284
|
+
context,
|
|
285
|
+
"distributed",
|
|
286
|
+
name,
|
|
287
|
+
scope,
|
|
288
|
+
"rejected",
|
|
289
|
+
admitted.occupancy,
|
|
290
|
+
"admission-expired",
|
|
291
|
+
)
|
|
279
292
|
throw new BulkheadRejectedError(
|
|
280
293
|
"distributed",
|
|
281
294
|
name,
|
|
282
295
|
scope,
|
|
283
296
|
"admission-expired",
|
|
284
297
|
)
|
|
298
|
+
}
|
|
285
299
|
admissionSignal(context)?.throwIfAborted()
|
|
286
300
|
event(
|
|
287
301
|
context,
|
package/src/core/operation.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
admissionSignal,
|
|
5
5
|
createClassifier,
|
|
6
6
|
createExecutionContext,
|
|
7
|
+
emitToSink,
|
|
7
8
|
} from "./runtime.js"
|
|
8
9
|
import type {
|
|
9
10
|
Adapter,
|
|
@@ -51,11 +52,7 @@ function normalizeSinks(events: EventSinks | undefined): readonly EventSink[] {
|
|
|
51
52
|
|
|
52
53
|
function emit(sinks: readonly EventSink[], event: OperationEvent): void {
|
|
53
54
|
for (const sink of sinks) {
|
|
54
|
-
|
|
55
|
-
sink.emit(event)
|
|
56
|
-
} catch {
|
|
57
|
-
// Observability must not modify resilience execution.
|
|
58
|
-
}
|
|
55
|
+
emitToSink(sink, event)
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
58
|
|
package/src/core/retry.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MAX_TIMER_MS,
|
|
3
|
+
admissionSignal,
|
|
4
|
+
emitRuntimeEvent,
|
|
5
|
+
nextAttempt,
|
|
6
|
+
} from "./runtime.js"
|
|
2
7
|
import type {
|
|
3
8
|
Classification,
|
|
4
9
|
ExecutionContext,
|
|
@@ -71,6 +76,11 @@ function delayFor(
|
|
|
71
76
|
if (!Number.isFinite(delay) || delay < 0) {
|
|
72
77
|
throw new RangeError("retry delay must be a finite non-negative number")
|
|
73
78
|
}
|
|
79
|
+
if (delay > MAX_TIMER_MS) {
|
|
80
|
+
throw new RangeError(
|
|
81
|
+
`retry delay must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
return delay
|
|
76
86
|
}
|
|
@@ -124,6 +134,16 @@ export function retry(options: RetryOptions): Policy {
|
|
|
124
134
|
if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) {
|
|
125
135
|
throw new RangeError("maxAttempts must be a positive integer")
|
|
126
136
|
}
|
|
137
|
+
if (
|
|
138
|
+
typeof options.delay === "number" &&
|
|
139
|
+
(!Number.isFinite(options.delay) ||
|
|
140
|
+
options.delay < 0 ||
|
|
141
|
+
options.delay > MAX_TIMER_MS)
|
|
142
|
+
) {
|
|
143
|
+
throw new RangeError(
|
|
144
|
+
`retry delay must be a finite number within 0..${MAX_TIMER_MS} ms`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
127
147
|
|
|
128
148
|
return Object.freeze({
|
|
129
149
|
name: "retry",
|
|
@@ -144,6 +164,17 @@ export function retry(options: RetryOptions): Policy {
|
|
|
144
164
|
outcomeClassification !== "retryable" ||
|
|
145
165
|
context.capabilities.replay !== "safe"
|
|
146
166
|
) {
|
|
167
|
+
// Declining to retry is observable: without this, a call that was
|
|
168
|
+
// never retried looks identical to one with no retry policy.
|
|
169
|
+
emitRuntimeEvent(context, {
|
|
170
|
+
type: "retry.declined",
|
|
171
|
+
outcome: summarized(outcome),
|
|
172
|
+
classification: outcomeClassification,
|
|
173
|
+
reason:
|
|
174
|
+
context.capabilities.replay !== "safe"
|
|
175
|
+
? "replay-unsafe"
|
|
176
|
+
: "not-retryable",
|
|
177
|
+
})
|
|
147
178
|
return value
|
|
148
179
|
}
|
|
149
180
|
|
|
@@ -170,11 +201,24 @@ export function retry(options: RetryOptions): Policy {
|
|
|
170
201
|
const outcome: Outcome<Result> = { status: "failure", error }
|
|
171
202
|
const outcomeClassification = classification(context, outcome)
|
|
172
203
|
|
|
204
|
+
if (admissionSignal(context)?.aborted) {
|
|
205
|
+
throw error
|
|
206
|
+
}
|
|
173
207
|
if (
|
|
174
|
-
admissionSignal(context)?.aborted ||
|
|
175
208
|
context.capabilities.replay !== "safe" ||
|
|
176
209
|
outcomeClassification !== "retryable"
|
|
177
210
|
) {
|
|
211
|
+
// Same event as the value path: the caller can tell a declined
|
|
212
|
+
// retry from a missing retry policy.
|
|
213
|
+
emitRuntimeEvent(context, {
|
|
214
|
+
type: "retry.declined",
|
|
215
|
+
outcome: summarized(outcome),
|
|
216
|
+
classification: outcomeClassification,
|
|
217
|
+
reason:
|
|
218
|
+
context.capabilities.replay !== "safe"
|
|
219
|
+
? "replay-unsafe"
|
|
220
|
+
: "not-retryable",
|
|
221
|
+
})
|
|
178
222
|
throw error
|
|
179
223
|
}
|
|
180
224
|
|
package/src/core/runtime.ts
CHANGED
|
@@ -94,6 +94,31 @@ export function withSignal(
|
|
|
94
94
|
)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Largest delay `setTimeout` honours. Anything above it is silently clamped to
|
|
99
|
+
* 1 ms by the platform, so accepting larger values would turn a long wait into
|
|
100
|
+
* an immediate one.
|
|
101
|
+
*/
|
|
102
|
+
export const MAX_TIMER_MS = 2_147_483_647
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delivers one event to one sink, isolating execution from it. Sinks are
|
|
106
|
+
* fire-and-forget: a synchronous throw and a rejected promise are both dropped,
|
|
107
|
+
* so a failing sink can neither modify resilience execution nor surface as an
|
|
108
|
+
* unhandled rejection.
|
|
109
|
+
*/
|
|
110
|
+
export function emitToSink(sink: EventSink, event: OperationEvent): void {
|
|
111
|
+
try {
|
|
112
|
+
const pending = sink.emit(event) as unknown
|
|
113
|
+
const thenable = pending as { catch?: unknown } | null | undefined
|
|
114
|
+
if (typeof thenable?.catch === "function") {
|
|
115
|
+
void (pending as Promise<unknown>).catch(() => {})
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// See above: a failing sink must never modify execution.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
export function emitRuntimeEvent(
|
|
98
123
|
context: ExecutionContext,
|
|
99
124
|
event: EventWithoutRuntimeFields,
|
|
@@ -102,11 +127,7 @@ export function emitRuntimeEvent(
|
|
|
102
127
|
const fullEvent = { ...event, at: Date.now(), context } as OperationEvent
|
|
103
128
|
|
|
104
129
|
for (const sink of sinks) {
|
|
105
|
-
|
|
106
|
-
sink.emit(fullEvent)
|
|
107
|
-
} catch {
|
|
108
|
-
// Observability must not modify resilience execution.
|
|
109
|
-
}
|
|
130
|
+
emitToSink(sink, fullEvent)
|
|
110
131
|
}
|
|
111
132
|
}
|
|
112
133
|
|
package/src/core/timeout.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MAX_TIMER_MS,
|
|
3
|
+
emitRuntimeEvent,
|
|
4
|
+
withAdmissionSignal,
|
|
5
|
+
withSignal,
|
|
6
|
+
} from "./runtime.js"
|
|
2
7
|
import type { ExecutionContext, Next, Policy } from "./types.js"
|
|
3
8
|
|
|
4
9
|
export class TimeoutError extends Error {
|
|
@@ -31,6 +36,11 @@ export function timeout(options: TimeoutOptions): Policy {
|
|
|
31
36
|
if (!Number.isFinite(options.ms) || options.ms <= 0) {
|
|
32
37
|
throw new RangeError("timeout ms must be a finite positive number")
|
|
33
38
|
}
|
|
39
|
+
if (options.ms > MAX_TIMER_MS) {
|
|
40
|
+
throw new RangeError(
|
|
41
|
+
`timeout ms must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
return Object.freeze({
|
|
36
46
|
name: "timeout",
|
package/src/core/types.ts
CHANGED
|
@@ -190,6 +190,14 @@ export type OperationEvent =
|
|
|
190
190
|
outcome: Outcome<undefined>
|
|
191
191
|
classification: Classification
|
|
192
192
|
}>
|
|
193
|
+
| Readonly<{
|
|
194
|
+
type: "retry.declined"
|
|
195
|
+
at: number
|
|
196
|
+
context: ExecutionContext
|
|
197
|
+
outcome: Outcome<undefined>
|
|
198
|
+
classification: Classification
|
|
199
|
+
reason: "replay-unsafe" | "not-retryable"
|
|
200
|
+
}>
|
|
193
201
|
|
|
194
202
|
/** Output-only observability contract. Sinks cannot alter policy execution. */
|
|
195
203
|
export interface EventSink {
|