kubernetes-fluent-client 2.6.5 → 3.0.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/fluent/watch.d.ts +12 -6
- package/dist/fluent/watch.d.ts.map +1 -1
- package/dist/fluent/watch.js +32 -12
- package/dist/fluent/watch.spec.js +10 -10
- package/package.json +1 -1
- package/src/fluent/watch.spec.ts +10 -10
- package/src/fluent/watch.ts +44 -18
package/dist/fluent/watch.d.ts
CHANGED
|
@@ -23,18 +23,24 @@ export declare enum WatchEvent {
|
|
|
23
23
|
/** Resource list operation run */
|
|
24
24
|
LIST = "list",
|
|
25
25
|
/** List operation error */
|
|
26
|
-
LIST_ERROR = "list_error"
|
|
26
|
+
LIST_ERROR = "list_error",
|
|
27
|
+
/** Cache Misses */
|
|
28
|
+
CACHE_MISS = "cache_miss",
|
|
29
|
+
/** Increment resync failure count */
|
|
30
|
+
INC_RESYNC_FAILURE_COUNT = "inc_resync_failure_count",
|
|
31
|
+
/** Initialize a relist window */
|
|
32
|
+
INIT_CACHE_MISS = "init_cache_miss"
|
|
27
33
|
}
|
|
28
34
|
/** Configuration for the watch function. */
|
|
29
35
|
export type WatchCfg = {
|
|
30
36
|
/** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
|
|
31
|
-
|
|
32
|
-
/** Seconds between each
|
|
33
|
-
|
|
37
|
+
resyncFailureMax?: number;
|
|
38
|
+
/** Seconds between each resync check. Defaults to 5. */
|
|
39
|
+
resyncDelaySec?: number;
|
|
34
40
|
/** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
|
|
35
41
|
relistIntervalSec?: number;
|
|
36
|
-
/**
|
|
37
|
-
|
|
42
|
+
/** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
|
|
43
|
+
lastSeenLimitSeconds?: number;
|
|
38
44
|
};
|
|
39
45
|
/** A wrapper around the Kubernetes watch API. */
|
|
40
46
|
export declare class Watcher<T extends GenericClass> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D,oBAAY,UAAU;IACpB,sCAAsC;IACtC,OAAO,YAAY;IACnB,2BAA2B;IAC3B,aAAa,kBAAkB;IAC/B,kDAAkD;IAClD,UAAU,eAAe;IACzB,0BAA0B;IAC1B,SAAS,cAAc;IACvB,8BAA8B;IAC9B,OAAO,YAAY;IACnB,sBAAsB;IACtB,KAAK,UAAU;IACf,mCAAmC;IACnC,IAAI,SAAS;IACb,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;IACvC,kCAAkC;IAClC,IAAI,SAAS;IACb,2BAA2B;IAC3B,UAAU,eAAe;
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D,oBAAY,UAAU;IACpB,sCAAsC;IACtC,OAAO,YAAY;IACnB,2BAA2B;IAC3B,aAAa,kBAAkB;IAC/B,kDAAkD;IAClD,UAAU,eAAe;IACzB,0BAA0B;IAC1B,SAAS,cAAc;IACvB,8BAA8B;IAC9B,OAAO,YAAY;IACnB,sBAAsB;IACtB,KAAK,UAAU;IACf,mCAAmC;IACnC,IAAI,SAAS;IACb,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;IACvC,kCAAkC;IAClC,IAAI,SAAS;IACb,2BAA2B;IAC3B,UAAU,eAAe;IACzB,mBAAmB;IACnB,UAAU,eAAe;IACzB,qCAAqC;IACrC,wBAAwB,6BAA6B;IACrD,iCAAiC;IACjC,eAAe,oBAAoB;CACpC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,+HAA+H;IAC/H,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wHAAwH;IACxH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAyBzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAc9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA0CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAM9C,gGAAgG;IACzF,KAAK;IAOZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAiUF"}
|
package/dist/fluent/watch.js
CHANGED
|
@@ -37,6 +37,12 @@ var WatchEvent;
|
|
|
37
37
|
WatchEvent["LIST"] = "list";
|
|
38
38
|
/** List operation error */
|
|
39
39
|
WatchEvent["LIST_ERROR"] = "list_error";
|
|
40
|
+
/** Cache Misses */
|
|
41
|
+
WatchEvent["CACHE_MISS"] = "cache_miss";
|
|
42
|
+
/** Increment resync failure count */
|
|
43
|
+
WatchEvent["INC_RESYNC_FAILURE_COUNT"] = "inc_resync_failure_count";
|
|
44
|
+
/** Initialize a relist window */
|
|
45
|
+
WatchEvent["INIT_CACHE_MISS"] = "init_cache_miss";
|
|
40
46
|
})(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
|
|
41
47
|
const NONE = 50;
|
|
42
48
|
const OVERRIDE = 100;
|
|
@@ -47,13 +53,14 @@ class Watcher {
|
|
|
47
53
|
#filters;
|
|
48
54
|
#callback;
|
|
49
55
|
#watchCfg;
|
|
56
|
+
#latestRelistWindow = "";
|
|
50
57
|
// Track the last time data was received
|
|
51
58
|
#lastSeenTime = NONE;
|
|
52
59
|
#lastSeenLimit;
|
|
53
60
|
// Create a wrapped AbortController to allow the watch to be aborted externally
|
|
54
61
|
#abortController;
|
|
55
62
|
// Track the number of retries
|
|
56
|
-
#
|
|
63
|
+
#resyncFailureCount = 0;
|
|
57
64
|
// Create a stream to read the response body
|
|
58
65
|
#stream;
|
|
59
66
|
// Create an EventEmitter to emit events
|
|
@@ -82,19 +89,25 @@ class Watcher {
|
|
|
82
89
|
*/
|
|
83
90
|
constructor(model, filters, callback, watchCfg = {}) {
|
|
84
91
|
// Set the retry delay to 5 seconds if not specified
|
|
85
|
-
watchCfg.
|
|
92
|
+
watchCfg.resyncDelaySec ??= 5;
|
|
86
93
|
// Set the relist interval to 30 minutes if not specified
|
|
87
94
|
watchCfg.relistIntervalSec ??= 1800;
|
|
88
95
|
// Set the resync interval to 10 minutes if not specified
|
|
89
|
-
watchCfg.
|
|
96
|
+
watchCfg.lastSeenLimitSeconds ??= 600;
|
|
90
97
|
// Set the last seen limit to the resync interval
|
|
91
|
-
this.#lastSeenLimit = watchCfg.
|
|
98
|
+
this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
|
|
99
|
+
// Set the latest relist interval to now
|
|
100
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
92
101
|
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
93
102
|
const jitter = Math.floor(Math.random() * 1000);
|
|
94
103
|
// Check every relist interval for cache staleness
|
|
95
|
-
this.$relistTimer = setInterval(
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
this.$relistTimer = setInterval(() => {
|
|
105
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
106
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
107
|
+
this.#list;
|
|
108
|
+
}, watchCfg.relistIntervalSec * 1000 + jitter);
|
|
109
|
+
// Rebuild the watch every resync delay interval
|
|
110
|
+
this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
|
|
98
111
|
// Bind class properties
|
|
99
112
|
this.#model = model;
|
|
100
113
|
this.#filters = filters;
|
|
@@ -109,6 +122,7 @@ class Watcher {
|
|
|
109
122
|
* @returns The AbortController for the watch.
|
|
110
123
|
*/
|
|
111
124
|
async start() {
|
|
125
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
112
126
|
await this.#watch();
|
|
113
127
|
return this.#abortController;
|
|
114
128
|
}
|
|
@@ -210,6 +224,7 @@ class Watcher {
|
|
|
210
224
|
const alreadyExists = removedItems.delete(uid);
|
|
211
225
|
// If the item does not exist, it is new and should be added
|
|
212
226
|
if (!alreadyExists) {
|
|
227
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
213
228
|
// Send added event. Use void here because we don't care about the result (no consequences here if it fails)
|
|
214
229
|
void this.#process(item, types_1.WatchPhase.Added);
|
|
215
230
|
continue;
|
|
@@ -219,6 +234,7 @@ class Watcher {
|
|
|
219
234
|
const itemRV = parseInt(item.metadata.resourceVersion);
|
|
220
235
|
// Check if the resource version is newer than the cached version
|
|
221
236
|
if (itemRV > cachedRV) {
|
|
237
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
222
238
|
// Send a modified event if the resource version has changed
|
|
223
239
|
void this.#process(item, types_1.WatchPhase.Modified);
|
|
224
240
|
}
|
|
@@ -232,6 +248,7 @@ class Watcher {
|
|
|
232
248
|
else {
|
|
233
249
|
// Otherwise, process the removed items
|
|
234
250
|
for (const item of removedItems.values()) {
|
|
251
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
235
252
|
void this.#process(item, types_1.WatchPhase.Deleted);
|
|
236
253
|
}
|
|
237
254
|
}
|
|
@@ -292,7 +309,8 @@ class Watcher {
|
|
|
292
309
|
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
293
310
|
const { body } = response;
|
|
294
311
|
// Reset the retry count
|
|
295
|
-
this.#
|
|
312
|
+
this.#resyncFailureCount = 0;
|
|
313
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
296
314
|
// Listen for events and call the callback function
|
|
297
315
|
this.#stream.on("data", async (line) => {
|
|
298
316
|
try {
|
|
@@ -348,23 +366,25 @@ class Watcher {
|
|
|
348
366
|
// Reset the last seen time to now to allow the resync to be called again in case of failure
|
|
349
367
|
this.#lastSeenTime = now;
|
|
350
368
|
// If there are more attempts, retry the watch (undefined is unlimited retries)
|
|
351
|
-
if (this.#watchCfg.
|
|
369
|
+
if (this.#watchCfg.resyncFailureMax === undefined ||
|
|
370
|
+
this.#watchCfg.resyncFailureMax > this.#resyncFailureCount) {
|
|
352
371
|
// Increment the retry count
|
|
353
|
-
this.#
|
|
372
|
+
this.#resyncFailureCount++;
|
|
373
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
354
374
|
if (this.#pendingReconnect) {
|
|
355
375
|
// wait for the connection to be re-established
|
|
356
376
|
this.#events.emit(WatchEvent.RECONNECT_PENDING);
|
|
357
377
|
}
|
|
358
378
|
else {
|
|
359
379
|
this.#pendingReconnect = true;
|
|
360
|
-
this.#events.emit(WatchEvent.RECONNECT, this.#
|
|
380
|
+
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
361
381
|
this.#streamCleanup();
|
|
362
382
|
void this.#watch();
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
else {
|
|
366
386
|
// Otherwise, call the finally function if it exists
|
|
367
|
-
this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.
|
|
387
|
+
this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`));
|
|
368
388
|
this.close();
|
|
369
389
|
}
|
|
370
390
|
}
|
|
@@ -116,13 +116,13 @@ const types_1 = require("./types");
|
|
|
116
116
|
});
|
|
117
117
|
(0, globals_1.it)("should return the cache id", () => {
|
|
118
118
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
119
|
-
|
|
119
|
+
resyncDelaySec: 1,
|
|
120
120
|
});
|
|
121
121
|
(0, globals_1.expect)(watcher.getCacheID()).toEqual("d69b75a611");
|
|
122
122
|
});
|
|
123
123
|
(0, globals_1.it)("should handle the CONNECT event", done => {
|
|
124
124
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
125
|
-
|
|
125
|
+
resyncDelaySec: 1,
|
|
126
126
|
});
|
|
127
127
|
setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
|
|
128
128
|
done();
|
|
@@ -130,7 +130,7 @@ const types_1 = require("./types");
|
|
|
130
130
|
});
|
|
131
131
|
(0, globals_1.it)("should handle the DATA event", done => {
|
|
132
132
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
133
|
-
|
|
133
|
+
resyncDelaySec: 1,
|
|
134
134
|
});
|
|
135
135
|
setupAndStartWatcher(__1.WatchEvent.DATA, (pod, phase) => {
|
|
136
136
|
(0, globals_1.expect)(pod.metadata?.name).toEqual(`pod-0`);
|
|
@@ -155,7 +155,7 @@ const types_1 = require("./types");
|
|
|
155
155
|
.query({ watch: "true", resourceVersion: "45" })
|
|
156
156
|
.replyWithError("Something bad happened");
|
|
157
157
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
158
|
-
|
|
158
|
+
resyncDelaySec: 1,
|
|
159
159
|
});
|
|
160
160
|
setupAndStartWatcher(__1.WatchEvent.NETWORK_ERROR, error => {
|
|
161
161
|
(0, globals_1.expect)(error.message).toEqual("request to http://jest-test:8080/api/v1/pods?watch=true&resourceVersion=45 failed, reason: Something bad happened");
|
|
@@ -179,7 +179,7 @@ const types_1 = require("./types");
|
|
|
179
179
|
.query({ watch: "true", resourceVersion: "65" })
|
|
180
180
|
.replyWithError("Something bad happened");
|
|
181
181
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
182
|
-
|
|
182
|
+
resyncDelaySec: 0.01,
|
|
183
183
|
});
|
|
184
184
|
setupAndStartWatcher(__1.WatchEvent.RECONNECT, count => {
|
|
185
185
|
(0, globals_1.expect)(count).toEqual(1);
|
|
@@ -188,8 +188,8 @@ const types_1 = require("./types");
|
|
|
188
188
|
});
|
|
189
189
|
(0, globals_1.it)("should perform a resync after the resync interval", done => {
|
|
190
190
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
resyncDelaySec: 0.01,
|
|
192
|
+
lastSeenLimitSeconds: 0.01,
|
|
193
193
|
});
|
|
194
194
|
setupAndStartWatcher(__1.WatchEvent.RECONNECT, count => {
|
|
195
195
|
(0, globals_1.expect)(count).toEqual(1);
|
|
@@ -213,9 +213,9 @@ const types_1 = require("./types");
|
|
|
213
213
|
.query({ watch: "true", resourceVersion: "75" })
|
|
214
214
|
.replyWithError("Something bad happened");
|
|
215
215
|
watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
resyncFailureMax: 1,
|
|
217
|
+
resyncDelaySec: 0.01,
|
|
218
|
+
lastSeenLimitSeconds: 1,
|
|
219
219
|
});
|
|
220
220
|
setupAndStartWatcher(__1.WatchEvent.GIVE_UP, error => {
|
|
221
221
|
(0, globals_1.expect)(error.message).toContain("Retry limit (1) exceeded, giving up");
|
package/package.json
CHANGED
package/src/fluent/watch.spec.ts
CHANGED
|
@@ -138,14 +138,14 @@ describe("Watcher", () => {
|
|
|
138
138
|
|
|
139
139
|
it("should return the cache id", () => {
|
|
140
140
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
141
|
-
|
|
141
|
+
resyncDelaySec: 1,
|
|
142
142
|
});
|
|
143
143
|
expect(watcher.getCacheID()).toEqual("d69b75a611");
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
it("should handle the CONNECT event", done => {
|
|
147
147
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
148
|
-
|
|
148
|
+
resyncDelaySec: 1,
|
|
149
149
|
});
|
|
150
150
|
setupAndStartWatcher(WatchEvent.CONNECT, () => {
|
|
151
151
|
done();
|
|
@@ -154,7 +154,7 @@ describe("Watcher", () => {
|
|
|
154
154
|
|
|
155
155
|
it("should handle the DATA event", done => {
|
|
156
156
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
157
|
-
|
|
157
|
+
resyncDelaySec: 1,
|
|
158
158
|
});
|
|
159
159
|
setupAndStartWatcher(WatchEvent.DATA, (pod, phase) => {
|
|
160
160
|
expect(pod.metadata?.name).toEqual(`pod-0`);
|
|
@@ -181,7 +181,7 @@ describe("Watcher", () => {
|
|
|
181
181
|
.replyWithError("Something bad happened");
|
|
182
182
|
|
|
183
183
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
184
|
-
|
|
184
|
+
resyncDelaySec: 1,
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
setupAndStartWatcher(WatchEvent.NETWORK_ERROR, error => {
|
|
@@ -210,7 +210,7 @@ describe("Watcher", () => {
|
|
|
210
210
|
.replyWithError("Something bad happened");
|
|
211
211
|
|
|
212
212
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
213
|
-
|
|
213
|
+
resyncDelaySec: 0.01,
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
setupAndStartWatcher(WatchEvent.RECONNECT, count => {
|
|
@@ -221,8 +221,8 @@ describe("Watcher", () => {
|
|
|
221
221
|
|
|
222
222
|
it("should perform a resync after the resync interval", done => {
|
|
223
223
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
resyncDelaySec: 0.01,
|
|
225
|
+
lastSeenLimitSeconds: 0.01,
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
setupAndStartWatcher(WatchEvent.RECONNECT, count => {
|
|
@@ -249,9 +249,9 @@ describe("Watcher", () => {
|
|
|
249
249
|
.replyWithError("Something bad happened");
|
|
250
250
|
|
|
251
251
|
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
resyncFailureMax: 1,
|
|
253
|
+
resyncDelaySec: 0.01,
|
|
254
|
+
lastSeenLimitSeconds: 1,
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
setupAndStartWatcher(WatchEvent.GIVE_UP, error => {
|
package/src/fluent/watch.ts
CHANGED
|
@@ -34,18 +34,24 @@ export enum WatchEvent {
|
|
|
34
34
|
LIST = "list",
|
|
35
35
|
/** List operation error */
|
|
36
36
|
LIST_ERROR = "list_error",
|
|
37
|
+
/** Cache Misses */
|
|
38
|
+
CACHE_MISS = "cache_miss",
|
|
39
|
+
/** Increment resync failure count */
|
|
40
|
+
INC_RESYNC_FAILURE_COUNT = "inc_resync_failure_count",
|
|
41
|
+
/** Initialize a relist window */
|
|
42
|
+
INIT_CACHE_MISS = "init_cache_miss",
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
/** Configuration for the watch function. */
|
|
40
46
|
export type WatchCfg = {
|
|
41
47
|
/** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
|
|
42
|
-
|
|
43
|
-
/** Seconds between each
|
|
44
|
-
|
|
48
|
+
resyncFailureMax?: number;
|
|
49
|
+
/** Seconds between each resync check. Defaults to 5. */
|
|
50
|
+
resyncDelaySec?: number;
|
|
45
51
|
/** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
|
|
46
52
|
relistIntervalSec?: number;
|
|
47
|
-
/**
|
|
48
|
-
|
|
53
|
+
/** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
|
|
54
|
+
lastSeenLimitSeconds?: number;
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
const NONE = 50;
|
|
@@ -58,6 +64,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
58
64
|
#filters: Filters;
|
|
59
65
|
#callback: WatchAction<T>;
|
|
60
66
|
#watchCfg: WatchCfg;
|
|
67
|
+
#latestRelistWindow: string = "";
|
|
61
68
|
|
|
62
69
|
// Track the last time data was received
|
|
63
70
|
#lastSeenTime = NONE;
|
|
@@ -67,7 +74,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
67
74
|
#abortController: AbortController;
|
|
68
75
|
|
|
69
76
|
// Track the number of retries
|
|
70
|
-
#
|
|
77
|
+
#resyncFailureCount = 0;
|
|
71
78
|
|
|
72
79
|
// Create a stream to read the response body
|
|
73
80
|
#stream?: byline.LineStream;
|
|
@@ -104,25 +111,35 @@ export class Watcher<T extends GenericClass> {
|
|
|
104
111
|
*/
|
|
105
112
|
constructor(model: T, filters: Filters, callback: WatchAction<T>, watchCfg: WatchCfg = {}) {
|
|
106
113
|
// Set the retry delay to 5 seconds if not specified
|
|
107
|
-
watchCfg.
|
|
114
|
+
watchCfg.resyncDelaySec ??= 5;
|
|
108
115
|
|
|
109
116
|
// Set the relist interval to 30 minutes if not specified
|
|
110
117
|
watchCfg.relistIntervalSec ??= 1800;
|
|
111
118
|
|
|
112
119
|
// Set the resync interval to 10 minutes if not specified
|
|
113
|
-
watchCfg.
|
|
120
|
+
watchCfg.lastSeenLimitSeconds ??= 600;
|
|
114
121
|
|
|
115
122
|
// Set the last seen limit to the resync interval
|
|
116
|
-
this.#lastSeenLimit = watchCfg.
|
|
123
|
+
this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
|
|
124
|
+
|
|
125
|
+
// Set the latest relist interval to now
|
|
126
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
117
127
|
|
|
118
128
|
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
119
129
|
const jitter = Math.floor(Math.random() * 1000);
|
|
120
130
|
|
|
121
131
|
// Check every relist interval for cache staleness
|
|
122
|
-
this.$relistTimer = setInterval(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
this.$relistTimer = setInterval(
|
|
133
|
+
() => {
|
|
134
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
135
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
136
|
+
this.#list;
|
|
137
|
+
},
|
|
138
|
+
watchCfg.relistIntervalSec * 1000 + jitter,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Rebuild the watch every resync delay interval
|
|
142
|
+
this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
|
|
126
143
|
|
|
127
144
|
// Bind class properties
|
|
128
145
|
this.#model = model;
|
|
@@ -140,6 +157,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
140
157
|
* @returns The AbortController for the watch.
|
|
141
158
|
*/
|
|
142
159
|
public async start(): Promise<AbortController> {
|
|
160
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
143
161
|
await this.#watch();
|
|
144
162
|
return this.#abortController;
|
|
145
163
|
}
|
|
@@ -267,6 +285,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
267
285
|
|
|
268
286
|
// If the item does not exist, it is new and should be added
|
|
269
287
|
if (!alreadyExists) {
|
|
288
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
270
289
|
// Send added event. Use void here because we don't care about the result (no consequences here if it fails)
|
|
271
290
|
void this.#process(item, WatchPhase.Added);
|
|
272
291
|
continue;
|
|
@@ -278,6 +297,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
278
297
|
|
|
279
298
|
// Check if the resource version is newer than the cached version
|
|
280
299
|
if (itemRV > cachedRV) {
|
|
300
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
281
301
|
// Send a modified event if the resource version has changed
|
|
282
302
|
void this.#process(item, WatchPhase.Modified);
|
|
283
303
|
}
|
|
@@ -291,6 +311,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
291
311
|
} else {
|
|
292
312
|
// Otherwise, process the removed items
|
|
293
313
|
for (const item of removedItems.values()) {
|
|
314
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
294
315
|
void this.#process(item, WatchPhase.Deleted);
|
|
295
316
|
}
|
|
296
317
|
}
|
|
@@ -362,7 +383,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
362
383
|
const { body } = response;
|
|
363
384
|
|
|
364
385
|
// Reset the retry count
|
|
365
|
-
this.#
|
|
386
|
+
this.#resyncFailureCount = 0;
|
|
387
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
366
388
|
|
|
367
389
|
// Listen for events and call the callback function
|
|
368
390
|
this.#stream.on("data", async line => {
|
|
@@ -430,16 +452,20 @@ export class Watcher<T extends GenericClass> {
|
|
|
430
452
|
this.#lastSeenTime = now;
|
|
431
453
|
|
|
432
454
|
// If there are more attempts, retry the watch (undefined is unlimited retries)
|
|
433
|
-
if (
|
|
455
|
+
if (
|
|
456
|
+
this.#watchCfg.resyncFailureMax === undefined ||
|
|
457
|
+
this.#watchCfg.resyncFailureMax > this.#resyncFailureCount
|
|
458
|
+
) {
|
|
434
459
|
// Increment the retry count
|
|
435
|
-
this.#
|
|
460
|
+
this.#resyncFailureCount++;
|
|
461
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
436
462
|
|
|
437
463
|
if (this.#pendingReconnect) {
|
|
438
464
|
// wait for the connection to be re-established
|
|
439
465
|
this.#events.emit(WatchEvent.RECONNECT_PENDING);
|
|
440
466
|
} else {
|
|
441
467
|
this.#pendingReconnect = true;
|
|
442
|
-
this.#events.emit(WatchEvent.RECONNECT, this.#
|
|
468
|
+
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
443
469
|
this.#streamCleanup();
|
|
444
470
|
|
|
445
471
|
void this.#watch();
|
|
@@ -448,7 +474,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
448
474
|
// Otherwise, call the finally function if it exists
|
|
449
475
|
this.#events.emit(
|
|
450
476
|
WatchEvent.GIVE_UP,
|
|
451
|
-
new Error(`Retry limit (${this.#watchCfg.
|
|
477
|
+
new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`),
|
|
452
478
|
);
|
|
453
479
|
this.close();
|
|
454
480
|
}
|