@nodii/retry 0.0.0 → 0.1.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/README.md +214 -1
- package/dist/adapters/bullmq.d.ts +38 -0
- package/dist/adapters/bullmq.d.ts.map +1 -0
- package/dist/adapters/bullmq.js +52 -0
- package/dist/adapters/bullmq.js.map +1 -0
- package/dist/attempts.d.ts +27 -0
- package/dist/attempts.d.ts.map +1 -0
- package/dist/attempts.js +69 -0
- package/dist/attempts.js.map +1 -0
- package/dist/classify.d.ts +41 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +75 -0
- package/dist/classify.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +44 -0
- package/dist/errors.js.map +1 -0
- package/dist/fleet-policies.d.ts +220 -0
- package/dist/fleet-policies.d.ts.map +1 -0
- package/dist/fleet-policies.js +232 -0
- package/dist/fleet-policies.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/jitter.d.ts +47 -0
- package/dist/jitter.d.ts.map +1 -0
- package/dist/jitter.js +84 -0
- package/dist/jitter.js.map +1 -0
- package/dist/policy.d.ts +95 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +121 -0
- package/dist/policy.js.map +1 -0
- package/dist/run.d.ts +83 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +113 -0
- package/dist/run.js.map +1 -0
- package/package.json +34 -5
- package/index.js +0 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { RetryPolicy } from "./policy.js";
|
|
2
|
+
export interface FleetSite {
|
|
3
|
+
/** Where the hand-rolled implementation lives on origin/main. */
|
|
4
|
+
readonly source: string;
|
|
5
|
+
/** The policy that reproduces it (or, where noted, corrects it). */
|
|
6
|
+
readonly policy: RetryPolicy;
|
|
7
|
+
/** What the site treats as retryable. */
|
|
8
|
+
readonly retryable: string;
|
|
9
|
+
/** Behaviour-preserving, or a deliberate change? */
|
|
10
|
+
readonly migration: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const FLEET_POLICIES: {
|
|
13
|
+
readonly "approval.consumer": {
|
|
14
|
+
readonly source: "ts/approval/src/consumer.ts:161 (BullMQ settings.backoffStrategy)";
|
|
15
|
+
readonly policy: {
|
|
16
|
+
readonly maxAttempts: 5;
|
|
17
|
+
readonly baseDelayMs: 250;
|
|
18
|
+
readonly multiplier: 2;
|
|
19
|
+
readonly maxDelayMs: 60000;
|
|
20
|
+
readonly growth: "exponential";
|
|
21
|
+
readonly jitter: "none";
|
|
22
|
+
readonly allowJitterAboveCap: false;
|
|
23
|
+
};
|
|
24
|
+
readonly retryable: "anything the handler throws; terminal at attemptsMade+1 >= consumerMaxRetries (consumer.ts:621 — the post-increment test, correct)";
|
|
25
|
+
readonly migration: "bullmqBackoffStrategy(FLEET_POLICIES['approval.consumer'].policy, { attemptOffset: 1 }) is byte-preserving. attemptOffset 0 (the correct index) makes the first retry 250ms instead of 500ms — a deliberate change, and the one worth taking, since 250ms is what the code claims its base is.";
|
|
26
|
+
};
|
|
27
|
+
readonly "approval.streamsReader": {
|
|
28
|
+
readonly source: "ts/approval/src/streams-reader.ts:389 (loop-error backoff)";
|
|
29
|
+
readonly policy: {
|
|
30
|
+
readonly maxAttempts: 0;
|
|
31
|
+
readonly baseDelayMs: 200;
|
|
32
|
+
readonly multiplier: 1;
|
|
33
|
+
readonly maxDelayMs: 200;
|
|
34
|
+
readonly growth: "exponential";
|
|
35
|
+
readonly jitter: "none";
|
|
36
|
+
readonly allowJitterAboveCap: false;
|
|
37
|
+
};
|
|
38
|
+
readonly retryable: "any XREADGROUP / handler throw; the loop never gives up";
|
|
39
|
+
readonly migration: "Behaviour-preserving as written. DELIBERATE CHANGE worth making: multiplier 2 + cap 30s + equal jitter, so a long Redis outage stops issuing 5 commands/second per pod.";
|
|
40
|
+
};
|
|
41
|
+
readonly "onboarding.submitLead": {
|
|
42
|
+
readonly source: "ts/onboarding/src/submit/client.ts:37 (MAX_ATTEMPTS/RETRY_DELAYS_MS)";
|
|
43
|
+
readonly policy: {
|
|
44
|
+
readonly maxAttempts: 3;
|
|
45
|
+
readonly baseDelayMs: 1000;
|
|
46
|
+
readonly multiplier: 2;
|
|
47
|
+
readonly maxDelayMs: number;
|
|
48
|
+
readonly growth: "exponential";
|
|
49
|
+
readonly jitter: "none";
|
|
50
|
+
readonly allowJitterAboveCap: false;
|
|
51
|
+
};
|
|
52
|
+
readonly retryable: "network error + request timeout + HTTP 5xx. NOT: 403, 409, 422, 429 (429 is caller-decides, deliberately not auto-retried)";
|
|
53
|
+
readonly migration: "Behaviour-preserving: the [1000, 2000] literal table IS base 1000 × 2^(n-1) for n = 1,2. Retryability moves into a shouldRetry predicate; the 403/409/422 paths become NonRetryableError throws.";
|
|
54
|
+
};
|
|
55
|
+
readonly "onboarding.submitSignup": {
|
|
56
|
+
readonly source: "ts/onboarding/src/submit/signup-client.ts:44 (verbatim copy of the above)";
|
|
57
|
+
readonly policy: {
|
|
58
|
+
readonly maxAttempts: 3;
|
|
59
|
+
readonly baseDelayMs: 1000;
|
|
60
|
+
readonly multiplier: 2;
|
|
61
|
+
readonly maxDelayMs: number;
|
|
62
|
+
readonly growth: "exponential";
|
|
63
|
+
readonly jitter: "none";
|
|
64
|
+
readonly allowJitterAboveCap: false;
|
|
65
|
+
};
|
|
66
|
+
readonly retryable: "identical to onboarding.submitLead";
|
|
67
|
+
readonly migration: "Behaviour-preserving. This site and submitLead are a literal duplicate — one shared policy constant removes the copy.";
|
|
68
|
+
};
|
|
69
|
+
readonly "outboxDispatcher.claimCooldown": {
|
|
70
|
+
readonly source: "ts/outbox-dispatcher/src/sql-builders.ts:39 (`last_attempt_at < now() - interval '1 minute' * publish_attempts`) + define-dispatcher.ts:18 (DEFAULT_PUBLISH_MAX_ATTEMPTS)";
|
|
71
|
+
readonly policy: {
|
|
72
|
+
readonly maxAttempts: 8;
|
|
73
|
+
readonly baseDelayMs: 60000;
|
|
74
|
+
readonly multiplier: 1;
|
|
75
|
+
readonly maxDelayMs: number;
|
|
76
|
+
readonly growth: "linear";
|
|
77
|
+
readonly jitter: "none";
|
|
78
|
+
readonly allowJitterAboveCap: false;
|
|
79
|
+
};
|
|
80
|
+
readonly retryable: "every publish failure. D401: the row is NEVER deleted and never DLQ'd; maxAttempts caps RE-CLAIM only, and the 24h sweep raises the M17 page.";
|
|
81
|
+
readonly migration: "NOT a drop-in: the delay is enforced by a SQL predicate, not a sleep. The package's role here is to GENERATE the interval expression / validate the constants, so the TS, Python and Go ports of the dispatcher cannot drift apart. Behaviour-preserving if the SQL keeps `interval '1 minute' * publish_attempts`.";
|
|
82
|
+
};
|
|
83
|
+
readonly "outboxDispatcher.drainLoop": {
|
|
84
|
+
readonly source: "ts/outbox-dispatcher/src/drainer.ts:421 (`sleep(Math.min(pollMs, 200))` on error)";
|
|
85
|
+
readonly policy: {
|
|
86
|
+
readonly maxAttempts: 0;
|
|
87
|
+
readonly baseDelayMs: 200;
|
|
88
|
+
readonly multiplier: 1;
|
|
89
|
+
readonly maxDelayMs: 200;
|
|
90
|
+
readonly growth: "exponential";
|
|
91
|
+
readonly jitter: "none";
|
|
92
|
+
readonly allowJitterAboveCap: false;
|
|
93
|
+
};
|
|
94
|
+
readonly retryable: "any drain-iteration throw; the loop never gives up";
|
|
95
|
+
readonly migration: "Behaviour-preserving. Same accidental-divergence class as approval.streamsReader and telemetry.outboxDispatcherLoop — three fixed error sleeps, three different rationales, no jitter anywhere.";
|
|
96
|
+
};
|
|
97
|
+
readonly "replicaConsumer.coldStart": {
|
|
98
|
+
readonly source: "ts/replica-consumer/src/start.ts:242 (coldStartBackoffMs) + DEFAULT_COLD_START_RETRY";
|
|
99
|
+
readonly policy: {
|
|
100
|
+
readonly maxAttempts: 0;
|
|
101
|
+
readonly baseDelayMs: 1000;
|
|
102
|
+
readonly multiplier: 2;
|
|
103
|
+
readonly maxDelayMs: 30000;
|
|
104
|
+
readonly growth: "exponential";
|
|
105
|
+
readonly jitter: {
|
|
106
|
+
readonly kind: "proportional";
|
|
107
|
+
readonly pct: 0.2;
|
|
108
|
+
};
|
|
109
|
+
readonly allowJitterAboveCap: true;
|
|
110
|
+
};
|
|
111
|
+
readonly retryable: "every bootstrap failure (fail-soft: the process stays alive)";
|
|
112
|
+
readonly migration: "Byte-preserving with allowJitterAboveCap: true — the jitter formula `capped·(1 + 0.2·(2r−1))` is identical to this package's proportional strategy for the same RNG draw. Flipping it to false is a DELIBERATE change that makes the declared 30s cap actually hold (max delay 36s → 30s).";
|
|
113
|
+
};
|
|
114
|
+
readonly "replicaConsumer.declaredApplyBackoff": {
|
|
115
|
+
readonly source: "ts/replica-consumer/src/define-replica.ts:294 + init.ts:17 + types.ts:334 (RetryBackoff) — DEAD CONFIG, never read";
|
|
116
|
+
readonly policy: {
|
|
117
|
+
readonly maxAttempts: 5;
|
|
118
|
+
readonly baseDelayMs: 100;
|
|
119
|
+
readonly multiplier: 2;
|
|
120
|
+
readonly maxDelayMs: 30000;
|
|
121
|
+
readonly growth: "exponential";
|
|
122
|
+
readonly jitter: {
|
|
123
|
+
readonly kind: "proportional";
|
|
124
|
+
readonly pct: 0.25;
|
|
125
|
+
};
|
|
126
|
+
readonly allowJitterAboveCap: false;
|
|
127
|
+
};
|
|
128
|
+
readonly retryable: "n/a — nothing reads this object";
|
|
129
|
+
readonly migration: "DELETE, do not migrate. `retryBackoff` is constructed in two places and read in ZERO (`git grep retryBackoff -- ts/` → 3 hits, all declarations). Migrating it would ACTIVATE a delay that has never run in production. Either wire it deliberately as a separate change, or drop the field.";
|
|
130
|
+
};
|
|
131
|
+
readonly "replicaConsumer.applyRetry": {
|
|
132
|
+
readonly source: "ts/replica-consumer/src/worker/streams-worker.ts:1120 (attempt >= replica.retryMaxAttempts)";
|
|
133
|
+
readonly policy: {
|
|
134
|
+
readonly maxAttempts: 5;
|
|
135
|
+
readonly baseDelayMs: 0;
|
|
136
|
+
readonly multiplier: 1;
|
|
137
|
+
readonly maxDelayMs: 0;
|
|
138
|
+
readonly growth: "exponential";
|
|
139
|
+
readonly jitter: "none";
|
|
140
|
+
readonly allowJitterAboveCap: false;
|
|
141
|
+
};
|
|
142
|
+
readonly retryable: "all apply errors incl. NoBaseRowForPartial; DLQ at the budget. Excluded: UnknownAggregateKind (process-fatal) and trimmed phantom entries (tombstoned).";
|
|
143
|
+
readonly migration: "Counting only. The DELAY between attempts is Redis PEL redelivery + the XCLAIM reaper, not a sleep — baseDelayMs 0 records that honestly. Migrate `isFinalAttempt(attempt, maxAttempts)` for the boundary test and leave the redelivery timing alone.";
|
|
144
|
+
};
|
|
145
|
+
readonly "roleCatalog.publishAtBoot": {
|
|
146
|
+
readonly source: "ts/role-catalog/src/publish.ts:203 (computeBackoff)";
|
|
147
|
+
readonly policy: {
|
|
148
|
+
readonly maxAttempts: 30;
|
|
149
|
+
readonly baseDelayMs: 100;
|
|
150
|
+
readonly multiplier: 2;
|
|
151
|
+
readonly maxDelayMs: 30000;
|
|
152
|
+
readonly growth: "exponential";
|
|
153
|
+
readonly jitter: {
|
|
154
|
+
readonly kind: "proportional";
|
|
155
|
+
readonly pct: 0.125;
|
|
156
|
+
};
|
|
157
|
+
readonly allowJitterAboveCap: true;
|
|
158
|
+
};
|
|
159
|
+
readonly retryable: "gRPC UNAVAILABLE only. FAILED_PRECONDITION → fail-fast; every other error → rethrow immediately.";
|
|
160
|
+
readonly migration: "Byte-preserving up to integer rounding: `randomFn()*0.25 + 0.875` is exactly `1 + 0.125·(2r−1)`, so the same RNG draw yields the same multiplier. This package rounds to whole ms (setTimeout truncates anyway). This is the ONLY surveyed site that already injects `sleepFn` + `randomFn`, i.e. the only one that was deterministically testable before this package existed.";
|
|
161
|
+
};
|
|
162
|
+
readonly "saga.participantWorkerLoop": {
|
|
163
|
+
readonly source: "ts/saga/src/participant-worker.ts:351 (sleep(250) on loop error)";
|
|
164
|
+
readonly policy: {
|
|
165
|
+
readonly maxAttempts: 0;
|
|
166
|
+
readonly baseDelayMs: 250;
|
|
167
|
+
readonly multiplier: 1;
|
|
168
|
+
readonly maxDelayMs: 250;
|
|
169
|
+
readonly growth: "exponential";
|
|
170
|
+
readonly jitter: "none";
|
|
171
|
+
readonly allowJitterAboveCap: false;
|
|
172
|
+
};
|
|
173
|
+
readonly retryable: "any XREADGROUP / handleMessage throw; the loop never gives up";
|
|
174
|
+
readonly migration: "Behaviour-preserving. 250ms here vs 200ms in the two sibling loops is the clearest ACCIDENTAL divergence in the survey — three loops, same failure mode, three different constants.";
|
|
175
|
+
};
|
|
176
|
+
readonly "telemetry.streamsWorkerRetry": {
|
|
177
|
+
readonly source: "ts/telemetry/src/worker/streams-worker.ts:776 (attempt < retryMaxAttempts) + DEFAULT_RETRY_MAX";
|
|
178
|
+
readonly policy: {
|
|
179
|
+
readonly maxAttempts: 3;
|
|
180
|
+
readonly baseDelayMs: 0;
|
|
181
|
+
readonly multiplier: 1;
|
|
182
|
+
readonly maxDelayMs: 0;
|
|
183
|
+
readonly growth: "exponential";
|
|
184
|
+
readonly jitter: "none";
|
|
185
|
+
readonly allowJitterAboveCap: false;
|
|
186
|
+
};
|
|
187
|
+
readonly retryable: "poison-tagged apply errors only; infra errors (fence lost, store down) leave the entry in the PEL WITHOUT bumping the ledger";
|
|
188
|
+
readonly migration: "Counting only, like replicaConsumer.applyRetry. `attempt < max` (here) and `attempt >= max` (replica) are complementary forms of the SAME 1-based convention — both dead-letter on attempt N. Migrating both to isFinalAttempt() removes the need to prove that each time.";
|
|
189
|
+
};
|
|
190
|
+
readonly "telemetry.outboxDispatcherLoop": {
|
|
191
|
+
readonly source: "ts/telemetry/src/worker/outbox-dispatcher.ts:262 (setTimeout(min(pollMs, 200)))";
|
|
192
|
+
readonly policy: {
|
|
193
|
+
readonly maxAttempts: 0;
|
|
194
|
+
readonly baseDelayMs: 200;
|
|
195
|
+
readonly multiplier: 1;
|
|
196
|
+
readonly maxDelayMs: 200;
|
|
197
|
+
readonly growth: "exponential";
|
|
198
|
+
readonly jitter: "none";
|
|
199
|
+
readonly allowJitterAboveCap: false;
|
|
200
|
+
};
|
|
201
|
+
readonly retryable: "any drain-iteration throw";
|
|
202
|
+
readonly migration: "Behaviour-preserving. Duplicate of outboxDispatcher.drainLoop.";
|
|
203
|
+
};
|
|
204
|
+
readonly "telemetry.singleActiveLeaseSelfHeal": {
|
|
205
|
+
readonly source: "ts/telemetry/src/worker/single-active-lease.ts:714 (retryMs = max(1000, ttlMs/2))";
|
|
206
|
+
readonly policy: {
|
|
207
|
+
readonly maxAttempts: 0;
|
|
208
|
+
readonly baseDelayMs: 15000;
|
|
209
|
+
readonly multiplier: 1;
|
|
210
|
+
readonly maxDelayMs: 15000;
|
|
211
|
+
readonly growth: "exponential";
|
|
212
|
+
readonly jitter: "none";
|
|
213
|
+
readonly allowJitterAboveCap: false;
|
|
214
|
+
};
|
|
215
|
+
readonly retryable: "lease not held / Redis down — retries on a fixed tick forever";
|
|
216
|
+
readonly migration: "DELIBERATE CHANGE recommended: add equal jitter. This is the one surveyed site with a genuine thundering-herd shape — every replica of a service self-heals on the SAME ttl/2 tick with zero jitter, so after a Redis blip they all hammer the acquire path in lockstep. Preserve the ttl/2 base; jitter the tick.";
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
export type FleetSiteId = keyof typeof FLEET_POLICIES;
|
|
220
|
+
//# sourceMappingURL=fleet-policies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-policies.d.ts","sourceRoot":"","sources":["../src/fleet-policies.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,SAAS;IACxB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAID,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsPmB,CAAC;AAE/C,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The survey, as executable constants.
|
|
2
|
+
//
|
|
3
|
+
// Every entry below is the REAL parameter set of a hand-rolled backoff found
|
|
4
|
+
// in ts/* on origin/main at 8af8e137 — not a suggestion. `tests/fleet-parity.
|
|
5
|
+
// test.ts` asserts this package reproduces each site's observed delay sequence,
|
|
6
|
+
// which is what makes "the union of what they actually do is the requirements
|
|
7
|
+
// document" a check rather than a claim.
|
|
8
|
+
//
|
|
9
|
+
// These are also the drop-in targets for the sequenced migration PR: a call
|
|
10
|
+
// site migrates by passing `FLEET_POLICIES["<id>"]` and deleting its local
|
|
11
|
+
// math. Each entry's `migration` note says whether that is behaviour-preserving
|
|
12
|
+
// or a deliberate change.
|
|
13
|
+
const UNCAPPED = Number.POSITIVE_INFINITY;
|
|
14
|
+
export const FLEET_POLICIES = {
|
|
15
|
+
// ── 1 ──────────────────────────────────────────────────────────────────
|
|
16
|
+
"approval.consumer": {
|
|
17
|
+
source: "ts/approval/src/consumer.ts:161 (BullMQ settings.backoffStrategy)",
|
|
18
|
+
policy: {
|
|
19
|
+
maxAttempts: 5, // DEFAULT_CONSUMER_MAX_RETRIES (ts/approval/src/types.ts:628)
|
|
20
|
+
baseDelayMs: 250,
|
|
21
|
+
multiplier: 2,
|
|
22
|
+
maxDelayMs: 60_000,
|
|
23
|
+
growth: "exponential",
|
|
24
|
+
jitter: "none",
|
|
25
|
+
allowJitterAboveCap: false,
|
|
26
|
+
},
|
|
27
|
+
retryable: "anything the handler throws; terminal at attemptsMade+1 >= consumerMaxRetries (consumer.ts:621 — the post-increment test, correct)",
|
|
28
|
+
migration: "bullmqBackoffStrategy(FLEET_POLICIES['approval.consumer'].policy, { attemptOffset: 1 }) is byte-preserving. attemptOffset 0 (the correct index) makes the first retry 250ms instead of 500ms — a deliberate change, and the one worth taking, since 250ms is what the code claims its base is.",
|
|
29
|
+
},
|
|
30
|
+
// ── 2 ──────────────────────────────────────────────────────────────────
|
|
31
|
+
"approval.streamsReader": {
|
|
32
|
+
source: "ts/approval/src/streams-reader.ts:389 (loop-error backoff)",
|
|
33
|
+
policy: {
|
|
34
|
+
maxAttempts: 0, // loop retries forever
|
|
35
|
+
baseDelayMs: 200,
|
|
36
|
+
multiplier: 1, // constant
|
|
37
|
+
maxDelayMs: 200,
|
|
38
|
+
growth: "exponential",
|
|
39
|
+
jitter: "none",
|
|
40
|
+
allowJitterAboveCap: false,
|
|
41
|
+
},
|
|
42
|
+
retryable: "any XREADGROUP / handler throw; the loop never gives up",
|
|
43
|
+
migration: "Behaviour-preserving as written. DELIBERATE CHANGE worth making: multiplier 2 + cap 30s + equal jitter, so a long Redis outage stops issuing 5 commands/second per pod.",
|
|
44
|
+
},
|
|
45
|
+
// ── 3 + 4 ──────────────────────────────────────────────────────────────
|
|
46
|
+
"onboarding.submitLead": {
|
|
47
|
+
source: "ts/onboarding/src/submit/client.ts:37 (MAX_ATTEMPTS/RETRY_DELAYS_MS)",
|
|
48
|
+
policy: {
|
|
49
|
+
maxAttempts: 3,
|
|
50
|
+
baseDelayMs: 1000,
|
|
51
|
+
multiplier: 2,
|
|
52
|
+
maxDelayMs: UNCAPPED, // the 2-entry table never reaches a cap
|
|
53
|
+
growth: "exponential",
|
|
54
|
+
jitter: "none",
|
|
55
|
+
allowJitterAboveCap: false,
|
|
56
|
+
},
|
|
57
|
+
retryable: "network error + request timeout + HTTP 5xx. NOT: 403, 409, 422, 429 (429 is caller-decides, deliberately not auto-retried)",
|
|
58
|
+
migration: "Behaviour-preserving: the [1000, 2000] literal table IS base 1000 × 2^(n-1) for n = 1,2. Retryability moves into a shouldRetry predicate; the 403/409/422 paths become NonRetryableError throws.",
|
|
59
|
+
},
|
|
60
|
+
"onboarding.submitSignup": {
|
|
61
|
+
source: "ts/onboarding/src/submit/signup-client.ts:44 (verbatim copy of the above)",
|
|
62
|
+
policy: {
|
|
63
|
+
maxAttempts: 3,
|
|
64
|
+
baseDelayMs: 1000,
|
|
65
|
+
multiplier: 2,
|
|
66
|
+
maxDelayMs: UNCAPPED,
|
|
67
|
+
growth: "exponential",
|
|
68
|
+
jitter: "none",
|
|
69
|
+
allowJitterAboveCap: false,
|
|
70
|
+
},
|
|
71
|
+
retryable: "identical to onboarding.submitLead",
|
|
72
|
+
migration: "Behaviour-preserving. This site and submitLead are a literal duplicate — one shared policy constant removes the copy.",
|
|
73
|
+
},
|
|
74
|
+
// ── 5 ──────────────────────────────────────────────────────────────────
|
|
75
|
+
"outboxDispatcher.claimCooldown": {
|
|
76
|
+
source: "ts/outbox-dispatcher/src/sql-builders.ts:39 (`last_attempt_at < now() - interval '1 minute' * publish_attempts`) + define-dispatcher.ts:18 (DEFAULT_PUBLISH_MAX_ATTEMPTS)",
|
|
77
|
+
policy: {
|
|
78
|
+
maxAttempts: 8,
|
|
79
|
+
baseDelayMs: 60_000,
|
|
80
|
+
multiplier: 1, // unused by linear growth
|
|
81
|
+
maxDelayMs: UNCAPPED, // 8 attempts → 8 min max cooldown; no explicit cap
|
|
82
|
+
growth: "linear", // 1, 2, 3, … minutes — the reason `growth` exists
|
|
83
|
+
jitter: "none",
|
|
84
|
+
allowJitterAboveCap: false,
|
|
85
|
+
},
|
|
86
|
+
retryable: "every publish failure. D401: the row is NEVER deleted and never DLQ'd; maxAttempts caps RE-CLAIM only, and the 24h sweep raises the M17 page.",
|
|
87
|
+
migration: "NOT a drop-in: the delay is enforced by a SQL predicate, not a sleep. The package's role here is to GENERATE the interval expression / validate the constants, so the TS, Python and Go ports of the dispatcher cannot drift apart. Behaviour-preserving if the SQL keeps `interval '1 minute' * publish_attempts`.",
|
|
88
|
+
},
|
|
89
|
+
// ── 6 ──────────────────────────────────────────────────────────────────
|
|
90
|
+
"outboxDispatcher.drainLoop": {
|
|
91
|
+
source: "ts/outbox-dispatcher/src/drainer.ts:421 (`sleep(Math.min(pollMs, 200))` on error)",
|
|
92
|
+
policy: {
|
|
93
|
+
maxAttempts: 0,
|
|
94
|
+
baseDelayMs: 200,
|
|
95
|
+
multiplier: 1,
|
|
96
|
+
maxDelayMs: 200,
|
|
97
|
+
growth: "exponential",
|
|
98
|
+
jitter: "none",
|
|
99
|
+
allowJitterAboveCap: false,
|
|
100
|
+
},
|
|
101
|
+
retryable: "any drain-iteration throw; the loop never gives up",
|
|
102
|
+
migration: "Behaviour-preserving. Same accidental-divergence class as approval.streamsReader and telemetry.outboxDispatcherLoop — three fixed error sleeps, three different rationales, no jitter anywhere.",
|
|
103
|
+
},
|
|
104
|
+
// ── 7 ──────────────────────────────────────────────────────────────────
|
|
105
|
+
"replicaConsumer.coldStart": {
|
|
106
|
+
source: "ts/replica-consumer/src/start.ts:242 (coldStartBackoffMs) + DEFAULT_COLD_START_RETRY",
|
|
107
|
+
policy: {
|
|
108
|
+
maxAttempts: 0, // indefinite — dropping a backfill silently is worse
|
|
109
|
+
baseDelayMs: 1000,
|
|
110
|
+
multiplier: 2,
|
|
111
|
+
maxDelayMs: 30_000,
|
|
112
|
+
growth: "exponential",
|
|
113
|
+
jitter: { kind: "proportional", pct: 0.2 },
|
|
114
|
+
// The live code jitters AFTER the cap and never re-clamps, so it can
|
|
115
|
+
// sleep 36s against a declared 30s cap. `true` preserves that.
|
|
116
|
+
allowJitterAboveCap: true,
|
|
117
|
+
},
|
|
118
|
+
retryable: "every bootstrap failure (fail-soft: the process stays alive)",
|
|
119
|
+
migration: "Byte-preserving with allowJitterAboveCap: true — the jitter formula `capped·(1 + 0.2·(2r−1))` is identical to this package's proportional strategy for the same RNG draw. Flipping it to false is a DELIBERATE change that makes the declared 30s cap actually hold (max delay 36s → 30s).",
|
|
120
|
+
},
|
|
121
|
+
// ── 8 ──────────────────────────────────────────────────────────────────
|
|
122
|
+
"replicaConsumer.declaredApplyBackoff": {
|
|
123
|
+
source: "ts/replica-consumer/src/define-replica.ts:294 + init.ts:17 + types.ts:334 (RetryBackoff) — DEAD CONFIG, never read",
|
|
124
|
+
policy: {
|
|
125
|
+
maxAttempts: 5,
|
|
126
|
+
baseDelayMs: 100,
|
|
127
|
+
multiplier: 2,
|
|
128
|
+
maxDelayMs: 30_000,
|
|
129
|
+
growth: "exponential",
|
|
130
|
+
// `jitterPct: 25` in the source. Read as a FRACTION (the unit its
|
|
131
|
+
// sibling start.ts uses) that is 2500%; read as a percent it is 0.25.
|
|
132
|
+
// Encoded as 0.25 here because that is the only reading that is not
|
|
133
|
+
// absurd — but the field is ambiguous BY CONSTRUCTION and this package
|
|
134
|
+
// rejects a pct > 1 outright so the ambiguity cannot survive migration.
|
|
135
|
+
jitter: { kind: "proportional", pct: 0.25 },
|
|
136
|
+
allowJitterAboveCap: false,
|
|
137
|
+
},
|
|
138
|
+
retryable: "n/a — nothing reads this object",
|
|
139
|
+
migration: "DELETE, do not migrate. `retryBackoff` is constructed in two places and read in ZERO (`git grep retryBackoff -- ts/` → 3 hits, all declarations). Migrating it would ACTIVATE a delay that has never run in production. Either wire it deliberately as a separate change, or drop the field.",
|
|
140
|
+
},
|
|
141
|
+
// ── 9 ──────────────────────────────────────────────────────────────────
|
|
142
|
+
"replicaConsumer.applyRetry": {
|
|
143
|
+
source: "ts/replica-consumer/src/worker/streams-worker.ts:1120 (attempt >= replica.retryMaxAttempts)",
|
|
144
|
+
policy: {
|
|
145
|
+
maxAttempts: 5,
|
|
146
|
+
baseDelayMs: 0, // no library-controlled delay — see below
|
|
147
|
+
multiplier: 1,
|
|
148
|
+
maxDelayMs: 0,
|
|
149
|
+
growth: "exponential",
|
|
150
|
+
jitter: "none",
|
|
151
|
+
allowJitterAboveCap: false,
|
|
152
|
+
},
|
|
153
|
+
retryable: "all apply errors incl. NoBaseRowForPartial; DLQ at the budget. Excluded: UnknownAggregateKind (process-fatal) and trimmed phantom entries (tombstoned).",
|
|
154
|
+
migration: "Counting only. The DELAY between attempts is Redis PEL redelivery + the XCLAIM reaper, not a sleep — baseDelayMs 0 records that honestly. Migrate `isFinalAttempt(attempt, maxAttempts)` for the boundary test and leave the redelivery timing alone.",
|
|
155
|
+
},
|
|
156
|
+
// ── 10 ─────────────────────────────────────────────────────────────────
|
|
157
|
+
"roleCatalog.publishAtBoot": {
|
|
158
|
+
source: "ts/role-catalog/src/publish.ts:203 (computeBackoff)",
|
|
159
|
+
policy: {
|
|
160
|
+
maxAttempts: 30, // ~5 min worst case
|
|
161
|
+
baseDelayMs: 100,
|
|
162
|
+
multiplier: 2,
|
|
163
|
+
maxDelayMs: 30_000,
|
|
164
|
+
growth: "exponential",
|
|
165
|
+
jitter: { kind: "proportional", pct: 0.125 }, // randomFn()*0.25 + 0.875
|
|
166
|
+
allowJitterAboveCap: true, // jitter applied after the cap, never re-clamped
|
|
167
|
+
},
|
|
168
|
+
retryable: "gRPC UNAVAILABLE only. FAILED_PRECONDITION → fail-fast; every other error → rethrow immediately.",
|
|
169
|
+
migration: "Byte-preserving up to integer rounding: `randomFn()*0.25 + 0.875` is exactly `1 + 0.125·(2r−1)`, so the same RNG draw yields the same multiplier. This package rounds to whole ms (setTimeout truncates anyway). This is the ONLY surveyed site that already injects `sleepFn` + `randomFn`, i.e. the only one that was deterministically testable before this package existed.",
|
|
170
|
+
},
|
|
171
|
+
// ── 11 ─────────────────────────────────────────────────────────────────
|
|
172
|
+
"saga.participantWorkerLoop": {
|
|
173
|
+
source: "ts/saga/src/participant-worker.ts:351 (sleep(250) on loop error)",
|
|
174
|
+
policy: {
|
|
175
|
+
maxAttempts: 0,
|
|
176
|
+
baseDelayMs: 250,
|
|
177
|
+
multiplier: 1,
|
|
178
|
+
maxDelayMs: 250,
|
|
179
|
+
growth: "exponential",
|
|
180
|
+
jitter: "none",
|
|
181
|
+
allowJitterAboveCap: false,
|
|
182
|
+
},
|
|
183
|
+
retryable: "any XREADGROUP / handleMessage throw; the loop never gives up",
|
|
184
|
+
migration: "Behaviour-preserving. 250ms here vs 200ms in the two sibling loops is the clearest ACCIDENTAL divergence in the survey — three loops, same failure mode, three different constants.",
|
|
185
|
+
},
|
|
186
|
+
// ── 12 ─────────────────────────────────────────────────────────────────
|
|
187
|
+
"telemetry.streamsWorkerRetry": {
|
|
188
|
+
source: "ts/telemetry/src/worker/streams-worker.ts:776 (attempt < retryMaxAttempts) + DEFAULT_RETRY_MAX",
|
|
189
|
+
policy: {
|
|
190
|
+
maxAttempts: 3,
|
|
191
|
+
baseDelayMs: 0, // PEL redelivery, not a sleep
|
|
192
|
+
multiplier: 1,
|
|
193
|
+
maxDelayMs: 0,
|
|
194
|
+
growth: "exponential",
|
|
195
|
+
jitter: "none",
|
|
196
|
+
allowJitterAboveCap: false,
|
|
197
|
+
},
|
|
198
|
+
retryable: "poison-tagged apply errors only; infra errors (fence lost, store down) leave the entry in the PEL WITHOUT bumping the ledger",
|
|
199
|
+
migration: "Counting only, like replicaConsumer.applyRetry. `attempt < max` (here) and `attempt >= max` (replica) are complementary forms of the SAME 1-based convention — both dead-letter on attempt N. Migrating both to isFinalAttempt() removes the need to prove that each time.",
|
|
200
|
+
},
|
|
201
|
+
// ── 13 ─────────────────────────────────────────────────────────────────
|
|
202
|
+
"telemetry.outboxDispatcherLoop": {
|
|
203
|
+
source: "ts/telemetry/src/worker/outbox-dispatcher.ts:262 (setTimeout(min(pollMs, 200)))",
|
|
204
|
+
policy: {
|
|
205
|
+
maxAttempts: 0,
|
|
206
|
+
baseDelayMs: 200,
|
|
207
|
+
multiplier: 1,
|
|
208
|
+
maxDelayMs: 200,
|
|
209
|
+
growth: "exponential",
|
|
210
|
+
jitter: "none",
|
|
211
|
+
allowJitterAboveCap: false,
|
|
212
|
+
},
|
|
213
|
+
retryable: "any drain-iteration throw",
|
|
214
|
+
migration: "Behaviour-preserving. Duplicate of outboxDispatcher.drainLoop.",
|
|
215
|
+
},
|
|
216
|
+
// ── 14 ─────────────────────────────────────────────────────────────────
|
|
217
|
+
"telemetry.singleActiveLeaseSelfHeal": {
|
|
218
|
+
source: "ts/telemetry/src/worker/single-active-lease.ts:714 (retryMs = max(1000, ttlMs/2))",
|
|
219
|
+
policy: {
|
|
220
|
+
maxAttempts: 0,
|
|
221
|
+
baseDelayMs: 15_000, // max(1000, 30_000/2) for the default 30s TTL
|
|
222
|
+
multiplier: 1,
|
|
223
|
+
maxDelayMs: 15_000,
|
|
224
|
+
growth: "exponential",
|
|
225
|
+
jitter: "none",
|
|
226
|
+
allowJitterAboveCap: false,
|
|
227
|
+
},
|
|
228
|
+
retryable: "lease not held / Redis down — retries on a fixed tick forever",
|
|
229
|
+
migration: "DELIBERATE CHANGE recommended: add equal jitter. This is the one surveyed site with a genuine thundering-herd shape — every replica of a service self-heals on the SAME ttl/2 tick with zero jitter, so after a Redis blip they all hammer the acquire path in lockstep. Preserve the ttl/2 base; jitter the tick.",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
//# sourceMappingURL=fleet-policies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-policies.js","sourceRoot":"","sources":["../src/fleet-policies.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,yCAAyC;AACzC,EAAE;AACF,4EAA4E;AAC5E,2EAA2E;AAC3E,gFAAgF;AAChF,0BAA0B;AAe1B,MAAM,QAAQ,GAAG,MAAM,CAAC,iBAAiB,CAAC;AAE1C,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,0EAA0E;IAC1E,mBAAmB,EAAE;QACnB,MAAM,EAAE,mEAAmE;QAC3E,MAAM,EAAE;YACN,WAAW,EAAE,CAAC,EAAE,8DAA8D;YAC9E,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EACP,oIAAoI;QACtI,SAAS,EACP,gSAAgS;KACnS;IACD,0EAA0E;IAC1E,wBAAwB,EAAE;QACxB,MAAM,EAAE,4DAA4D;QACpE,MAAM,EAAE;YACN,WAAW,EAAE,CAAC,EAAE,uBAAuB;YACvC,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC,EAAE,WAAW;YAC1B,UAAU,EAAE,GAAG;YACf,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,yDAAyD;QACpE,SAAS,EACP,yKAAyK;KAC5K;IACD,0EAA0E;IAC1E,uBAAuB,EAAE;QACvB,MAAM,EACJ,sEAAsE;QACxE,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,IAAI;YACjB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,QAAQ,EAAE,wCAAwC;YAC9D,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EACP,4HAA4H;QAC9H,SAAS,EACP,kMAAkM;KACrM;IACD,yBAAyB,EAAE;QACzB,MAAM,EACJ,2EAA2E;QAC7E,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,IAAI;YACjB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,QAAQ;YACpB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,oCAAoC;QAC/C,SAAS,EACP,uHAAuH;KAC1H;IACD,0EAA0E;IAC1E,gCAAgC,EAAE;QAChC,MAAM,EACJ,2KAA2K;QAC7K,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,MAAM;YACnB,UAAU,EAAE,CAAC,EAAE,0BAA0B;YACzC,UAAU,EAAE,QAAQ,EAAE,mDAAmD;YACzE,MAAM,EAAE,QAAQ,EAAE,kDAAkD;YACpE,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EACP,+IAA+I;QACjJ,SAAS,EACP,qTAAqT;KACxT;IACD,0EAA0E;IAC1E,4BAA4B,EAAE;QAC5B,MAAM,EACJ,mFAAmF;QACrF,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,GAAG;YACf,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,oDAAoD;QAC/D,SAAS,EACP,iMAAiM;KACpM;IACD,0EAA0E;IAC1E,2BAA2B,EAAE;QAC3B,MAAM,EACJ,sFAAsF;QACxF,MAAM,EAAE;YACN,WAAW,EAAE,CAAC,EAAE,qDAAqD;YACrE,WAAW,EAAE,IAAI;YACjB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,EAAE;YAC1C,qEAAqE;YACrE,+DAA+D;YAC/D,mBAAmB,EAAE,IAAI;SAC1B;QACD,SAAS,EAAE,8DAA8D;QACzE,SAAS,EACP,4RAA4R;KAC/R;IACD,0EAA0E;IAC1E,sCAAsC,EAAE;QACtC,MAAM,EACJ,oHAAoH;QACtH,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,aAAa;YACrB,kEAAkE;YAClE,sEAAsE;YACtE,oEAAoE;YACpE,uEAAuE;YACvE,wEAAwE;YACxE,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,IAAI,EAAE;YAC3C,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,iCAAiC;QAC5C,SAAS,EACP,8RAA8R;KACjS;IACD,0EAA0E;IAC1E,4BAA4B,EAAE;QAC5B,MAAM,EACJ,6FAA6F;QAC/F,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,CAAC,EAAE,0CAA0C;YAC1D,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EACP,yJAAyJ;QAC3J,SAAS,EACP,uPAAuP;KAC1P;IACD,0EAA0E;IAC1E,2BAA2B,EAAE;QAC3B,MAAM,EAAE,qDAAqD;QAC7D,MAAM,EAAE;YACN,WAAW,EAAE,EAAE,EAAE,oBAAoB;YACrC,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,0BAA0B;YACxE,mBAAmB,EAAE,IAAI,EAAE,iDAAiD;SAC7E;QACD,SAAS,EACP,kGAAkG;QACpG,SAAS,EACP,iXAAiX;KACpX;IACD,0EAA0E;IAC1E,4BAA4B,EAAE;QAC5B,MAAM,EAAE,kEAAkE;QAC1E,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,GAAG;YACf,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,+DAA+D;QAC1E,SAAS,EACP,qLAAqL;KACxL;IACD,0EAA0E;IAC1E,8BAA8B,EAAE;QAC9B,MAAM,EACJ,gGAAgG;QAClG,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,CAAC,EAAE,8BAA8B;YAC9C,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EACP,8HAA8H;QAChI,SAAS,EACP,4QAA4Q;KAC/Q;IACD,0EAA0E;IAC1E,gCAAgC,EAAE;QAChC,MAAM,EACJ,iFAAiF;QACnF,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,GAAG;YACf,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,2BAA2B;QACtC,SAAS,EAAE,gEAAgE;KAC5E;IACD,0EAA0E;IAC1E,qCAAqC,EAAE;QACrC,MAAM,EACJ,mFAAmF;QACrF,MAAM,EAAE;YACN,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,MAAM,EAAE,8CAA8C;YACnE,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,MAAM;YACd,mBAAmB,EAAE,KAAK;SAC3B;QACD,SAAS,EAAE,+DAA+D;QAC1E,SAAS,EACP,oTAAoT;KACvT;CAC2C,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { type GrowthKind, type JitterStrategy, type RetryPolicy, DEFAULT_POLICY, InvalidRetryPolicyError, resolvePolicy, baseDelayForAttempt, } from "./policy.js";
|
|
2
|
+
export { type RandomFn, applyJitter, jitteredDelayForAttempt, backoffSequence, } from "./jitter.js";
|
|
3
|
+
export { isFinalAttempt, isFinalAttemptFromAttemptsMade, remainingAttempts, } from "./attempts.js";
|
|
4
|
+
export { type ShouldRetry, type ShouldRetryContext, retryAll, retryNone, allOf, anyOf, shouldRetryFromOutcomeClassifier, } from "./classify.js";
|
|
5
|
+
export { NonRetryableError, RetryExhaustedError, isNonRetryableError, } from "./errors.js";
|
|
6
|
+
export { type AttemptContext, type ExhaustedEvent, type RetryEvent, type RetryOptions, type SleepFn, defaultSleep, retry, } from "./run.js";
|
|
7
|
+
export { type BullmqBackoffOptions, bullmqBackoffStrategy, } from "./adapters/bullmq.js";
|
|
8
|
+
export { FLEET_POLICIES, type FleetSiteId } from "./fleet-policies.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,cAAc,EACd,uBAAuB,EACvB,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,KAAK,QAAQ,EACb,WAAW,EACX,uBAAuB,EACvB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,cAAc,EACd,8BAA8B,EAC9B,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,QAAQ,EACR,SAAS,EACT,KAAK,EACL,KAAK,EACL,gCAAgC,GACjC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,YAAY,EACZ,KAAK,GACN,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,KAAK,oBAAoB,EACzB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @nodii/retry — the one retry / backoff / jitter policy for the Nodii stack.
|
|
2
|
+
//
|
|
3
|
+
// Built because the audit found retry+backoff+jitter hand-rolled in 14 places
|
|
4
|
+
// INSIDE the shared libraries, so every service inherited whichever variant
|
|
5
|
+
// its lib happened to implement. See README § "The survey" for the table and
|
|
6
|
+
// § "Divergences" for which of those disagreements look deliberate.
|
|
7
|
+
export { DEFAULT_POLICY, InvalidRetryPolicyError, resolvePolicy, baseDelayForAttempt, } from "./policy.js";
|
|
8
|
+
export { applyJitter, jitteredDelayForAttempt, backoffSequence, } from "./jitter.js";
|
|
9
|
+
export { isFinalAttempt, isFinalAttemptFromAttemptsMade, remainingAttempts, } from "./attempts.js";
|
|
10
|
+
export { retryAll, retryNone, allOf, anyOf, shouldRetryFromOutcomeClassifier, } from "./classify.js";
|
|
11
|
+
export { NonRetryableError, RetryExhaustedError, isNonRetryableError, } from "./errors.js";
|
|
12
|
+
export { defaultSleep, retry, } from "./run.js";
|
|
13
|
+
export { bullmqBackoffStrategy, } from "./adapters/bullmq.js";
|
|
14
|
+
export { FLEET_POLICIES } from "./fleet-policies.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,oEAAoE;AAEpE,OAAO,EAIL,cAAc,EACd,uBAAuB,EACvB,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAEL,WAAW,EACX,uBAAuB,EACvB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,cAAc,EACd,8BAA8B,EAC9B,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAGL,QAAQ,EACR,SAAS,EACT,KAAK,EACL,KAAK,EACL,gCAAgC,GACjC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAML,YAAY,EACZ,KAAK,GACN,MAAM,UAAU,CAAC;AAElB,OAAO,EAEL,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,cAAc,EAAoB,MAAM,qBAAqB,CAAC"}
|
package/dist/jitter.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type JitterStrategy, type RetryPolicy } from "./policy.js";
|
|
2
|
+
/** Injectable RNG. Tests pass a deterministic sequence; production omits it. */
|
|
3
|
+
export type RandomFn = () => number;
|
|
4
|
+
/**
|
|
5
|
+
* Apply `strategy` to an already-capped delay `d`.
|
|
6
|
+
*
|
|
7
|
+
* `previousDelayMs` is only consulted by `"decorrelated"` (which is a
|
|
8
|
+
* recurrence, not a function of the attempt index). Callers that do not track
|
|
9
|
+
* it get the seeded form (`previous = base`).
|
|
10
|
+
*
|
|
11
|
+
* The result is NOT clamped here — {@link jitteredDelayForAttempt} owns the
|
|
12
|
+
* final clamp so `allowJitterAboveCap` has exactly one implementation.
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyJitter(d: number, strategy: JitterStrategy, random: RandomFn, ctx: {
|
|
15
|
+
baseDelayMs: number;
|
|
16
|
+
maxDelayMs: number;
|
|
17
|
+
previousDelayMs: number;
|
|
18
|
+
}): number;
|
|
19
|
+
/**
|
|
20
|
+
* The FINAL delay (ms, integer) for a 1-based `attempt`.
|
|
21
|
+
*
|
|
22
|
+
* Order of operations — this is the part the fleet gets wrong:
|
|
23
|
+
*
|
|
24
|
+
* 1. grow → base · mult^(n-1) (or base · n for linear)
|
|
25
|
+
* 2. cap → min(·, maxDelayMs)
|
|
26
|
+
* 3. jitter → strategy(·)
|
|
27
|
+
* 4. clamp → min(·, maxDelayMs) unless `allowJitterAboveCap`
|
|
28
|
+
* 5. floor at 0 + round to integer ms
|
|
29
|
+
*
|
|
30
|
+
* Step 4 is the fix. `@nodii/role-catalog` and `@nodii/replica-consumer` both
|
|
31
|
+
* stop after step 3, so their declared 30s cap is really a 33.75s / 36s cap.
|
|
32
|
+
*/
|
|
33
|
+
export declare function jitteredDelayForAttempt(policy: RetryPolicy, attempt: number, opts: {
|
|
34
|
+
random: RandomFn;
|
|
35
|
+
previousDelayMs?: number;
|
|
36
|
+
}): number;
|
|
37
|
+
/**
|
|
38
|
+
* The first `count` delays under `policy` — the deterministic preview used by
|
|
39
|
+
* tests and by operators reasoning about "how long until we give up".
|
|
40
|
+
*
|
|
41
|
+
* Threads `previousDelayMs` so `"decorrelated"` produces its true recurrence
|
|
42
|
+
* rather than a seeded-every-time sequence.
|
|
43
|
+
*/
|
|
44
|
+
export declare function backoffSequence(policy: RetryPolicy, count: number, opts?: {
|
|
45
|
+
random?: RandomFn;
|
|
46
|
+
}): number[];
|
|
47
|
+
//# sourceMappingURL=jitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jitter.d.ts","sourceRoot":"","sources":["../src/jitter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GACxE,MAAM,CAwBR;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,MAAM,CAWR;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,QAAQ,CAAA;CAAE,GAC3B,MAAM,EAAE,CAaV"}
|
package/dist/jitter.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { baseDelayForAttempt, } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Apply `strategy` to an already-capped delay `d`.
|
|
4
|
+
*
|
|
5
|
+
* `previousDelayMs` is only consulted by `"decorrelated"` (which is a
|
|
6
|
+
* recurrence, not a function of the attempt index). Callers that do not track
|
|
7
|
+
* it get the seeded form (`previous = base`).
|
|
8
|
+
*
|
|
9
|
+
* The result is NOT clamped here — {@link jitteredDelayForAttempt} owns the
|
|
10
|
+
* final clamp so `allowJitterAboveCap` has exactly one implementation.
|
|
11
|
+
*/
|
|
12
|
+
export function applyJitter(d, strategy, random, ctx) {
|
|
13
|
+
if (typeof strategy === "object") {
|
|
14
|
+
// proportional: d · (1 + pct·U(-1,1))
|
|
15
|
+
return d * (1 + strategy.pct * (random() * 2 - 1));
|
|
16
|
+
}
|
|
17
|
+
switch (strategy) {
|
|
18
|
+
case "none":
|
|
19
|
+
return d;
|
|
20
|
+
case "full":
|
|
21
|
+
// U(0, d)
|
|
22
|
+
return random() * d;
|
|
23
|
+
case "equal":
|
|
24
|
+
// d/2 + U(0, d/2) — a FLOOR of d/2 that still grows, plus dispersal.
|
|
25
|
+
return d / 2 + random() * (d / 2);
|
|
26
|
+
case "decorrelated": {
|
|
27
|
+
// AWS decorrelated: sleep = min(cap, U(base, 3·previous)).
|
|
28
|
+
// Seeded at `base`, so the first delay is U(base, 3·base).
|
|
29
|
+
const lo = ctx.baseDelayMs;
|
|
30
|
+
const hi = ctx.previousDelayMs * 3;
|
|
31
|
+
const span = hi - lo;
|
|
32
|
+
const v = span > 0 ? lo + random() * span : lo;
|
|
33
|
+
return Math.min(ctx.maxDelayMs, v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The FINAL delay (ms, integer) for a 1-based `attempt`.
|
|
39
|
+
*
|
|
40
|
+
* Order of operations — this is the part the fleet gets wrong:
|
|
41
|
+
*
|
|
42
|
+
* 1. grow → base · mult^(n-1) (or base · n for linear)
|
|
43
|
+
* 2. cap → min(·, maxDelayMs)
|
|
44
|
+
* 3. jitter → strategy(·)
|
|
45
|
+
* 4. clamp → min(·, maxDelayMs) unless `allowJitterAboveCap`
|
|
46
|
+
* 5. floor at 0 + round to integer ms
|
|
47
|
+
*
|
|
48
|
+
* Step 4 is the fix. `@nodii/role-catalog` and `@nodii/replica-consumer` both
|
|
49
|
+
* stop after step 3, so their declared 30s cap is really a 33.75s / 36s cap.
|
|
50
|
+
*/
|
|
51
|
+
export function jitteredDelayForAttempt(policy, attempt, opts) {
|
|
52
|
+
const capped = baseDelayForAttempt(policy, attempt);
|
|
53
|
+
const jittered = applyJitter(capped, policy.jitter, opts.random, {
|
|
54
|
+
baseDelayMs: policy.baseDelayMs,
|
|
55
|
+
maxDelayMs: policy.maxDelayMs,
|
|
56
|
+
previousDelayMs: opts.previousDelayMs ?? policy.baseDelayMs,
|
|
57
|
+
});
|
|
58
|
+
const clamped = policy.allowJitterAboveCap
|
|
59
|
+
? jittered
|
|
60
|
+
: Math.min(jittered, policy.maxDelayMs);
|
|
61
|
+
return Math.max(0, Math.round(clamped));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The first `count` delays under `policy` — the deterministic preview used by
|
|
65
|
+
* tests and by operators reasoning about "how long until we give up".
|
|
66
|
+
*
|
|
67
|
+
* Threads `previousDelayMs` so `"decorrelated"` produces its true recurrence
|
|
68
|
+
* rather than a seeded-every-time sequence.
|
|
69
|
+
*/
|
|
70
|
+
export function backoffSequence(policy, count, opts) {
|
|
71
|
+
const random = opts?.random ?? Math.random;
|
|
72
|
+
const out = [];
|
|
73
|
+
let previous = policy.baseDelayMs;
|
|
74
|
+
for (let attempt = 1; attempt <= count; attempt++) {
|
|
75
|
+
const d = jitteredDelayForAttempt(policy, attempt, {
|
|
76
|
+
random,
|
|
77
|
+
previousDelayMs: previous,
|
|
78
|
+
});
|
|
79
|
+
out.push(d);
|
|
80
|
+
previous = d;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=jitter.js.map
|