@nodii/telemetry 0.13.0 → 0.14.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/dist/worker/checkpoint-store.d.ts +66 -0
- package/dist/worker/checkpoint-store.d.ts.map +1 -0
- package/dist/worker/checkpoint-store.js +131 -0
- package/dist/worker/checkpoint-store.js.map +1 -0
- package/dist/worker/dead-letter.d.ts +81 -0
- package/dist/worker/dead-letter.d.ts.map +1 -0
- package/dist/worker/dead-letter.js +165 -0
- package/dist/worker/dead-letter.js.map +1 -0
- package/dist/worker/dedupe-store.d.ts +46 -0
- package/dist/worker/dedupe-store.d.ts.map +1 -0
- package/dist/worker/dedupe-store.js +113 -0
- package/dist/worker/dedupe-store.js.map +1 -0
- package/dist/worker/index.d.ts +23 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +46 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/observability-hooks.d.ts +99 -0
- package/dist/worker/observability-hooks.d.ts.map +1 -0
- package/dist/worker/observability-hooks.js +189 -0
- package/dist/worker/observability-hooks.js.map +1 -0
- package/dist/worker/outbox-dispatcher.d.ts +106 -0
- package/dist/worker/outbox-dispatcher.d.ts.map +1 -0
- package/dist/worker/outbox-dispatcher.js +285 -0
- package/dist/worker/outbox-dispatcher.js.map +1 -0
- package/dist/worker/replication-worker.d.ts +162 -0
- package/dist/worker/replication-worker.d.ts.map +1 -0
- package/dist/worker/replication-worker.js +513 -0
- package/dist/worker/replication-worker.js.map +1 -0
- package/dist/worker/signal-drain.d.ts +83 -0
- package/dist/worker/signal-drain.d.ts.map +1 -0
- package/dist/worker/signal-drain.js +131 -0
- package/dist/worker/signal-drain.js.map +1 -0
- package/dist/worker/single-active-lease.d.ts +180 -0
- package/dist/worker/single-active-lease.d.ts.map +1 -0
- package/dist/worker/single-active-lease.js +394 -0
- package/dist/worker/single-active-lease.js.map +1 -0
- package/dist/worker/streams-worker.d.ts +192 -0
- package/dist/worker/streams-worker.d.ts.map +1 -0
- package/dist/worker/streams-worker.js +488 -0
- package/dist/worker/streams-worker.js.map +1 -0
- package/dist/worker/sweeper-worker.d.ts +107 -0
- package/dist/worker/sweeper-worker.d.ts.map +1 -0
- package/dist/worker/sweeper-worker.js +245 -0
- package/dist/worker/sweeper-worker.js.map +1 -0
- package/dist/worker/tx.d.ts +39 -0
- package/dist/worker/tx.d.ts.map +1 -0
- package/dist/worker/tx.js +51 -0
- package/dist/worker/tx.js.map +1 -0
- package/dist/worker/worker-handle.d.ts +94 -0
- package/dist/worker/worker-handle.d.ts.map +1 -0
- package/dist/worker/worker-handle.js +15 -0
- package/dist/worker/worker-handle.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// D402 — StreamsWorker: the generic (non-replication) Redis-Streams consumer —
|
|
2
|
+
// the canonical Pattern-2 begin/completion-event consumer.
|
|
3
|
+
//
|
|
4
|
+
// D402 (LOCKED): "StreamsWorker — generic (non-replication) stream consumer; the
|
|
5
|
+
// canonical Pattern-2 begin/completion-event consumer." It closes the gap D402
|
|
6
|
+
// names directly: "the Pattern-2 begin/completion-event consumer must be
|
|
7
|
+
// StreamsWorker (today hand-rolled per service via
|
|
8
|
+
// idempotency.wrapConsumerWorker)."
|
|
9
|
+
//
|
|
10
|
+
// ── Composition (which primitives, how) ──────────────────────────────────────
|
|
11
|
+
// - SingleActiveLease — single-active per (service, stream); ttl/2 renew loop
|
|
12
|
+
// runs inside it. `validateFence(token)` guards the apply side-effect: a
|
|
13
|
+
// lease-lost worker mid-apply has its stale write REJECTED (the key test).
|
|
14
|
+
// - DedupeStore — `seen(...)` skips an already-applied (event_id, topic)
|
|
15
|
+
// before any work; `record(...)` runs INSIDE the apply tx so the dedupe row
|
|
16
|
+
// commits together-or-none with the projection + the cursor advance.
|
|
17
|
+
// - CheckpointStore — `advance(...)` runs in the SAME apply tx → HWM cursor
|
|
18
|
+
// never advances past an un-applied event.
|
|
19
|
+
// - ObservabilityHooks — lease/lag/hwm/depth metrics, canonical names.
|
|
20
|
+
// - SignalDrain — `stop({timeoutMs})` is registerable; bounded drain.
|
|
21
|
+
// - DeadLetterSink — INJECTED (no default); a poison event after retries
|
|
22
|
+
// is dead-lettered + XACK'd so the PEL doesn't grow unbounded.
|
|
23
|
+
//
|
|
24
|
+
// ── The consume sequence (per 01-comms § 9.2 / 09-replication § 4) ───────────
|
|
25
|
+
// XREADGROUP > → for each (id, event):
|
|
26
|
+
// [1] dedupe.seen? → XACK + skip (already applied; redelivery is a no-op)
|
|
27
|
+
// [2] apply tx: projection upsert + dedupe.record + checkpoint.advance, with
|
|
28
|
+
// validateFence(token) called BEFORE the side-effect (the fence guard)
|
|
29
|
+
// [3] XACK on success; on error retry-then-dead-letter.
|
|
30
|
+
//
|
|
31
|
+
// The apply itself is the INJECTED handler — the caller owns the projection
|
|
32
|
+
// upsert (this lib doesn't know the consumer's schema). The handler receives the
|
|
33
|
+
// tx executor + the dedupe/checkpoint primitives pre-bound so it runs everything
|
|
34
|
+
// on ONE transaction.
|
|
35
|
+
import { ensurePiiRedaction } from "./dead-letter.js";
|
|
36
|
+
import { ObservabilityHooks } from "./observability-hooks.js";
|
|
37
|
+
import { FenceCheckUnavailableError, FenceLostError, SingleActiveLease, } from "./single-active-lease.js";
|
|
38
|
+
const DEFAULT_BATCH = 32;
|
|
39
|
+
const DEFAULT_BLOCK_MS = 2_000;
|
|
40
|
+
const DEFAULT_RETRY_MAX = 3;
|
|
41
|
+
const DEFAULT_STOP_TIMEOUT_MS = 30_000;
|
|
42
|
+
const DEFAULT_RECLAIM_MIN_IDLE_MS = 30_000;
|
|
43
|
+
/** Consumer-group name — `${service}:${stream}` (canonical per replica-consumer). */
|
|
44
|
+
export function streamsConsumerGroup(service, stream) {
|
|
45
|
+
return `${service}:${stream}`;
|
|
46
|
+
}
|
|
47
|
+
/** Parse an ioredis XREADGROUP response into flat (id, event) pairs. */
|
|
48
|
+
export function parseStreamEntries(raw) {
|
|
49
|
+
if (!Array.isArray(raw))
|
|
50
|
+
return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const streamEntry of raw) {
|
|
53
|
+
if (!Array.isArray(streamEntry) || streamEntry.length < 2)
|
|
54
|
+
continue;
|
|
55
|
+
const entries = streamEntry[1];
|
|
56
|
+
if (!Array.isArray(entries))
|
|
57
|
+
continue;
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
if (!Array.isArray(e) || e.length < 2)
|
|
60
|
+
continue;
|
|
61
|
+
const id = String(e[0]);
|
|
62
|
+
const fields = e[1];
|
|
63
|
+
if (!Array.isArray(fields))
|
|
64
|
+
continue;
|
|
65
|
+
let payload;
|
|
66
|
+
for (let i = 0; i < fields.length - 1; i += 2) {
|
|
67
|
+
if (String(fields[i]) === "event") {
|
|
68
|
+
payload = String(fields[i + 1]);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (payload === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
out.push({ id, event: JSON.parse(payload) });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Malformed payload — skip (the entry stays in the PEL; an operator can
|
|
79
|
+
// inspect it). Never crash the parse loop on one bad entry.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse an ioredis XAUTOCLAIM reply `[cursor, entries, deleted]` into flat
|
|
87
|
+
* (id, event) pairs. `entries` has the same `[[id, [f, v, ...]], ...]` shape as
|
|
88
|
+
* one stream's entries in an XREADGROUP reply, so we wrap it and reuse
|
|
89
|
+
* {@link parseStreamEntries}.
|
|
90
|
+
*/
|
|
91
|
+
export function parseAutoClaimEntries(raw) {
|
|
92
|
+
if (!Array.isArray(raw) || raw.length < 2)
|
|
93
|
+
return [];
|
|
94
|
+
const entries = raw[1];
|
|
95
|
+
if (!Array.isArray(entries))
|
|
96
|
+
return [];
|
|
97
|
+
// Wrap as a single-stream XREADGROUP shape: [[<stream>, <entries>]].
|
|
98
|
+
return parseStreamEntries([["", entries]]);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* StreamsWorker — the generic Pattern-2 single-active stream consumer. Composes
|
|
102
|
+
* lease + dedupe + checkpoint + hooks + drain to consume EXACTLY-ONCE.
|
|
103
|
+
*/
|
|
104
|
+
export class StreamsWorker {
|
|
105
|
+
redis;
|
|
106
|
+
stream;
|
|
107
|
+
apply;
|
|
108
|
+
begin;
|
|
109
|
+
dedupe;
|
|
110
|
+
checkpoint;
|
|
111
|
+
lease;
|
|
112
|
+
subscription;
|
|
113
|
+
deadLetter;
|
|
114
|
+
retryMaxAttempts;
|
|
115
|
+
pollBatch;
|
|
116
|
+
blockMs;
|
|
117
|
+
reclaimMinIdleMs;
|
|
118
|
+
hooks;
|
|
119
|
+
autoStart;
|
|
120
|
+
group;
|
|
121
|
+
consumer;
|
|
122
|
+
retries = new Map();
|
|
123
|
+
_running = false;
|
|
124
|
+
_stopped = false;
|
|
125
|
+
_inFlight = 0;
|
|
126
|
+
_hwm = { lastStreamId: null };
|
|
127
|
+
_lastAppliedAtMs = null;
|
|
128
|
+
groupEnsured = false;
|
|
129
|
+
loop = Promise.resolve();
|
|
130
|
+
constructor(opts) {
|
|
131
|
+
this.redis = opts.redis;
|
|
132
|
+
this.stream = opts.stream;
|
|
133
|
+
this.apply = opts.apply;
|
|
134
|
+
this.begin = opts.begin;
|
|
135
|
+
this.dedupe = opts.dedupe;
|
|
136
|
+
this.checkpoint = opts.checkpoint;
|
|
137
|
+
this.lease = new SingleActiveLease({
|
|
138
|
+
...opts.lease,
|
|
139
|
+
onLeaseLost: () => this.hooks.leaseLost(),
|
|
140
|
+
});
|
|
141
|
+
this.subscription = opts.subscription ?? `streams:${opts.stream}`;
|
|
142
|
+
this.deadLetter = ensurePiiRedaction(opts.deadLetter);
|
|
143
|
+
this.retryMaxAttempts = opts.retryMaxAttempts ?? DEFAULT_RETRY_MAX;
|
|
144
|
+
this.pollBatch = opts.pollBatch ?? DEFAULT_BATCH;
|
|
145
|
+
this.blockMs = opts.blockMs ?? DEFAULT_BLOCK_MS;
|
|
146
|
+
this.reclaimMinIdleMs =
|
|
147
|
+
opts.reclaimMinIdleMs ??
|
|
148
|
+
Math.max(opts.lease.ttlMs, DEFAULT_RECLAIM_MIN_IDLE_MS);
|
|
149
|
+
this.autoStart = opts.autoStart !== false;
|
|
150
|
+
this.hooks =
|
|
151
|
+
opts.hooks ??
|
|
152
|
+
new ObservabilityHooks({
|
|
153
|
+
attributes: {
|
|
154
|
+
service: opts.lease.service,
|
|
155
|
+
scope: opts.lease.scope,
|
|
156
|
+
worker: opts.workerLabel ?? "streams",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
this.group = streamsConsumerGroup(opts.lease.service, opts.stream);
|
|
160
|
+
this.consumer = `${opts.lease.service}:${this.lease.ownerId}`;
|
|
161
|
+
}
|
|
162
|
+
get running() {
|
|
163
|
+
return this._running;
|
|
164
|
+
}
|
|
165
|
+
get leaseAcquired() {
|
|
166
|
+
return this.lease.leaseAcquired;
|
|
167
|
+
}
|
|
168
|
+
get lag() {
|
|
169
|
+
if (this._lastAppliedAtMs === null)
|
|
170
|
+
return null;
|
|
171
|
+
return Math.max(0, (Date.now() - this._lastAppliedAtMs) / 1000);
|
|
172
|
+
}
|
|
173
|
+
get currentHwm() {
|
|
174
|
+
return this._hwm;
|
|
175
|
+
}
|
|
176
|
+
/** Idempotently create the consumer group (swallow BUSYGROUP). */
|
|
177
|
+
async ensureGroup() {
|
|
178
|
+
if (this.groupEnsured)
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
await this.redis.xgroup("CREATE", this.stream, this.group, "$", "MKSTREAM");
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
185
|
+
if (!/BUSYGROUP/i.test(msg))
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
this.groupEnsured = true;
|
|
189
|
+
}
|
|
190
|
+
async start() {
|
|
191
|
+
if (this._running)
|
|
192
|
+
return;
|
|
193
|
+
this._stopped = false;
|
|
194
|
+
const acquired = await this.lease.acquire();
|
|
195
|
+
if (acquired)
|
|
196
|
+
this.hooks.leaseAcquired();
|
|
197
|
+
// BUG-1 fix: if ANY boot step AFTER a successful acquire fails (ensureGroup
|
|
198
|
+
// throws — redis down at boot / a non-BUSYGROUP error), RELEASE the lease
|
|
199
|
+
// before rethrowing. A boot failure must NOT orphan the lease (a held key
|
|
200
|
+
// blocks a restart until the TTL lapses).
|
|
201
|
+
try {
|
|
202
|
+
await this.ensureGroup();
|
|
203
|
+
await this.hydrateCursor();
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (acquired)
|
|
207
|
+
await this.lease.release();
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
this._running = true;
|
|
211
|
+
// Background consume loop. `autoStart:false` lets a test drive `runOnce()`.
|
|
212
|
+
if (this.autoStart)
|
|
213
|
+
this.loop = this.runLoop();
|
|
214
|
+
}
|
|
215
|
+
async hydrateCursor() {
|
|
216
|
+
try {
|
|
217
|
+
await this.begin(async (tx) => {
|
|
218
|
+
const cp = await this.checkpoint.get(this.subscription, tx);
|
|
219
|
+
if (cp)
|
|
220
|
+
this._hwm = { lastStreamId: cp.lastStreamId };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Cold-start read failure — cursor stays null, consume starts from `>`.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async runLoop() {
|
|
228
|
+
while (!this._stopped) {
|
|
229
|
+
if (this.lease.lost)
|
|
230
|
+
break;
|
|
231
|
+
try {
|
|
232
|
+
const processed = await this.pollOnce();
|
|
233
|
+
if (processed === 0) {
|
|
234
|
+
// Yield so a synchronous test-double doesn't tight-loop.
|
|
235
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
// BUG-2 fix: a poll that throws BECAUSE we're shutting down (stop() set
|
|
240
|
+
// `_stopped` and may have torn down the redis client mid-poll) is a CLEAN
|
|
241
|
+
// cancel, NOT a redis failure — exit quietly (no error metric, no
|
|
242
|
+
// error-sleep). A genuine poll-level error (XREADGROUP rejected — Redis
|
|
243
|
+
// down) while still running is the Pillar-5 infra-failure case: log via
|
|
244
|
+
// metric + back off, NEVER crash the loop.
|
|
245
|
+
if (this._stopped)
|
|
246
|
+
break;
|
|
247
|
+
this.hooks.recordDepth(-1, { error: "poll_failed" });
|
|
248
|
+
void err;
|
|
249
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Poll one batch + apply each entry. Returns count processed.
|
|
255
|
+
*
|
|
256
|
+
* Two reads per poll: FIRST our OWN pending-entries-list (PEL) at id `0` —
|
|
257
|
+
* entries delivered to this consumer but NOT yet XACK'd (a prior apply failed
|
|
258
|
+
* + NACK'd them); they MUST be retried, and a plain `>` read never re-returns
|
|
259
|
+
* a PEL entry. THEN `>` for brand-new entries. Without the PEL re-read a
|
|
260
|
+
* poison event would be applied exactly once and never retried.
|
|
261
|
+
*/
|
|
262
|
+
async pollOnce() {
|
|
263
|
+
if (!this.lease.leaseAcquired) {
|
|
264
|
+
const got = await this.lease.acquire();
|
|
265
|
+
if (got) {
|
|
266
|
+
this.hooks.leaseAcquired();
|
|
267
|
+
// Re-ensure the consumer group on every re-acquire: a Redis failover
|
|
268
|
+
// across the prior holder's death may have wiped the group, so a cached
|
|
269
|
+
// `groupEnsured` would let xreadgroup hit NOGROUP forever. Re-running is a
|
|
270
|
+
// harmless BUSYGROUP no-op when the group still exists.
|
|
271
|
+
this.groupEnsured = false;
|
|
272
|
+
// BUG fix (in-loop re-acquire): if ensureGroup throws AFTER this poll
|
|
273
|
+
// re-acquired the lease, RELEASE it before rethrowing — otherwise the
|
|
274
|
+
// renewal loop holds + renews the lease INDEFINITELY while the next poll
|
|
275
|
+
// skips this block (leaseAcquired is true) and hammers a never-created
|
|
276
|
+
// group (NOGROUP loop), deadlocking every other instance.
|
|
277
|
+
try {
|
|
278
|
+
await this.ensureGroup();
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
await this.lease.release();
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
let processed = 0;
|
|
290
|
+
// [reclaim] CRASH-RECOVERY reaper: XAUTOCLAIM transfers PENDING entries idle
|
|
291
|
+
// ≥ reclaimMinIdleMs from a DEAD predecessor (a crashed worker with a
|
|
292
|
+
// different consumer name) to US, the single-active successor. Without this,
|
|
293
|
+
// a crashed worker's un-ACK'd in-flight entries are PERMANENTLY ORPHANED (a
|
|
294
|
+
// plain "0" PEL re-read is per-consumer) → silent event loss → broken
|
|
295
|
+
// at-least-once. The fence guard inside the apply tx still rejects a stale
|
|
296
|
+
// write if a still-alive predecessor races us.
|
|
297
|
+
const claimedRaw = await this.redis.xautoclaim(this.stream, this.group, this.consumer, this.reclaimMinIdleMs, "0-0", "COUNT", this.pollBatch);
|
|
298
|
+
for (const p of parseAutoClaimEntries(claimedRaw)) {
|
|
299
|
+
if (this._stopped || this.lease.lost)
|
|
300
|
+
break;
|
|
301
|
+
await this.applyOne(p.id, p.event);
|
|
302
|
+
processed += 1;
|
|
303
|
+
}
|
|
304
|
+
// [retry] Re-read our own PEL (id `0`, non-blocking) for NACK'd entries.
|
|
305
|
+
const pendingRaw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "STREAMS", this.stream, "0");
|
|
306
|
+
for (const p of parseStreamEntries(pendingRaw)) {
|
|
307
|
+
if (this._stopped || this.lease.lost)
|
|
308
|
+
break;
|
|
309
|
+
await this.applyOne(p.id, p.event);
|
|
310
|
+
processed += 1;
|
|
311
|
+
}
|
|
312
|
+
// [new] Read brand-new entries (`>`, blocking up to blockMs).
|
|
313
|
+
const raw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "BLOCK", this.blockMs, "STREAMS", this.stream, ">");
|
|
314
|
+
// Sample backlog depth (cheap; best-effort).
|
|
315
|
+
try {
|
|
316
|
+
const depth = await this.redis.xlen(this.stream);
|
|
317
|
+
this.hooks.recordDepth(depth);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* depth is advisory — never fail the poll on it */
|
|
321
|
+
}
|
|
322
|
+
for (const p of parseStreamEntries(raw)) {
|
|
323
|
+
if (this._stopped || this.lease.lost)
|
|
324
|
+
break;
|
|
325
|
+
await this.applyOne(p.id, p.event);
|
|
326
|
+
processed += 1;
|
|
327
|
+
}
|
|
328
|
+
return processed;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Apply one entry: dedupe-skip → apply-tx (projection + dedupe.record +
|
|
332
|
+
* checkpoint.advance, fence-guarded) → XACK. On error, retry-then-dead-letter.
|
|
333
|
+
*/
|
|
334
|
+
async applyOne(streamId, event) {
|
|
335
|
+
// We dropped the lease mid-flight; the entry stays un-ACK'd in our PEL. The
|
|
336
|
+
// single-active successor (a new instance with a DIFFERENT consumer name)
|
|
337
|
+
// reclaims it via the [reclaim] XAUTOCLAIM reaper once idle — NOT via a plain
|
|
338
|
+
// "0" PEL re-read (that is per-consumer).
|
|
339
|
+
if (this.lease.lost)
|
|
340
|
+
return;
|
|
341
|
+
this._inFlight += 1;
|
|
342
|
+
const token = this.lease.fencingToken;
|
|
343
|
+
try {
|
|
344
|
+
// [1] Pre-tx dedupe probe (cheap). A redelivered already-applied event is
|
|
345
|
+
// XACK'd + skipped (exactly-once on the consumer side).
|
|
346
|
+
const alreadySeen = await this.begin((tx) => this.dedupe.seen(event.event_id, event.topic, tx));
|
|
347
|
+
if (alreadySeen) {
|
|
348
|
+
// The event already committed (durable dedupe row) — only the XACK is
|
|
349
|
+
// outstanding. A failing XACK here is a TRANSIENT ack-failure, NOT a
|
|
350
|
+
// poison event: swallow it (the entry stays un-acked in the PEL, re-acked
|
|
351
|
+
// next poll once redis recovers). NEVER route a committed event to
|
|
352
|
+
// retry/DLQ over an ack blip (matches Go's `_, _ = Ack(...)`).
|
|
353
|
+
await this.ackSwallow(streamId);
|
|
354
|
+
this.retries.delete(streamId);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// [2] Apply tx — projection upsert + dedupe.record + checkpoint.advance,
|
|
358
|
+
// all on ONE tx (atomic). The fence guard runs BEFORE the projection.
|
|
359
|
+
await this.begin(async (tx) => {
|
|
360
|
+
const ctx = {
|
|
361
|
+
tx,
|
|
362
|
+
event,
|
|
363
|
+
streamId,
|
|
364
|
+
validateFence: async () => {
|
|
365
|
+
if (token === null) {
|
|
366
|
+
throw new FenceLostError({
|
|
367
|
+
leaseKey: this.lease.key,
|
|
368
|
+
expectedToken: -1,
|
|
369
|
+
current: null,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
await this.lease.validateFence(token);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
await this.apply(ctx);
|
|
376
|
+
await this.dedupe.record(event.event_id, event.topic, tx);
|
|
377
|
+
await this.checkpoint.advance({ subscription: this.subscription, lastStreamId: streamId }, tx);
|
|
378
|
+
});
|
|
379
|
+
// [3] Commit succeeded → XACK + advance local HWM. The XACK is POST-COMMIT
|
|
380
|
+
// and NOT part of the apply tx: a failing XACK here is a transient
|
|
381
|
+
// ack-failure, NOT a poison event — swallow it (the committed event is
|
|
382
|
+
// re-acked next poll; the dedupe row makes a redelivery a no-op). Do NOT
|
|
383
|
+
// route a successfully-committed event to retry/DLQ over an ack blip.
|
|
384
|
+
await this.ackSwallow(streamId);
|
|
385
|
+
this.retries.delete(streamId);
|
|
386
|
+
this._hwm = { lastStreamId: streamId };
|
|
387
|
+
this._lastAppliedAtMs = Date.now();
|
|
388
|
+
if (event.occurred_at_ms !== undefined) {
|
|
389
|
+
this.hooks.recordLag(Math.max(0, (Date.now() - event.occurred_at_ms) / 1000));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
this.hooks.recordLag(0);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
// A FenceLostError means the lease was lost/taken-over mid-apply — the
|
|
397
|
+
// stale write was REJECTED before it landed. Do NOT XACK (the entry stays
|
|
398
|
+
// in our PEL; the single-active successor reclaims it via the XAUTOCLAIM
|
|
399
|
+
// reaper once idle) and do NOT dead-letter (benign handoff).
|
|
400
|
+
if (err instanceof FenceLostError) {
|
|
401
|
+
this.hooks.leaseLost();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// A FenceCheckUnavailableError means the fence-check READ failed (redis
|
|
405
|
+
// down) — TRANSIENT. Fail closed (the write was withheld), but do NOT XACK
|
|
406
|
+
// and do NOT dead-letter a non-poison event over a redis blip: leave it in
|
|
407
|
+
// the PEL to be reprocessed once redis recovers (cross-runtime-identical).
|
|
408
|
+
if (err instanceof FenceCheckUnavailableError) {
|
|
409
|
+
this.hooks.recordDepth(-1, { error: "fence_check_unavailable" });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await this.retryOrDeadLetter(streamId, event, err);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
this._inFlight -= 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* XACK an already-committed entry, SWALLOWING a transient ack failure. The
|
|
420
|
+
* XACK is post-commit (or post-dedupe-skip) and NOT part of the apply tx — a
|
|
421
|
+
* failing XACK is a transient redis blip, never a poison event. On failure the
|
|
422
|
+
* entry simply stays un-acked in the PEL and is re-acked next poll once redis
|
|
423
|
+
* recovers (the durable dedupe row makes the redelivery a no-op). This is what
|
|
424
|
+
* Go does (`_, _ = Ack(...)`); a throw here must NEVER reach retry/DLQ.
|
|
425
|
+
*/
|
|
426
|
+
async ackSwallow(streamId) {
|
|
427
|
+
try {
|
|
428
|
+
await this.redis.xack(this.stream, this.group, streamId);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
this.hooks.recordDepth(-1, { error: "ack_failed" });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Increment the retry ledger; dead-letter (+XACK) once the budget is spent. */
|
|
435
|
+
async retryOrDeadLetter(streamId, event, err) {
|
|
436
|
+
const attempt = (this.retries.get(streamId) ?? 0) + 1;
|
|
437
|
+
this.retries.set(streamId, attempt);
|
|
438
|
+
if (attempt < this.retryMaxAttempts) {
|
|
439
|
+
// No XACK — the entry stays in the PEL and is redelivered next poll.
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await this.deadLetter.deadLetter({
|
|
444
|
+
sourceStream: this.stream,
|
|
445
|
+
topic: event.topic,
|
|
446
|
+
eventId: event.event_id,
|
|
447
|
+
payload: event,
|
|
448
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
449
|
+
retryCount: attempt,
|
|
450
|
+
});
|
|
451
|
+
// ACK after the dead-letter is durable so the PEL doesn't grow unbounded.
|
|
452
|
+
await this.redis.xack(this.stream, this.group, streamId);
|
|
453
|
+
this.retries.delete(streamId);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// The dead-letter sink itself failed — leave the entry in the PEL (it will
|
|
457
|
+
// be redelivered) rather than losing it. Never XACK on a failed dead-letter.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** Run exactly one poll batch (test seam / cron tick). */
|
|
461
|
+
async runOnce() {
|
|
462
|
+
return this.pollOnce();
|
|
463
|
+
}
|
|
464
|
+
async stop(opts) {
|
|
465
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
|
|
466
|
+
const start = Date.now();
|
|
467
|
+
this._stopped = true;
|
|
468
|
+
this._running = false;
|
|
469
|
+
try {
|
|
470
|
+
await Promise.race([
|
|
471
|
+
this.loop,
|
|
472
|
+
new Promise((r) => setTimeout(r, timeoutMs)),
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
/* swallow */
|
|
477
|
+
}
|
|
478
|
+
while (this._inFlight > 0 && Date.now() - start < timeoutMs) {
|
|
479
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
480
|
+
}
|
|
481
|
+
const drainedCleanly = this._inFlight === 0;
|
|
482
|
+
await this.lease.release();
|
|
483
|
+
const durationMs = Date.now() - start;
|
|
484
|
+
this.hooks.recordDrainDuration(durationMs);
|
|
485
|
+
return { drainedCleanly, durationMs };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
//# sourceMappingURL=streams-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams-worker.js","sourceRoot":"","sources":["../../src/worker/streams-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { DeadLetterSink } from "./dead-letter.js";
|
|
2
|
+
import { ObservabilityHooks } from "./observability-hooks.js";
|
|
3
|
+
import { type SingleActiveLeaseOpts } from "./single-active-lease.js";
|
|
4
|
+
import type { SqlExecutor } from "./tx.js";
|
|
5
|
+
import type { WorkerHandle, WorkerHwm, WorkerStopOptions, WorkerStopResult } from "./worker-handle.js";
|
|
6
|
+
/**
|
|
7
|
+
* The context a sweeper job receives each tick. Carries the executor the job
|
|
8
|
+
* runs its delete + checkpoint advance on, the durable cursor it resumes from,
|
|
9
|
+
* and the fence guard it MUST call before any non-idempotent delete.
|
|
10
|
+
*/
|
|
11
|
+
export interface SweeperJobContext {
|
|
12
|
+
/** A pooled executor (the sweep is independent of any apply transaction; the
|
|
13
|
+
* job MAY open its own tx via the caller-provided executor factory). */
|
|
14
|
+
exec: SqlExecutor;
|
|
15
|
+
/** The durable cursor the worker resumes from (null on cold start). */
|
|
16
|
+
lastStreamId: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* THE side-effecting guard. The job MUST `await ctx.validateFence()` right
|
|
19
|
+
* before any non-idempotent delete: it re-reads the live lease and throws
|
|
20
|
+
* {@link FenceLostError} if the lease was lost/taken-over since the tick
|
|
21
|
+
* began — so a stale sweeper's late delete cannot land.
|
|
22
|
+
*/
|
|
23
|
+
validateFence(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** What a single sweep tick returns: how many rows it removed + the new cursor. */
|
|
26
|
+
export interface SweeperJobResult {
|
|
27
|
+
/** Rows removed this tick (surfaced as `nodii.worker.swept_total`). */
|
|
28
|
+
swept: number;
|
|
29
|
+
/**
|
|
30
|
+
* The new durable cursor position to advance the checkpoint to (e.g. the
|
|
31
|
+
* max consumed_at swept, or a synthetic monotonic marker). Omit / null to
|
|
32
|
+
* leave the cursor unchanged (a no-op sweep).
|
|
33
|
+
*/
|
|
34
|
+
lastStreamId?: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** The injected unit of sweep work (e.g. `dedupe.sweep(...)`, a retention DELETE). */
|
|
37
|
+
export interface SweeperJobDefinition {
|
|
38
|
+
/** Stable name for logs/metrics (the lease scope defaults to this). */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Run exactly one sweep pass. MUST call `ctx.validateFence()` before delete. */
|
|
41
|
+
runOnce(ctx: SweeperJobContext): Promise<SweeperJobResult>;
|
|
42
|
+
}
|
|
43
|
+
export interface SweeperWorkerOpts {
|
|
44
|
+
/** The injected sweep job. */
|
|
45
|
+
job: SweeperJobDefinition;
|
|
46
|
+
/** Provides a fresh pooled executor per tick (e.g. `() => postgresJsExecutor(sql)`). */
|
|
47
|
+
executor: () => SqlExecutor | Promise<SqlExecutor>;
|
|
48
|
+
/** Lease config (service + scope + ttlMs). The renew loop runs inside it. */
|
|
49
|
+
lease: Omit<SingleActiveLeaseOpts, "onLeaseLost">;
|
|
50
|
+
/** The durable checkpoint store (resume cursor). */
|
|
51
|
+
checkpoint: import("./checkpoint-store.js").CheckpointStore;
|
|
52
|
+
/** The checkpoint subscription key (defaults to `sweeper:<job.name>`). */
|
|
53
|
+
subscription?: string;
|
|
54
|
+
/** Tick interval (ms) for the autonomous loop. Default 60_000. */
|
|
55
|
+
intervalMs?: number;
|
|
56
|
+
/** Injected dead-letter sink (NO default). A tick error is dead-lettered. */
|
|
57
|
+
deadLetter: DeadLetterSink;
|
|
58
|
+
/** Observability hooks (canonical metrics). Constructed if omitted. */
|
|
59
|
+
hooks?: ObservabilityHooks;
|
|
60
|
+
/** Worker label for metric attributes. Default "sweeper". */
|
|
61
|
+
workerLabel?: string;
|
|
62
|
+
/** Launch the interval loop on `start()`. Default true; tests pass false to
|
|
63
|
+
* drive `runOnce()` deterministically. */
|
|
64
|
+
autoStart?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* SweeperWorker — the reference {@link WorkerHandle}. Acquires a single-active
|
|
68
|
+
* lease, runs the injected sweep job on an interval, advances the checkpoint
|
|
69
|
+
* each tick (resumable), and drains bounded on stop.
|
|
70
|
+
*/
|
|
71
|
+
export declare class SweeperWorker implements WorkerHandle {
|
|
72
|
+
private readonly job;
|
|
73
|
+
private readonly executorFactory;
|
|
74
|
+
private readonly lease;
|
|
75
|
+
private readonly checkpoint;
|
|
76
|
+
private readonly subscription;
|
|
77
|
+
private readonly intervalMs;
|
|
78
|
+
private readonly deadLetter;
|
|
79
|
+
private readonly hooks;
|
|
80
|
+
private readonly autoStart;
|
|
81
|
+
private _running;
|
|
82
|
+
private _stopped;
|
|
83
|
+
private _inFlight;
|
|
84
|
+
private _hwm;
|
|
85
|
+
private _lastAppliedAtMs;
|
|
86
|
+
private loopTimer;
|
|
87
|
+
constructor(opts: SweeperWorkerOpts);
|
|
88
|
+
get running(): boolean;
|
|
89
|
+
get leaseAcquired(): boolean;
|
|
90
|
+
get lag(): number | null;
|
|
91
|
+
get currentHwm(): WorkerHwm;
|
|
92
|
+
/** Acquire the lease + start the interval loop. Idempotent. */
|
|
93
|
+
start(): Promise<void>;
|
|
94
|
+
/** Read the durable checkpoint so a restart resumes rather than re-sweeps. */
|
|
95
|
+
private hydrateCursor;
|
|
96
|
+
/** One interval tick: acquire-if-needed → runOnce → swallow into dead-letter. */
|
|
97
|
+
private tick;
|
|
98
|
+
/**
|
|
99
|
+
* Run exactly ONE sweep pass. Acquires/validates the lease for the tick,
|
|
100
|
+
* invokes the injected job, advances the checkpoint, emits `swept_total`.
|
|
101
|
+
* Resolves the number of rows swept (0 when the lease is not held).
|
|
102
|
+
*/
|
|
103
|
+
runOnce(): Promise<number>;
|
|
104
|
+
/** Bounded graceful stop: halt the loop, let an in-flight tick finish, release. */
|
|
105
|
+
stop(opts?: WorkerStopOptions): Promise<WorkerStopResult>;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=sweeper-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweeper-worker.d.ts","sourceRoot":"","sources":["../../src/worker/sweeper-worker.ts"],"names":[],"mappings":"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"}
|