@flowcore/data-pump 0.22.1 → 0.23.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 +28 -0
- package/README.md +313 -191
- package/dist/mod.d.ts +147 -16
- package/dist/mod.js +623 -170
- package/package.json +1 -1
package/dist/mod.js
CHANGED
|
@@ -17,58 +17,227 @@ import {
|
|
|
17
17
|
import { FlowcoreClient } from "@flowcore/sdk";
|
|
18
18
|
|
|
19
19
|
// src/data-pump/metrics.ts
|
|
20
|
-
import
|
|
21
|
-
var dataPumpPromRegistry = new
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
20
|
+
import { Counter, Gauge, Histogram, register as defaultPromRegistry, Registry } from "prom-client";
|
|
21
|
+
var dataPumpPromRegistry = new Registry();
|
|
22
|
+
var SOURCE_LABELS = ["tenant", "data_core", "flow_type"];
|
|
23
|
+
var EVENT_SOURCE_LABELS = [...SOURCE_LABELS, "event_type"];
|
|
24
|
+
var REPLAY_DURATION_BUCKETS_SECONDS = [
|
|
25
|
+
5e-3,
|
|
26
|
+
0.01,
|
|
27
|
+
0.025,
|
|
28
|
+
0.05,
|
|
29
|
+
0.1,
|
|
30
|
+
0.25,
|
|
31
|
+
0.5,
|
|
32
|
+
1,
|
|
33
|
+
2,
|
|
34
|
+
5,
|
|
35
|
+
10,
|
|
36
|
+
20,
|
|
37
|
+
30,
|
|
38
|
+
60,
|
|
39
|
+
120
|
|
40
|
+
];
|
|
41
|
+
var REPLAY_EVENT_COUNT_BUCKETS = [1, 10, 50, 100, 250, 500, 1e3, 2e3, 5e3, 1e4];
|
|
42
|
+
var REPLAY_BATCH_SIZE_BUCKETS = ["empty", "1", "2-10", "11-100", "101-1000", "1001+"];
|
|
43
|
+
var REPLAY_FETCH_RESULTS = ["success", "no_events", "error"];
|
|
44
|
+
var REPLAY_STAGE_RESULTS = ["success", "error"];
|
|
45
|
+
var REPLAY_IDLE_REASONS = ["no_events", "buffer_full", "waiting_for_events"];
|
|
46
|
+
function replayBatchSizeBucket(size) {
|
|
47
|
+
if (size <= 0) return "empty";
|
|
48
|
+
if (size === 1) return "1";
|
|
49
|
+
if (size <= 10) return "2-10";
|
|
50
|
+
if (size <= 100) return "11-100";
|
|
51
|
+
if (size <= 1e3) return "101-1000";
|
|
52
|
+
return "1001+";
|
|
53
|
+
}
|
|
54
|
+
function createDataPumpMetrics(registry, includeDefaultRegistry = false) {
|
|
55
|
+
const registers = includeDefaultRegistry ? [registry, defaultPromRegistry] : [registry];
|
|
56
|
+
const bufferEventCountGauge = new Gauge({
|
|
57
|
+
name: "flowcore_data_pump_buffer_events_gauge",
|
|
58
|
+
help: "The number of events in the buffer",
|
|
59
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
60
|
+
registers
|
|
61
|
+
});
|
|
62
|
+
const bufferReservedEventCountGauge = new Gauge({
|
|
63
|
+
name: "flowcore_data_pump_buffer_reserved_events_gauge",
|
|
64
|
+
help: "The number of reserved events in the buffer",
|
|
65
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
66
|
+
registers
|
|
67
|
+
});
|
|
68
|
+
const bufferSizeBytesGauge = new Gauge({
|
|
69
|
+
name: "flowcore_data_pump_buffer_size_bytes_gauge",
|
|
70
|
+
help: "The size of the buffer in bytes",
|
|
71
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
72
|
+
registers
|
|
73
|
+
});
|
|
74
|
+
const eventsAcknowledgedCounter = new Counter({
|
|
75
|
+
name: "flowcore_data_pump_events_acknowledged_counter",
|
|
76
|
+
help: "The number of events acknowledged",
|
|
77
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
78
|
+
registers
|
|
79
|
+
});
|
|
80
|
+
const eventsFailedCounter = new Counter({
|
|
81
|
+
name: "flowcore_data_pump_events_failed_counter",
|
|
82
|
+
help: "The number of events failed",
|
|
83
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
84
|
+
registers
|
|
85
|
+
});
|
|
86
|
+
const eventsPulledSizeBytesCounter = new Counter({
|
|
87
|
+
name: "flowcore_data_pump_events_pulled_size_bytes_counter",
|
|
88
|
+
help: "The size of the events pulled in bytes",
|
|
89
|
+
labelNames: EVENT_SOURCE_LABELS,
|
|
90
|
+
registers
|
|
91
|
+
});
|
|
92
|
+
const sdkCommandsCounter = new Counter({
|
|
93
|
+
name: "flowcore_data_pump_sdk_commands_counter",
|
|
94
|
+
help: "The number of SDK commands",
|
|
95
|
+
labelNames: ["command"],
|
|
96
|
+
registers
|
|
97
|
+
});
|
|
98
|
+
const replayFetchDuration = new Histogram({
|
|
99
|
+
name: "flowcore_data_pump_replay_fetch_duration_seconds",
|
|
100
|
+
help: "Replay event fetch duration in seconds",
|
|
101
|
+
labelNames: [...SOURCE_LABELS, "result"],
|
|
102
|
+
buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
|
|
103
|
+
registers
|
|
104
|
+
});
|
|
105
|
+
const replayFetchEvents = new Histogram({
|
|
106
|
+
name: "flowcore_data_pump_replay_fetch_events",
|
|
107
|
+
help: "Number of events returned by a replay fetch",
|
|
108
|
+
labelNames: SOURCE_LABELS,
|
|
109
|
+
buckets: [...REPLAY_EVENT_COUNT_BUCKETS],
|
|
110
|
+
registers
|
|
111
|
+
});
|
|
112
|
+
const replayHandlerDuration = new Histogram({
|
|
113
|
+
name: "flowcore_data_pump_replay_handler_duration_seconds",
|
|
114
|
+
help: "Replay handler duration in seconds",
|
|
115
|
+
labelNames: [...SOURCE_LABELS, "result", "batch_size_bucket"],
|
|
116
|
+
buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
|
|
117
|
+
registers
|
|
118
|
+
});
|
|
119
|
+
const replayAcknowledgementDuration = new Histogram({
|
|
120
|
+
name: "flowcore_data_pump_replay_acknowledgement_duration_seconds",
|
|
121
|
+
help: "Replay acknowledgement duration in seconds, excluding checkpointing",
|
|
122
|
+
labelNames: [...SOURCE_LABELS, "result"],
|
|
123
|
+
buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
|
|
124
|
+
registers
|
|
125
|
+
});
|
|
126
|
+
const replayCheckpointDuration = new Histogram({
|
|
127
|
+
name: "flowcore_data_pump_replay_checkpoint_duration_seconds",
|
|
128
|
+
help: "Replay state checkpoint duration in seconds",
|
|
129
|
+
labelNames: [...SOURCE_LABELS, "result"],
|
|
130
|
+
buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
|
|
131
|
+
registers
|
|
132
|
+
});
|
|
133
|
+
const replayIdleDuration = new Histogram({
|
|
134
|
+
name: "flowcore_data_pump_replay_idle_duration_seconds",
|
|
135
|
+
help: "Time the replay loop spends idle",
|
|
136
|
+
labelNames: [...SOURCE_LABELS, "reason"],
|
|
137
|
+
buckets: [...REPLAY_DURATION_BUCKETS_SECONDS, 300],
|
|
138
|
+
registers
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
bufferEventCountGauge,
|
|
142
|
+
bufferReservedEventCountGauge,
|
|
143
|
+
bufferSizeBytesGauge,
|
|
144
|
+
eventsAcknowledgedCounter,
|
|
145
|
+
eventsFailedCounter,
|
|
146
|
+
eventsPulledSizeBytesCounter,
|
|
147
|
+
sdkCommandsCounter,
|
|
148
|
+
replayFetchDuration,
|
|
149
|
+
replayFetchEvents,
|
|
150
|
+
replayHandlerDuration,
|
|
151
|
+
replayAcknowledgementDuration,
|
|
152
|
+
replayCheckpointDuration,
|
|
153
|
+
replayIdleDuration
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
var metrics = createDataPumpMetrics(dataPumpPromRegistry, true);
|
|
157
|
+
var ReplayStageObserver = class {
|
|
158
|
+
constructor(stageMetrics, source, now = () => performance.now()) {
|
|
159
|
+
this.stageMetrics = stageMetrics;
|
|
160
|
+
this.source = source;
|
|
161
|
+
this.now = now;
|
|
162
|
+
}
|
|
163
|
+
stageMetrics;
|
|
164
|
+
source;
|
|
165
|
+
now;
|
|
166
|
+
async observeFetch(operation) {
|
|
167
|
+
const startedAt = this.now();
|
|
168
|
+
try {
|
|
169
|
+
const result = await operation();
|
|
170
|
+
this.stageMetrics.replayFetchDuration.observe(
|
|
171
|
+
{ ...this.source, result: result.events.length ? "success" : "no_events" },
|
|
172
|
+
this.elapsedSeconds(startedAt)
|
|
173
|
+
);
|
|
174
|
+
this.stageMetrics.replayFetchEvents.observe(this.source, result.events.length);
|
|
175
|
+
return result;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.stageMetrics.replayFetchDuration.observe({ ...this.source, result: "error" }, this.elapsedSeconds(startedAt));
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async observeHandler(events, operation) {
|
|
182
|
+
const startedAt = this.now();
|
|
183
|
+
const batchSize = replayBatchSizeBucket(events.length);
|
|
184
|
+
try {
|
|
185
|
+
const result = await operation();
|
|
186
|
+
this.stageMetrics.replayHandlerDuration.observe(
|
|
187
|
+
{ ...this.source, result: "success", batch_size_bucket: batchSize },
|
|
188
|
+
this.elapsedSeconds(startedAt)
|
|
189
|
+
);
|
|
190
|
+
return result;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.stageMetrics.replayHandlerDuration.observe(
|
|
193
|
+
{ ...this.source, result: "error", batch_size_bucket: batchSize },
|
|
194
|
+
this.elapsedSeconds(startedAt)
|
|
195
|
+
);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
observeAcknowledgement(operation) {
|
|
200
|
+
const startedAt = this.now();
|
|
201
|
+
try {
|
|
202
|
+
const result = operation();
|
|
203
|
+
this.stageMetrics.replayAcknowledgementDuration.observe(
|
|
204
|
+
{ ...this.source, result: "success" },
|
|
205
|
+
this.elapsedSeconds(startedAt)
|
|
206
|
+
);
|
|
207
|
+
return result;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.stageMetrics.replayAcknowledgementDuration.observe(
|
|
210
|
+
{ ...this.source, result: "error" },
|
|
211
|
+
this.elapsedSeconds(startedAt)
|
|
212
|
+
);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
observeCheckpoint(operation) {
|
|
217
|
+
return this.observeResult(this.stageMetrics.replayCheckpointDuration, operation);
|
|
218
|
+
}
|
|
219
|
+
async observeIdle(reason, operation) {
|
|
220
|
+
const startedAt = this.now();
|
|
221
|
+
try {
|
|
222
|
+
return await operation();
|
|
223
|
+
} finally {
|
|
224
|
+
this.stageMetrics.replayIdleDuration.observe({ ...this.source, reason }, this.elapsedSeconds(startedAt));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async observeResult(histogram, operation) {
|
|
228
|
+
const startedAt = this.now();
|
|
229
|
+
try {
|
|
230
|
+
const result = await operation();
|
|
231
|
+
histogram.observe({ ...this.source, result: "success" }, this.elapsedSeconds(startedAt));
|
|
232
|
+
return result;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
histogram.observe({ ...this.source, result: "error" }, this.elapsedSeconds(startedAt));
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
elapsedSeconds(startedAt) {
|
|
239
|
+
return Math.max(0, this.now() - startedAt) / 1e3;
|
|
240
|
+
}
|
|
72
241
|
};
|
|
73
242
|
var activeWorkersGauge = new Gauge({
|
|
74
243
|
name: "flowcore_data_pump_cluster_active_workers_gauge",
|
|
@@ -145,6 +314,8 @@ var FlowcoreDataSource = class {
|
|
|
145
314
|
eventTypeIds;
|
|
146
315
|
/** Cached time buckets */
|
|
147
316
|
timeBuckets;
|
|
317
|
+
/** Index for each cached bucket. */
|
|
318
|
+
timeBucketIndexes = /* @__PURE__ */ new Map();
|
|
148
319
|
/**
|
|
149
320
|
* Gets the tenant name from the data source configuration
|
|
150
321
|
* @returns Tenant name
|
|
@@ -277,16 +448,46 @@ var FlowcoreDataSource = class {
|
|
|
277
448
|
tenant: this.options.dataSource.tenant,
|
|
278
449
|
eventTypeId: await this.getEventTypeIds(),
|
|
279
450
|
cursor: cursor || void 0,
|
|
280
|
-
pageSize: 1e4
|
|
451
|
+
pageSize: 1e4,
|
|
452
|
+
order: "asc"
|
|
281
453
|
}),
|
|
282
454
|
this.options.directMode
|
|
283
455
|
);
|
|
284
456
|
timeBuckets.push(...result.timeBuckets);
|
|
285
457
|
cursor = result.nextCursor;
|
|
286
458
|
} while (cursor !== void 0);
|
|
287
|
-
|
|
459
|
+
const normalizedTimeBuckets = [...new Set(timeBuckets.sort())];
|
|
460
|
+
this.timeBuckets = normalizedTimeBuckets;
|
|
461
|
+
this.timeBucketIndexes = /* @__PURE__ */ new Map();
|
|
462
|
+
for (let index = 0; index < normalizedTimeBuckets.length; index++) {
|
|
463
|
+
this.timeBucketIndexes.set(normalizedTimeBuckets[index], index);
|
|
464
|
+
}
|
|
288
465
|
return this.timeBuckets;
|
|
289
466
|
}
|
|
467
|
+
lowerBoundTimeBucket(timeBucket) {
|
|
468
|
+
const timeBuckets = this.timeBuckets ?? [];
|
|
469
|
+
const target = Number.parseFloat(timeBucket);
|
|
470
|
+
let low = 0;
|
|
471
|
+
let high = timeBuckets.length;
|
|
472
|
+
while (low < high) {
|
|
473
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
474
|
+
if (Number.parseFloat(timeBuckets[middle]) < target) low = middle + 1;
|
|
475
|
+
else high = middle;
|
|
476
|
+
}
|
|
477
|
+
return low;
|
|
478
|
+
}
|
|
479
|
+
upperBoundTimeBucket(timeBucket) {
|
|
480
|
+
const timeBuckets = this.timeBuckets ?? [];
|
|
481
|
+
const target = Number.parseFloat(timeBucket);
|
|
482
|
+
let low = 0;
|
|
483
|
+
let high = timeBuckets.length;
|
|
484
|
+
while (low < high) {
|
|
485
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
486
|
+
if (Number.parseFloat(timeBuckets[middle]) <= target) low = middle + 1;
|
|
487
|
+
else high = middle;
|
|
488
|
+
}
|
|
489
|
+
return low;
|
|
490
|
+
}
|
|
290
491
|
/**
|
|
291
492
|
* Gets the next time bucket after the specified time bucket
|
|
292
493
|
* @param timeBucket - The reference time bucket
|
|
@@ -298,8 +499,8 @@ var FlowcoreDataSource = class {
|
|
|
298
499
|
return null;
|
|
299
500
|
}
|
|
300
501
|
const timeBuckets = await this.getTimeBuckets();
|
|
301
|
-
const index =
|
|
302
|
-
if (index ===
|
|
502
|
+
const index = this.timeBucketIndexes.get(closestTimeBucket);
|
|
503
|
+
if (index === void 0) {
|
|
303
504
|
throw new Error(`Could not get next timeBucket, timeBucket ${timeBucket} not found`);
|
|
304
505
|
}
|
|
305
506
|
return timeBuckets[index + 1] ?? null;
|
|
@@ -316,10 +517,14 @@ var FlowcoreDataSource = class {
|
|
|
316
517
|
if (!timeBucket.match(/^\d{14}$/)) {
|
|
317
518
|
throw new Error(`Invalid timebucket: ${timeBucket}`);
|
|
318
519
|
}
|
|
520
|
+
if (!timeBuckets.length) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
319
523
|
if (getBefore) {
|
|
320
|
-
|
|
524
|
+
const index = this.upperBoundTimeBucket(timeBucket) - 1;
|
|
525
|
+
return timeBuckets[index] ?? timeBuckets[timeBuckets.length - 1];
|
|
321
526
|
}
|
|
322
|
-
return timeBuckets.
|
|
527
|
+
return timeBuckets[this.lowerBoundTimeBucket(timeBucket)] ?? timeBuckets[timeBuckets.length - 1];
|
|
323
528
|
}
|
|
324
529
|
/**
|
|
325
530
|
* Gets events from the data source starting from a specific state
|
|
@@ -620,6 +825,9 @@ var PulseEmitter = class {
|
|
|
620
825
|
timeBucket: snapshot.timeBucket,
|
|
621
826
|
eventId: snapshot.eventId ?? null,
|
|
622
827
|
isLive: snapshot.isLive,
|
|
828
|
+
// Same widening as `sourceId` above: the CP accepts `paused`, the pinned sdk
|
|
829
|
+
// type does not declare it yet. The cast below absorbs it.
|
|
830
|
+
paused: snapshot.paused,
|
|
623
831
|
buffer: {
|
|
624
832
|
depth: snapshot.bufferDepth,
|
|
625
833
|
reserved: snapshot.bufferReserved,
|
|
@@ -644,6 +852,7 @@ var PulseEmitter = class {
|
|
|
644
852
|
};
|
|
645
853
|
|
|
646
854
|
// src/data-pump/data-pump.ts
|
|
855
|
+
var textEncoder = new TextEncoder();
|
|
647
856
|
var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
648
857
|
constructor(dataSource, notifier, stateManager, options, logger) {
|
|
649
858
|
this.dataSource = dataSource;
|
|
@@ -651,6 +860,14 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
651
860
|
this.stateManager = stateManager;
|
|
652
861
|
this.options = options;
|
|
653
862
|
this.logger = logger;
|
|
863
|
+
this.replayObserver = new ReplayStageObserver(metrics, {
|
|
864
|
+
tenant: this.dataSource.tenant,
|
|
865
|
+
data_core: this.dataSource.dataCore,
|
|
866
|
+
flow_type: this.dataSource.flowType
|
|
867
|
+
});
|
|
868
|
+
for (const eventType of this.dataSource.eventTypes) {
|
|
869
|
+
this.bufferStats.set(eventType, { eventCount: 0, eventReservedCount: 0, eventSizeBytes: 0 });
|
|
870
|
+
}
|
|
654
871
|
this.bufferState = {
|
|
655
872
|
timeBucket: format(startOfHour(utc(/* @__PURE__ */ new Date())), "yyyyMMddHH0000"),
|
|
656
873
|
eventId: TimeUuid.now().toString()
|
|
@@ -663,7 +880,18 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
663
880
|
logger;
|
|
664
881
|
nextCursor;
|
|
665
882
|
running = false;
|
|
883
|
+
// Delivery pause. Independent of `running`: a paused pump keeps fetching, keeps its
|
|
884
|
+
// buffer, keeps its cursor and keeps emitting pulses — it only stops handing events
|
|
885
|
+
// to the processor. The flag is sticky across `restart()` and `stop()`/`start()`, so
|
|
886
|
+
// a repositioned or bounced pump stays paused until `resume()` is called.
|
|
887
|
+
paused = false;
|
|
666
888
|
restartTo;
|
|
889
|
+
// Invalidates delivery work that crossed a stop/restart boundary. Event IDs
|
|
890
|
+
// can reappear during replay, so `running` alone cannot identify the owner.
|
|
891
|
+
processLoopGeneration = 0;
|
|
892
|
+
activeProcessLoopGeneration;
|
|
893
|
+
processLoopBackoffGeneration;
|
|
894
|
+
processLoopRestartTimer;
|
|
667
895
|
abortController;
|
|
668
896
|
buffer = [];
|
|
669
897
|
bufferState;
|
|
@@ -677,13 +905,23 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
677
905
|
pulledCount = 0;
|
|
678
906
|
processLoopRestartAttempts = 0;
|
|
679
907
|
mainLoopRestartAttempts = 0;
|
|
908
|
+
replayObserver;
|
|
909
|
+
bufferStats = /* @__PURE__ */ new Map();
|
|
910
|
+
bufferReservedCount = 0;
|
|
911
|
+
bufferSizeBytes = 0;
|
|
912
|
+
gaugePublicationScheduled = false;
|
|
913
|
+
/**
|
|
914
|
+
* Whether delivery to the processor is currently paused.
|
|
915
|
+
* A paused pump is still running: it fetches, buffers and pulses.
|
|
916
|
+
*/
|
|
917
|
+
get isPaused() {
|
|
918
|
+
return this.paused;
|
|
919
|
+
}
|
|
680
920
|
get isRunning() {
|
|
681
921
|
return this.running;
|
|
682
922
|
}
|
|
683
923
|
getSnapshot() {
|
|
684
924
|
if (!this.running) return null;
|
|
685
|
-
const reserved = this.buffer.filter((b) => b.status === "reserved").length;
|
|
686
|
-
const sizeBytes = this.buffer.reduce((sum, b) => sum + JSON.stringify(b.event.payload).length, 0);
|
|
687
925
|
return {
|
|
688
926
|
pathwayId: this.pulseEmitter ? "" : "",
|
|
689
927
|
// set by caller
|
|
@@ -691,9 +929,10 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
691
929
|
timeBucket: this.bufferState.timeBucket,
|
|
692
930
|
eventId: this.bufferState.eventId,
|
|
693
931
|
isLive: this.isLive,
|
|
932
|
+
paused: this.paused,
|
|
694
933
|
bufferDepth: this.buffer.length,
|
|
695
|
-
bufferReserved:
|
|
696
|
-
bufferSizeBytes:
|
|
934
|
+
bufferReserved: this.bufferReservedCount,
|
|
935
|
+
bufferSizeBytes: this.bufferSizeBytes,
|
|
697
936
|
acknowledgedTotal: this.acknowledgedCount,
|
|
698
937
|
failedTotal: this.failedCount,
|
|
699
938
|
pulledTotal: this.pulledCount,
|
|
@@ -739,6 +978,9 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
739
978
|
},
|
|
740
979
|
options.logger
|
|
741
980
|
);
|
|
981
|
+
if (options.paused && options.processor) {
|
|
982
|
+
pump.paused = true;
|
|
983
|
+
}
|
|
742
984
|
if (options.pulse) {
|
|
743
985
|
const pathwayId = options.pulse.pathwayId;
|
|
744
986
|
const sourceId = options.pulse.sourceId;
|
|
@@ -771,7 +1013,7 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
771
1013
|
this.running = true;
|
|
772
1014
|
this.startedAt = Date.now();
|
|
773
1015
|
this.nextCursor = void 0;
|
|
774
|
-
this.updateMetricsGauges();
|
|
1016
|
+
this.updateMetricsGauges(true);
|
|
775
1017
|
this.pulseEmitter?.start();
|
|
776
1018
|
const currentState = await this.stateManager.getState();
|
|
777
1019
|
const timeBucket = currentState ? await this.dataSource.getClosestTimeBucket(currentState.timeBucket) : format(startOfHour(utc(/* @__PURE__ */ new Date())), "yyyyMMddHH0000");
|
|
@@ -826,23 +1068,68 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
826
1068
|
if (stopAt !== void 0) {
|
|
827
1069
|
this.options.stopAt = stopAt ?? void 0;
|
|
828
1070
|
}
|
|
1071
|
+
this.isLive = false;
|
|
829
1072
|
this.stop(true);
|
|
830
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Pause delivery to the processor.
|
|
1076
|
+
*
|
|
1077
|
+
* The fetch loop keeps running and tops the buffer up to `bufferSize`, then blocks on
|
|
1078
|
+
* normal backpressure. The buffer, the cursor and the pulse emitter are untouched, so
|
|
1079
|
+
* the control plane still sees a live pump. An in-flight batch finishes its handler
|
|
1080
|
+
* and acknowledges, so the checkpoint stays accurate and nothing is redelivered
|
|
1081
|
+
* needlessly.
|
|
1082
|
+
*
|
|
1083
|
+
* Idempotent. A paused pump holds up to `bufferSize` events in memory.
|
|
1084
|
+
*/
|
|
1085
|
+
pause() {
|
|
1086
|
+
if (!this.options.processor) {
|
|
1087
|
+
this.logger?.warn("pause() ignored: this pump has no processor. Stop calling reserve() instead.");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (this.paused) return;
|
|
1091
|
+
this.paused = true;
|
|
1092
|
+
this.logger?.info("Data pump paused");
|
|
1093
|
+
this.notifyEventWaiters(false);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Resume delivery to the processor from the exact position where {@link pause} stopped it.
|
|
1097
|
+
* Idempotent.
|
|
1098
|
+
*/
|
|
1099
|
+
resume() {
|
|
1100
|
+
if (!this.paused) return;
|
|
1101
|
+
this.paused = false;
|
|
1102
|
+
if (this.processLoopRestartTimer) {
|
|
1103
|
+
clearTimeout(this.processLoopRestartTimer);
|
|
1104
|
+
this.processLoopRestartTimer = void 0;
|
|
1105
|
+
}
|
|
1106
|
+
this.processLoopBackoffGeneration = void 0;
|
|
1107
|
+
this.processLoopRestartAttempts = 0;
|
|
1108
|
+
this.logger?.info("Data pump resumed");
|
|
1109
|
+
this.ensureProcessLoop();
|
|
1110
|
+
}
|
|
831
1111
|
stop(isRestart = false) {
|
|
832
1112
|
this.running = false;
|
|
1113
|
+
this.processLoopGeneration++;
|
|
1114
|
+
this.activeProcessLoopGeneration = void 0;
|
|
1115
|
+
this.processLoopBackoffGeneration = void 0;
|
|
1116
|
+
if (this.processLoopRestartTimer) {
|
|
1117
|
+
clearTimeout(this.processLoopRestartTimer);
|
|
1118
|
+
this.processLoopRestartTimer = void 0;
|
|
1119
|
+
}
|
|
833
1120
|
this.processLoopRestartAttempts = 0;
|
|
834
1121
|
this.mainLoopRestartAttempts = 0;
|
|
835
1122
|
this.buffer = [];
|
|
836
|
-
this.
|
|
1123
|
+
this.resetBufferStats();
|
|
1124
|
+
this.updateMetricsGauges(true);
|
|
837
1125
|
this.pulseEmitter?.stop();
|
|
838
1126
|
this.abortController?.abort();
|
|
839
1127
|
this.waiterBufferThreshold?.();
|
|
840
|
-
|
|
841
|
-
this.waiterEvents?.();
|
|
842
|
-
}
|
|
1128
|
+
this.notifyEventWaiters(!isRestart);
|
|
843
1129
|
}
|
|
844
1130
|
updateState(eventId) {
|
|
845
|
-
|
|
1131
|
+
const stateManager = this.stateManager;
|
|
1132
|
+
if (!stateManager.setState) {
|
|
846
1133
|
return;
|
|
847
1134
|
}
|
|
848
1135
|
const stateEventId = eventId ?? this.buffer[0]?.event.eventId;
|
|
@@ -851,23 +1138,26 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
851
1138
|
}
|
|
852
1139
|
const date = TimeUuid.fromString(stateEventId).getDate();
|
|
853
1140
|
const timeBucket = format(startOfHour(utc(date)), "yyyyMMddHH0000");
|
|
854
|
-
return this.stateManager.setState
|
|
1141
|
+
return this.replayObserver.observeCheckpoint(() => stateManager.setState({ timeBucket, eventId: stateEventId }));
|
|
855
1142
|
}
|
|
856
1143
|
async loop() {
|
|
857
1144
|
do {
|
|
1145
|
+
this.ensureProcessLoop();
|
|
858
1146
|
const amountToFetch = this.options.bufferSize - this.buffer.length;
|
|
859
1147
|
if (amountToFetch <= 0) {
|
|
860
1148
|
this.logger?.info("Buffer is full, waiting for space");
|
|
861
|
-
await this.waitForBufferThreshold();
|
|
1149
|
+
await this.replayObserver.observeIdle("buffer_full", () => this.waitForBufferThreshold());
|
|
862
1150
|
continue;
|
|
863
1151
|
}
|
|
864
1152
|
this.logger?.debug(`fetching ${amountToFetch} events from ${this.bufferState.timeBucket}(${this.nextCursor})`);
|
|
865
|
-
const { events, nextCursor } = await this.
|
|
866
|
-
this.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1153
|
+
const { events, nextCursor } = await this.replayObserver.observeFetch(
|
|
1154
|
+
() => this.dataSource.getEvents(
|
|
1155
|
+
this.bufferState,
|
|
1156
|
+
amountToFetch,
|
|
1157
|
+
this.stopAtState?.eventId,
|
|
1158
|
+
this.nextCursor,
|
|
1159
|
+
this.options.includeSensitiveData
|
|
1160
|
+
)
|
|
871
1161
|
);
|
|
872
1162
|
if (!this.running) {
|
|
873
1163
|
break;
|
|
@@ -875,10 +1165,10 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
875
1165
|
this.logger?.debug(`fetched ${events.length} events`);
|
|
876
1166
|
this.pulledCount += events.length;
|
|
877
1167
|
this.mainLoopRestartAttempts = 0;
|
|
878
|
-
this.
|
|
1168
|
+
this.addEventsToBuffer(events);
|
|
879
1169
|
this.nextCursor = nextCursor;
|
|
880
1170
|
this.updateMetricsGauges();
|
|
881
|
-
events.length && this.
|
|
1171
|
+
events.length && this.notifyEventWaiters();
|
|
882
1172
|
this.bufferState.eventId = events[events.length - 1]?.eventId ?? this.bufferState.eventId;
|
|
883
1173
|
if (this.stopAtState?.timeBucket && this.bufferState.timeBucket >= this.stopAtState.timeBucket && !events.length) {
|
|
884
1174
|
this.logger?.info("Stopping at stopAt state");
|
|
@@ -900,9 +1190,12 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
900
1190
|
this.isLive = true;
|
|
901
1191
|
this.logger?.debug("Going live...");
|
|
902
1192
|
this.abortController = new AbortController();
|
|
903
|
-
await this.notifier.wait(this.abortController.signal);
|
|
1193
|
+
await this.replayObserver.observeIdle("no_events", () => this.notifier.wait(this.abortController.signal));
|
|
904
1194
|
} else if (this.isLive) {
|
|
905
|
-
await
|
|
1195
|
+
await this.replayObserver.observeIdle(
|
|
1196
|
+
"no_events",
|
|
1197
|
+
() => new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
1198
|
+
);
|
|
906
1199
|
}
|
|
907
1200
|
}
|
|
908
1201
|
}
|
|
@@ -915,18 +1208,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
915
1208
|
this.bufferState = this.restartTo;
|
|
916
1209
|
this.restartTo = void 0;
|
|
917
1210
|
this.running = true;
|
|
1211
|
+
this.ensureProcessLoop();
|
|
1212
|
+
this.pulseEmitter?.start();
|
|
918
1213
|
return this.loop();
|
|
919
1214
|
} catch (error) {
|
|
920
1215
|
this.logger?.error("Failed to consume restartTo, dropping it", { error });
|
|
921
1216
|
this.restartTo = void 0;
|
|
1217
|
+
this.notifyEventWaiters();
|
|
922
1218
|
return;
|
|
923
1219
|
}
|
|
924
1220
|
}
|
|
925
1221
|
this.logger?.info("Data pump stopped");
|
|
926
1222
|
}
|
|
927
1223
|
// #region Puller
|
|
928
|
-
|
|
929
|
-
|
|
1224
|
+
reserve(amount) {
|
|
1225
|
+
return this.reserveInternal(amount);
|
|
1226
|
+
}
|
|
1227
|
+
async reserveInternal(amount, generation) {
|
|
1228
|
+
if (!this.running || generation !== void 0 && generation !== this.processLoopGeneration) {
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
if (generation !== void 0 && this.paused) {
|
|
930
1232
|
return [];
|
|
931
1233
|
}
|
|
932
1234
|
const events = [];
|
|
@@ -936,23 +1238,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
936
1238
|
event.status = "reserved";
|
|
937
1239
|
event.deliveryId = deliveryId;
|
|
938
1240
|
event.deliveryCount++;
|
|
1241
|
+
this.updateReservedStats(event, 1);
|
|
939
1242
|
events.push(event.event);
|
|
940
|
-
|
|
1243
|
+
event.eventSizeBytes ??= textEncoder.encode(JSON.stringify(event.event)).byteLength;
|
|
1244
|
+
this.incMetricsCounter("pulled", event.event.eventType, event.eventSizeBytes);
|
|
941
1245
|
if (events.length === amount) {
|
|
942
1246
|
break;
|
|
943
1247
|
}
|
|
944
1248
|
}
|
|
945
1249
|
}
|
|
946
1250
|
if (!events.length) {
|
|
947
|
-
await this.waitForEvents();
|
|
948
|
-
return this.
|
|
1251
|
+
await this.waitForEvents(generation);
|
|
1252
|
+
return this.reserveInternal(amount, generation);
|
|
949
1253
|
}
|
|
950
1254
|
this.updateMetricsGauges();
|
|
951
1255
|
setTimeout(() => {
|
|
952
|
-
this.reOpen(
|
|
1256
|
+
void this.reOpen(
|
|
953
1257
|
events.map((event) => event.eventId),
|
|
954
1258
|
deliveryId
|
|
955
|
-
)
|
|
1259
|
+
).catch((error) => {
|
|
1260
|
+
this.logger?.error("Failed to reopen events after acknowledgement timeout", { error });
|
|
1261
|
+
});
|
|
956
1262
|
}, this.options.achknowledgeTimeoutMs);
|
|
957
1263
|
return events;
|
|
958
1264
|
}
|
|
@@ -963,22 +1269,28 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
963
1269
|
if (!this.running) {
|
|
964
1270
|
return;
|
|
965
1271
|
}
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1272
|
+
const eventIdSet = new Set(eventIds);
|
|
1273
|
+
const checkpointEventId = this.replayObserver.observeAcknowledgement(() => {
|
|
1274
|
+
const lastEventInBuffer = this.buffer[this.buffer.length - 1];
|
|
1275
|
+
this.buffer = this.buffer.filter((event) => {
|
|
1276
|
+
if (eventIdSet.has(event.event.eventId)) {
|
|
1277
|
+
this.incMetricsCounter("acknowledged", event.event.eventType, 1);
|
|
1278
|
+
this.acknowledgedCount++;
|
|
1279
|
+
this.removeFromBufferStats(event);
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
return true;
|
|
1283
|
+
});
|
|
1284
|
+
if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
|
|
1285
|
+
this.waiterBufferThreshold?.();
|
|
972
1286
|
}
|
|
973
|
-
return
|
|
1287
|
+
return this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId;
|
|
974
1288
|
});
|
|
975
|
-
if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
|
|
976
|
-
this.waiterBufferThreshold?.();
|
|
977
|
-
}
|
|
978
|
-
await this.updateState(this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId);
|
|
979
1289
|
this.updateMetricsGauges();
|
|
980
|
-
|
|
981
|
-
this.
|
|
1290
|
+
try {
|
|
1291
|
+
await this.updateState(checkpointEventId);
|
|
1292
|
+
} finally {
|
|
1293
|
+
this.notifyBufferEmpty();
|
|
982
1294
|
}
|
|
983
1295
|
}
|
|
984
1296
|
async fail(eventIds) {
|
|
@@ -986,84 +1298,140 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
986
1298
|
return;
|
|
987
1299
|
}
|
|
988
1300
|
const lastEventInBuffer = this.buffer[this.buffer.length - 1];
|
|
1301
|
+
const eventIdSet = new Set(eventIds);
|
|
989
1302
|
const failedEvents = [];
|
|
990
1303
|
this.buffer = this.buffer.filter((event) => {
|
|
991
|
-
if (
|
|
1304
|
+
if (eventIdSet.has(event.event.eventId)) {
|
|
992
1305
|
this.incMetricsCounter("failed", event.event.eventType, 1);
|
|
993
1306
|
this.failedCount++;
|
|
994
1307
|
failedEvents.push(event.event);
|
|
1308
|
+
this.removeFromBufferStats(event);
|
|
995
1309
|
return false;
|
|
996
1310
|
}
|
|
997
1311
|
return true;
|
|
998
1312
|
});
|
|
999
1313
|
this.logger?.info(`Failed ${failedEvents.length} events`);
|
|
1000
|
-
void this.options.processor?.failedHandler?.(failedEvents);
|
|
1001
1314
|
if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
|
|
1002
1315
|
this.waiterBufferThreshold?.();
|
|
1003
1316
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
this.
|
|
1317
|
+
this.updateMetricsGauges();
|
|
1318
|
+
try {
|
|
1319
|
+
await this.options.processor?.failedHandler?.(failedEvents);
|
|
1320
|
+
await this.updateState(this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId);
|
|
1321
|
+
} finally {
|
|
1322
|
+
this.notifyBufferEmpty();
|
|
1007
1323
|
}
|
|
1008
1324
|
}
|
|
1009
1325
|
async reOpen(eventIds, deliveryId) {
|
|
1326
|
+
const eventIdSet = new Set(eventIds);
|
|
1010
1327
|
let lastEvent;
|
|
1011
1328
|
const failedEvents = [];
|
|
1012
1329
|
const reopenedEvents = [];
|
|
1013
1330
|
this.buffer = this.buffer.filter((event) => {
|
|
1014
|
-
if (event.deliveryId !== deliveryId || !
|
|
1331
|
+
if (event.deliveryId !== deliveryId || !eventIdSet.has(event.event.eventId)) {
|
|
1015
1332
|
return true;
|
|
1016
1333
|
}
|
|
1017
1334
|
if (this.options.maxRedeliveryCount > -1 && event.deliveryCount > this.options.maxRedeliveryCount) {
|
|
1018
1335
|
this.incMetricsCounter("failed", event.event.eventType, 1);
|
|
1336
|
+
this.failedCount++;
|
|
1019
1337
|
failedEvents.push(event.event);
|
|
1020
1338
|
lastEvent = event.event;
|
|
1339
|
+
this.removeFromBufferStats(event);
|
|
1021
1340
|
return false;
|
|
1022
1341
|
}
|
|
1023
1342
|
event.status = "open";
|
|
1024
1343
|
event.deliveryId = void 0;
|
|
1344
|
+
this.updateReservedStats(event, -1);
|
|
1025
1345
|
reopenedEvents.push(event.event);
|
|
1026
1346
|
return true;
|
|
1027
1347
|
});
|
|
1028
1348
|
this.updateMetricsGauges();
|
|
1029
1349
|
if (reopenedEvents.length) {
|
|
1030
1350
|
this.logger?.info(`Reopened ${reopenedEvents.length} events`);
|
|
1031
|
-
|
|
1351
|
+
this.notifyEventWaiters();
|
|
1032
1352
|
}
|
|
1033
1353
|
if (!failedEvents.length) {
|
|
1034
1354
|
return;
|
|
1035
1355
|
}
|
|
1036
1356
|
this.logger?.info(`Failed ${failedEvents.length} events`);
|
|
1037
|
-
void this.options.processor?.failedHandler?.(failedEvents);
|
|
1038
|
-
void this.finallyFailedHandler?.(failedEvents);
|
|
1039
1357
|
if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
|
|
1040
1358
|
this.waiterBufferThreshold?.();
|
|
1041
1359
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1360
|
+
try {
|
|
1361
|
+
const callbackResults = await Promise.allSettled([
|
|
1362
|
+
Promise.resolve().then(() => this.options.processor?.failedHandler?.(failedEvents)),
|
|
1363
|
+
Promise.resolve().then(() => this.finallyFailedHandler?.(failedEvents))
|
|
1364
|
+
]);
|
|
1365
|
+
const callbackFailure = callbackResults.find(
|
|
1366
|
+
(result) => result.status === "rejected"
|
|
1367
|
+
);
|
|
1368
|
+
if (callbackFailure) {
|
|
1369
|
+
throw callbackFailure.reason;
|
|
1370
|
+
}
|
|
1371
|
+
await this.updateState(this.buffer.length ? void 0 : lastEvent?.eventId);
|
|
1372
|
+
} finally {
|
|
1373
|
+
this.notifyBufferEmpty();
|
|
1045
1374
|
}
|
|
1046
1375
|
}
|
|
1047
1376
|
// #endregion
|
|
1048
1377
|
// #region Pusher
|
|
1378
|
+
/**
|
|
1379
|
+
* Guarantee a live process loop whenever the pump is running with a
|
|
1380
|
+
* processor. Called from the fetch loop, so a delivery loop that exited for
|
|
1381
|
+
* any reason — most importantly a restart, which clears `running` while the
|
|
1382
|
+
* loop is mid-batch — comes back within one fetch iteration instead of
|
|
1383
|
+
* leaving a pump that pulls but never delivers.
|
|
1384
|
+
*/
|
|
1385
|
+
ensureProcessLoop() {
|
|
1386
|
+
if (!this.options.processor || !this.running || this.paused || this.activeProcessLoopGeneration === this.processLoopGeneration || this.processLoopBackoffGeneration === this.processLoopGeneration) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
this.startProcessLoop();
|
|
1390
|
+
}
|
|
1049
1391
|
startProcessLoop() {
|
|
1050
|
-
this.
|
|
1392
|
+
const generation = this.processLoopGeneration;
|
|
1393
|
+
if (!this.options.processor || this.paused || !this.isCurrentProcessLoop(generation) || this.activeProcessLoopGeneration === generation || this.processLoopBackoffGeneration === generation) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
this.activeProcessLoopGeneration = generation;
|
|
1397
|
+
this.processLoop(generation).then(() => {
|
|
1398
|
+
if (this.activeProcessLoopGeneration !== generation) return;
|
|
1399
|
+
this.activeProcessLoopGeneration = void 0;
|
|
1400
|
+
if (this.isCurrentProcessLoop(generation)) {
|
|
1401
|
+
this.startProcessLoop();
|
|
1402
|
+
}
|
|
1403
|
+
}).catch((error) => {
|
|
1404
|
+
if (this.activeProcessLoopGeneration !== generation) return;
|
|
1405
|
+
this.activeProcessLoopGeneration = void 0;
|
|
1051
1406
|
this.logger?.error("Error in processor", { error });
|
|
1052
|
-
if (!this.
|
|
1407
|
+
if (!this.isCurrentProcessLoop(generation)) return;
|
|
1053
1408
|
this.processLoopRestartAttempts++;
|
|
1054
1409
|
const delay = Math.min(1e3 * Math.pow(2, this.processLoopRestartAttempts - 1), 3e4);
|
|
1055
1410
|
this.logger?.warn(`Restarting process loop in ${delay}ms (attempt ${this.processLoopRestartAttempts})`);
|
|
1056
|
-
|
|
1057
|
-
|
|
1411
|
+
this.processLoopBackoffGeneration = generation;
|
|
1412
|
+
this.processLoopRestartTimer = setTimeout(() => {
|
|
1413
|
+
if (this.processLoopBackoffGeneration !== generation) return;
|
|
1414
|
+
this.processLoopBackoffGeneration = void 0;
|
|
1415
|
+
this.processLoopRestartTimer = void 0;
|
|
1416
|
+
if (!this.isCurrentProcessLoop(generation)) return;
|
|
1058
1417
|
this.startProcessLoop();
|
|
1059
1418
|
}, delay);
|
|
1060
1419
|
});
|
|
1061
1420
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1421
|
+
isCurrentProcessLoop(generation) {
|
|
1422
|
+
return this.running && generation === this.processLoopGeneration;
|
|
1423
|
+
}
|
|
1424
|
+
async processLoop(generation) {
|
|
1425
|
+
while (this.isCurrentProcessLoop(generation)) {
|
|
1426
|
+
if (this.paused) return;
|
|
1064
1427
|
try {
|
|
1065
|
-
const events = await this.
|
|
1066
|
-
|
|
1428
|
+
const events = await this.reserveInternal(this.options.processor?.concurrency ?? 1, generation);
|
|
1429
|
+
if (!this.isCurrentProcessLoop(generation)) return;
|
|
1430
|
+
if (!events.length) continue;
|
|
1431
|
+
await this.replayObserver.observeHandler(events, async () => {
|
|
1432
|
+
await this.options.processor?.handler(events);
|
|
1433
|
+
});
|
|
1434
|
+
if (!this.isCurrentProcessLoop(generation)) return;
|
|
1067
1435
|
await this.acknowledge(events.map((event) => event.eventId));
|
|
1068
1436
|
this.processLoopRestartAttempts = 0;
|
|
1069
1437
|
} catch (error) {
|
|
@@ -1074,54 +1442,71 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
1074
1442
|
}
|
|
1075
1443
|
// #endregion
|
|
1076
1444
|
// #region Metrics
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const stat =
|
|
1088
|
-
if (
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
stat.eventCount++;
|
|
1092
|
-
if (item.status === "reserved") {
|
|
1093
|
-
stat.eventReservedCount++;
|
|
1445
|
+
addEventsToBuffer(events) {
|
|
1446
|
+
for (const event of events) {
|
|
1447
|
+
const item = {
|
|
1448
|
+
event,
|
|
1449
|
+
status: "open",
|
|
1450
|
+
deliveryCount: 0,
|
|
1451
|
+
payloadSizeBytes: textEncoder.encode(JSON.stringify(event.payload)).byteLength
|
|
1452
|
+
};
|
|
1453
|
+
this.buffer.push(item);
|
|
1454
|
+
this.bufferSizeBytes += item.payloadSizeBytes;
|
|
1455
|
+
const stat = this.bufferStats.get(event.eventType);
|
|
1456
|
+
if (stat) {
|
|
1457
|
+
stat.eventCount++;
|
|
1458
|
+
stat.eventSizeBytes += item.payloadSizeBytes;
|
|
1094
1459
|
}
|
|
1095
|
-
stat.eventSizeBytes += JSON.stringify(item.event.payload).length;
|
|
1096
1460
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
);
|
|
1461
|
+
}
|
|
1462
|
+
updateReservedStats(item, delta) {
|
|
1463
|
+
this.bufferReservedCount += delta;
|
|
1464
|
+
const stat = this.bufferStats.get(item.event.eventType);
|
|
1465
|
+
if (stat) stat.eventReservedCount += delta;
|
|
1466
|
+
}
|
|
1467
|
+
removeFromBufferStats(item) {
|
|
1468
|
+
this.bufferSizeBytes -= item.payloadSizeBytes;
|
|
1469
|
+
const stat = this.bufferStats.get(item.event.eventType);
|
|
1470
|
+
if (stat) {
|
|
1471
|
+
stat.eventCount--;
|
|
1472
|
+
stat.eventSizeBytes -= item.payloadSizeBytes;
|
|
1473
|
+
}
|
|
1474
|
+
if (item.status === "reserved") this.updateReservedStats(item, -1);
|
|
1475
|
+
}
|
|
1476
|
+
resetBufferStats() {
|
|
1477
|
+
this.bufferReservedCount = 0;
|
|
1478
|
+
this.bufferSizeBytes = 0;
|
|
1479
|
+
for (const stat of this.bufferStats.values()) {
|
|
1480
|
+
stat.eventCount = 0;
|
|
1481
|
+
stat.eventReservedCount = 0;
|
|
1482
|
+
stat.eventSizeBytes = 0;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
updateMetricsGauges(synchronous = false) {
|
|
1486
|
+
if (synchronous) {
|
|
1487
|
+
this.gaugePublicationScheduled = false;
|
|
1488
|
+
this.publishMetricsGauges();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (this.gaugePublicationScheduled) return;
|
|
1492
|
+
this.gaugePublicationScheduled = true;
|
|
1493
|
+
queueMicrotask(() => {
|
|
1494
|
+
if (!this.gaugePublicationScheduled) return;
|
|
1495
|
+
this.gaugePublicationScheduled = false;
|
|
1496
|
+
this.publishMetricsGauges();
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
publishMetricsGauges() {
|
|
1500
|
+
for (const [eventType, stat] of this.bufferStats) {
|
|
1501
|
+
const labels = {
|
|
1502
|
+
tenant: this.dataSource.tenant,
|
|
1503
|
+
data_core: this.dataSource.dataCore,
|
|
1504
|
+
flow_type: this.dataSource.flowType,
|
|
1505
|
+
event_type: eventType
|
|
1506
|
+
};
|
|
1507
|
+
metrics.bufferEventCountGauge.set(labels, stat.eventCount);
|
|
1508
|
+
metrics.bufferReservedEventCountGauge.set(labels, stat.eventReservedCount);
|
|
1509
|
+
metrics.bufferSizeBytesGauge.set(labels, stat.eventSizeBytes);
|
|
1125
1510
|
}
|
|
1126
1511
|
}
|
|
1127
1512
|
incMetricsCounter(name, eventType, value) {
|
|
@@ -1163,12 +1548,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
1163
1548
|
}
|
|
1164
1549
|
// #endregion
|
|
1165
1550
|
// #region Waiters
|
|
1166
|
-
|
|
1167
|
-
|
|
1551
|
+
publicEventWaiter;
|
|
1552
|
+
processEventWaiters = /* @__PURE__ */ new Map();
|
|
1553
|
+
notifyEventWaiters(includePublic = true) {
|
|
1554
|
+
if (includePublic) {
|
|
1555
|
+
const publicWaiter = this.publicEventWaiter;
|
|
1556
|
+
this.publicEventWaiter = void 0;
|
|
1557
|
+
publicWaiter?.();
|
|
1558
|
+
}
|
|
1559
|
+
const processWaiters = [...this.processEventWaiters.values()];
|
|
1560
|
+
this.processEventWaiters.clear();
|
|
1561
|
+
for (const waiter of processWaiters) waiter();
|
|
1562
|
+
}
|
|
1563
|
+
async waitForEvents(generation) {
|
|
1168
1564
|
const promise = new Promise((resolve) => {
|
|
1169
|
-
|
|
1565
|
+
if (generation === void 0) {
|
|
1566
|
+
this.publicEventWaiter = resolve;
|
|
1567
|
+
} else {
|
|
1568
|
+
this.processEventWaiters.set(generation, resolve);
|
|
1569
|
+
}
|
|
1170
1570
|
});
|
|
1171
|
-
await promise;
|
|
1571
|
+
await this.replayObserver.observeIdle("waiting_for_events", () => promise);
|
|
1172
1572
|
}
|
|
1173
1573
|
waiterBufferThreshold;
|
|
1174
1574
|
async waitForBufferThreshold() {
|
|
@@ -1178,6 +1578,11 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
|
|
|
1178
1578
|
await promise;
|
|
1179
1579
|
}
|
|
1180
1580
|
waiterBufferEmpty;
|
|
1581
|
+
notifyBufferEmpty() {
|
|
1582
|
+
if (!this.buffer.length) {
|
|
1583
|
+
this.waiterBufferEmpty?.();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1181
1586
|
async waitForBufferEmpty() {
|
|
1182
1587
|
if (!this.buffer.length) {
|
|
1183
1588
|
return;
|
|
@@ -1429,6 +1834,7 @@ var FlowcoreDataPumpCluster = class {
|
|
|
1429
1834
|
this.leaseRenewIntervalMs = options.leaseRenewIntervalMs ?? DEFAULT_LEASE_RENEW_INTERVAL_MS;
|
|
1430
1835
|
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
1431
1836
|
this.workerConcurrency = options.workerConcurrency ?? DEFAULT_WORKER_CONCURRENCY;
|
|
1837
|
+
this.paused = options.paused ?? false;
|
|
1432
1838
|
this.logger = options.logger;
|
|
1433
1839
|
this.workerHandler = options.processor?.handler;
|
|
1434
1840
|
this.workerFailedHandler = options.processor?.failedHandler;
|
|
@@ -1443,6 +1849,10 @@ var FlowcoreDataPumpCluster = class {
|
|
|
1443
1849
|
logger;
|
|
1444
1850
|
running = false;
|
|
1445
1851
|
isLeader = false;
|
|
1852
|
+
// Delivery pause held on the CLUSTER, not on the pump. A leader change destroys the
|
|
1853
|
+
// pump instance and builds a new one, so a pause stored on the pump would silently
|
|
1854
|
+
// evaporate on failover and delivery would resume with no operator action.
|
|
1855
|
+
paused;
|
|
1446
1856
|
pump;
|
|
1447
1857
|
leaderConnection;
|
|
1448
1858
|
// leader state
|
|
@@ -1546,6 +1956,33 @@ var FlowcoreDataPumpCluster = class {
|
|
|
1546
1956
|
this.startHeartbeat();
|
|
1547
1957
|
this.startElectionLoop();
|
|
1548
1958
|
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Whether delivery is paused across the cluster.
|
|
1961
|
+
* The flag lives on the cluster, so it survives a leader change.
|
|
1962
|
+
*/
|
|
1963
|
+
get isPaused() {
|
|
1964
|
+
return this.paused;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Pause delivery. A leader applies it to its pump immediately. A follower records it,
|
|
1968
|
+
* so the pause is re-applied if this instance later becomes leader.
|
|
1969
|
+
*
|
|
1970
|
+
* The flag is in-memory. It does NOT survive a process restart or a full rolling
|
|
1971
|
+
* deploy — persist it in the coordinator or the control plane if you need that.
|
|
1972
|
+
*/
|
|
1973
|
+
pause() {
|
|
1974
|
+
if (this.paused) return;
|
|
1975
|
+
this.paused = true;
|
|
1976
|
+
this.pump?.pause();
|
|
1977
|
+
this.logger?.info("Cluster delivery paused", { instanceId: this.instanceId, isLeader: this.isLeader });
|
|
1978
|
+
}
|
|
1979
|
+
/** Resume delivery from the position where {@link pause} stopped it. */
|
|
1980
|
+
resume() {
|
|
1981
|
+
if (!this.paused) return;
|
|
1982
|
+
this.paused = false;
|
|
1983
|
+
this.pump?.resume();
|
|
1984
|
+
this.logger?.info("Cluster delivery resumed", { instanceId: this.instanceId, isLeader: this.isLeader });
|
|
1985
|
+
}
|
|
1549
1986
|
async stop() {
|
|
1550
1987
|
if (!this.running) return;
|
|
1551
1988
|
this.running = false;
|
|
@@ -1668,6 +2105,10 @@ var FlowcoreDataPumpCluster = class {
|
|
|
1668
2105
|
startPumpAsLeader() {
|
|
1669
2106
|
const pumpOptions = {
|
|
1670
2107
|
...this.options,
|
|
2108
|
+
// Build the pump already paused. Pausing it after `start()` would let the new
|
|
2109
|
+
// leader deliver events in the gap, which is exactly what the operator asked
|
|
2110
|
+
// us not to do.
|
|
2111
|
+
paused: this.paused,
|
|
1671
2112
|
processor: {
|
|
1672
2113
|
concurrency: this.workerConcurrency,
|
|
1673
2114
|
handler: async (events) => {
|
|
@@ -1678,6 +2119,9 @@ var FlowcoreDataPumpCluster = class {
|
|
|
1678
2119
|
}
|
|
1679
2120
|
};
|
|
1680
2121
|
this.pump = FlowcoreDataPump.create(pumpOptions, this.options.dataSourceOverride);
|
|
2122
|
+
if (this.paused) {
|
|
2123
|
+
this.logger?.info("Leader pump started paused", { instanceId: this.instanceId });
|
|
2124
|
+
}
|
|
1681
2125
|
this.pump.start().catch((error) => {
|
|
1682
2126
|
this.logger?.error("Pump error in leader mode", { error });
|
|
1683
2127
|
if (!this.running || !this.isLeader) return;
|
|
@@ -1902,11 +2346,20 @@ export {
|
|
|
1902
2346
|
NatsDistributionLeader,
|
|
1903
2347
|
NatsDistributionWorker,
|
|
1904
2348
|
PulseEmitter,
|
|
2349
|
+
REPLAY_BATCH_SIZE_BUCKETS,
|
|
2350
|
+
REPLAY_DURATION_BUCKETS_SECONDS,
|
|
2351
|
+
REPLAY_EVENT_COUNT_BUCKETS,
|
|
2352
|
+
REPLAY_FETCH_RESULTS,
|
|
2353
|
+
REPLAY_IDLE_REASONS,
|
|
2354
|
+
REPLAY_STAGE_RESULTS,
|
|
2355
|
+
ReplayStageObserver,
|
|
1905
2356
|
WsConnection,
|
|
1906
2357
|
clusterMetrics,
|
|
2358
|
+
createDataPumpMetrics,
|
|
1907
2359
|
dataPumpPromRegistry,
|
|
1908
2360
|
deserializeMessage,
|
|
1909
2361
|
metrics,
|
|
1910
2362
|
noOpLogger,
|
|
2363
|
+
replayBatchSizeBucket,
|
|
1911
2364
|
serializeMessage
|
|
1912
2365
|
};
|