@rocicorp/zero 1.11.0-canary.35 → 1.11.0-canary.37
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/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +28 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +83 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +2 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +5 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts +4 -0
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +18 -4
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +12 -6
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +17 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +27 -7
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +124 -62
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +9 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +19 -6
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +8 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +131 -62
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +124 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +33 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +24 -0
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js +67 -0
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts +214 -0
- package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/restore-admission.js +294 -0
- package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +13 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +7 -6
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +2 -2
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.js +2 -2
- package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +39 -0
- package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.js +19 -2
- package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +22 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +41 -15
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/http-service.d.ts +6 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +14 -2
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +33 -0
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +56 -10
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +8 -0
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +22 -1
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts +74 -0
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/drain-handler.js +185 -0
- package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -0
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +66 -13
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +2 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +2 -2
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/batch-pacer.d.ts +79 -0
- package/out/zero-cache/src/services/view-syncer/batch-pacer.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/batch-pacer.js +249 -0
- package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +27 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +107 -17
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +85 -3
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +301 -20
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/push-cost.d.ts +132 -0
- package/out/zero-cache/src/services/view-syncer/push-cost.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/push-cost.js +164 -0
- package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/push-cutoff.d.ts +55 -0
- package/out/zero-cache/src/services/view-syncer/push-cutoff.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/push-cutoff.js +56 -0
- package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/query-stats.d.ts +7 -0
- package/out/zero-cache/src/services/view-syncer/query-stats.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-stats.js +20 -1
- package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +136 -5
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +246 -18
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +8 -2
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +381 -16
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/processes.d.ts +8 -0
- package/out/zero-cache/src/types/processes.d.ts.map +1 -1
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-react/src/use-query.js +1 -1
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +1 -1
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { getOrCreateCounter, getOrCreateLatencyHistogram, getOrCreateNativeHisto
|
|
|
17
17
|
import { rowIDString } from "../../types/row-key.js";
|
|
18
18
|
import "../replicator/schema/replication-state.js";
|
|
19
19
|
import { planWarningMessage } from "./plan-warnings.js";
|
|
20
|
+
import { shouldCutOffPush } from "./push-cutoff.js";
|
|
20
21
|
import { queryShape } from "./query-shape.js";
|
|
21
22
|
import { ResetPipelinesSignal } from "./snapshotter.js";
|
|
22
23
|
import { Subscription } from "../../types/subscription.js";
|
|
@@ -56,7 +57,36 @@ function shutdownBeforeInitializationError() {
|
|
|
56
57
|
* use a timer to update the ttlClock every minute.
|
|
57
58
|
*/
|
|
58
59
|
var TTL_CLOCK_INTERVAL = 6e4;
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* A slow query is typically slow for every client group that runs it, so
|
|
62
|
+
* slow hydrations are logged at most once per query shape per this window,
|
|
63
|
+
* per process. See {@link LogThrottle}.
|
|
64
|
+
*/
|
|
65
|
+
var SLOW_HYDRATION_LOG_WINDOW_MS = 5 * 6e4;
|
|
66
|
+
/** See `ViewSyncerService.#logRebuilds()`. */
|
|
67
|
+
var REBUILD_LOG_INTERVAL_MS = 6e4;
|
|
68
|
+
var REBUILD_LOG_MIN_MS = 1e3;
|
|
69
|
+
function newRebuildLog() {
|
|
70
|
+
return {
|
|
71
|
+
/** The batches that hydrated queries again. */
|
|
72
|
+
batches: 0,
|
|
73
|
+
/** How many of them were not read, which hydrates every query again. */
|
|
74
|
+
batchesNotRead: 0,
|
|
75
|
+
/** The queries that they abandoned, and why. */
|
|
76
|
+
queries: 0,
|
|
77
|
+
estimate: 0,
|
|
78
|
+
read: 0,
|
|
79
|
+
cutoff: 0,
|
|
80
|
+
/** How many of those were hydrated again, and how long that took. */
|
|
81
|
+
hydrated: 0,
|
|
82
|
+
hydrationTimeMs: 0,
|
|
83
|
+
/** How many were removed instead. */
|
|
84
|
+
timedOut: 0,
|
|
85
|
+
rejectedByCircuitBreaker: 0,
|
|
86
|
+
evictedForHydrationBudget: 0
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
var slowHydrationLogThrottle = new LogThrottle({ windowMs: SLOW_HYDRATION_LOG_WINDOW_MS });
|
|
60
90
|
var ViewSyncerService = class {
|
|
61
91
|
id;
|
|
62
92
|
createdAtMs = Date.now();
|
|
@@ -210,9 +240,65 @@ var ViewSyncerService = class {
|
|
|
210
240
|
});
|
|
211
241
|
#inspectorDelegate;
|
|
212
242
|
#config;
|
|
243
|
+
/**
|
|
244
|
+
* Set when `costBasedSyncEnabled`: advancements are then bounded by
|
|
245
|
+
* cutting off the queries this says to and hydrating them again in the
|
|
246
|
+
* same poke, instead of by the advancement time limit that resets every
|
|
247
|
+
* pipeline of the client group.
|
|
248
|
+
*/
|
|
249
|
+
#pushCutoff;
|
|
250
|
+
/**
|
|
251
|
+
* Shared by the client groups of the sync worker. Set when
|
|
252
|
+
* `costBasedSyncEnabled`.
|
|
253
|
+
*/
|
|
254
|
+
#pacer;
|
|
255
|
+
/**
|
|
256
|
+
* The time that the pipelines took for the batch being processed, if the
|
|
257
|
+
* pipelines were advanced: reading the changes, pushing them, and
|
|
258
|
+
* hydrating queries again. Unlike the processing time of the advancement,
|
|
259
|
+
* this leaves out the time spent awaiting I/O (e.g. the CVR flush), during
|
|
260
|
+
* which other client groups run.
|
|
261
|
+
*/
|
|
262
|
+
#batchTimeMs;
|
|
263
|
+
#pipelineTime = { ms: 0 };
|
|
264
|
+
/** What the client group's batches have been taking, for pacing. */
|
|
265
|
+
#smoothedBatchTimeMs;
|
|
266
|
+
/** Ends the wait between batches, if the client group is in one. */
|
|
267
|
+
#endBatchWait;
|
|
268
|
+
/** The waits between batches since the last time they were logged. */
|
|
269
|
+
#batchWaitLog = {
|
|
270
|
+
loggedAt: -Infinity,
|
|
271
|
+
waits: 0,
|
|
272
|
+
waitedMs: 0
|
|
273
|
+
};
|
|
274
|
+
/** What batches have hydrated again since that was last logged. */
|
|
275
|
+
#rebuildLog = newRebuildLog();
|
|
276
|
+
#rebuildsLoggedAt = -Infinity;
|
|
277
|
+
#queryRebuilds = getOrCreateCounter("sync", "query-rebuilds", {
|
|
278
|
+
description: "Number of queries hydrated again by an advancement instead of being pushed (all of) its changes, by reason: \"estimate\" if pushing them to it was estimated to cost more, \"read\" if reading them was, for the queries that were left to push them to, or \"cutoff\" if the pushes had taken as long as hydrating the query did",
|
|
279
|
+
unit: "{query}"
|
|
280
|
+
});
|
|
281
|
+
#queryRebuildTime = getOrCreateLatencyHistogram("sync", "query-rebuild-time", "Time to hydrate a query again in an advancement.");
|
|
282
|
+
#pushEstimateRatio = getOrCreateValueHistogram("sync", "push-estimate-ratio", {
|
|
283
|
+
description: "The estimated time to push the changes of an advancement to a query, divided by the time it took, for queries that were pushed all of the changes",
|
|
284
|
+
unit: "1",
|
|
285
|
+
bucketBoundaries: [
|
|
286
|
+
.1,
|
|
287
|
+
.25,
|
|
288
|
+
.5,
|
|
289
|
+
.8,
|
|
290
|
+
1.25,
|
|
291
|
+
2,
|
|
292
|
+
4,
|
|
293
|
+
10
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
#batchWaitTime = getOrCreateLatencyHistogram("sync", "batch-wait-time", "Time a client group is made to wait between advancements by pacing.");
|
|
213
297
|
#runPriorityOp;
|
|
214
|
-
constructor(config, lc, shard, taskID, clientGroupID, cvrDb, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, inspectorDelegate, connContextManager, customQueryTransformer, runPriorityOp, keepaliveMs = DEFAULT_KEEPALIVE_MS, setTimeoutFn = setTimeout.bind(globalThis), now = performance.now.bind(performance)) {
|
|
298
|
+
constructor(config, lc, shard, taskID, clientGroupID, cvrDb, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, inspectorDelegate, connContextManager, customQueryTransformer, runPriorityOp, keepaliveMs = DEFAULT_KEEPALIVE_MS, setTimeoutFn = setTimeout.bind(globalThis), now = performance.now.bind(performance), pushCutoff = shouldCutOffPush, pacer) {
|
|
215
299
|
this.#config = config;
|
|
300
|
+
this.#pushCutoff = config.costBasedSyncEnabled ? pushCutoff : void 0;
|
|
301
|
+
this.#pacer = config.costBasedSyncEnabled ? pacer : void 0;
|
|
216
302
|
this.id = clientGroupID;
|
|
217
303
|
this.connContextManager = connContextManager;
|
|
218
304
|
this.#shard = shard;
|
|
@@ -335,6 +421,7 @@ var ViewSyncerService = class {
|
|
|
335
421
|
}
|
|
336
422
|
assert(state === "version-ready", "state should be version-ready");
|
|
337
423
|
this.#e2eServingLagTracker.onVersionReady(replicaState);
|
|
424
|
+
this.#batchTimeMs = void 0;
|
|
338
425
|
await this.#runInLockWithCVR(async (lc, cvr) => {
|
|
339
426
|
const clientSchema = must(cvr.clientSchema, "cvr.clientSchema missing after initialization");
|
|
340
427
|
if (!this.#pipelines.initialized()) this.#pipelines.init(clientSchema);
|
|
@@ -354,7 +441,7 @@ var ViewSyncerService = class {
|
|
|
354
441
|
case "scalar-subquery":
|
|
355
442
|
case "truncation":
|
|
356
443
|
case "schema-change":
|
|
357
|
-
if (!Object.values(cvr.queries).some((q) => q.type === "client")) previousQueries = new Map(this.#pipelines.queries());
|
|
444
|
+
if (!Object.values(cvr.queries).some((q) => q.type === "client")) previousQueries = new Map([...this.#pipelines.queries(), ...this.#pipelines.abandonedQueries()]);
|
|
358
445
|
break;
|
|
359
446
|
case "permissions-change": break;
|
|
360
447
|
default: unreachable(result.reason);
|
|
@@ -366,7 +453,9 @@ var ViewSyncerService = class {
|
|
|
366
453
|
const backgroundConnCtx = this.connContextManager.getBackgroundConnectionContext();
|
|
367
454
|
if (backgroundConnCtx) await this.#maybeHydratePipelines(lc, cvr, backgroundConnCtx, previousQueries);
|
|
368
455
|
else lc.info?.("No validated background connection; deferring pipeline init");
|
|
456
|
+
if (this.#batchTimeMs !== void 0) this.#batchTimeMs += this.#totalHydrationTimeMs();
|
|
369
457
|
});
|
|
458
|
+
if (this.#pacer && this.#batchTimeMs !== void 0) await this.#paceBatches(this.#pacer, this.#batchTimeMs);
|
|
370
459
|
}
|
|
371
460
|
if (this.#drainCoordinator.shouldDrain()) this.#drainCoordinator.drainNextIn(this.#totalHydrationTimeMs());
|
|
372
461
|
await this.#cleanup();
|
|
@@ -406,7 +495,7 @@ var ViewSyncerService = class {
|
|
|
406
495
|
if (!this.#pipelines.initialized()) return;
|
|
407
496
|
let version;
|
|
408
497
|
try {
|
|
409
|
-
version = this.#pipelines.advanceWithoutDiff();
|
|
498
|
+
version = this.#pipelines.advanceWithoutDiff(this.#pushCutoff !== void 0);
|
|
410
499
|
} catch (e) {
|
|
411
500
|
if (!(e instanceof ResetPipelinesSignal)) throw e;
|
|
412
501
|
lc.info?.(`resetting pipelines: ${e.message}${e.breakdown ? ` (${e.breakdown})` : ""}`);
|
|
@@ -932,7 +1021,7 @@ var ViewSyncerService = class {
|
|
|
932
1021
|
coveredHydratedQueries++;
|
|
933
1022
|
firstCoveredQuery ??= covered;
|
|
934
1023
|
}
|
|
935
|
-
const timer =
|
|
1024
|
+
const timer = this.#timer(lc, "hydration");
|
|
936
1025
|
let count = 0;
|
|
937
1026
|
let timedOut = false;
|
|
938
1027
|
await startAsyncSpan(tracer, "vs.#hydrateUnchangedQueries.addQuery", async (span) => {
|
|
@@ -1379,7 +1468,7 @@ var ViewSyncerService = class {
|
|
|
1379
1468
|
this.#queryReplacements.delete(q.id);
|
|
1380
1469
|
}
|
|
1381
1470
|
let totalProcessTime = 0;
|
|
1382
|
-
const timer =
|
|
1471
|
+
const timer = this.#timer(lc, "hydration");
|
|
1383
1472
|
const pipelines = this.#pipelines;
|
|
1384
1473
|
const hydrations = this.#hydrations;
|
|
1385
1474
|
const hydrationTime = this.#hydrationTime;
|
|
@@ -1393,7 +1482,7 @@ var ViewSyncerService = class {
|
|
|
1393
1482
|
const rejectedQueries = [];
|
|
1394
1483
|
const circuitBreaker = this.#hydrationCircuitBreaker;
|
|
1395
1484
|
const self = this;
|
|
1396
|
-
await yieldProcess(lc);
|
|
1485
|
+
await (this.#pacer ? this.#pacer.yieldSlice("hydration") : yieldProcess(lc));
|
|
1397
1486
|
function* hydrateQueries(queries, optional, slowHydrateThreshold) {
|
|
1398
1487
|
for (let i = 0; i < queries.length; i++) {
|
|
1399
1488
|
if (optional && hydrationBudget.exhausted()) {
|
|
@@ -1634,16 +1723,19 @@ var ViewSyncerService = class {
|
|
|
1634
1723
|
span.setAttribute("clientGroupID", this.id);
|
|
1635
1724
|
assert(this.#pipelines.initialized(), "pipelines must be initialized (advancePipelines");
|
|
1636
1725
|
const start = performance.now();
|
|
1637
|
-
const timer =
|
|
1726
|
+
const timer = this.#timer(lc, "batch");
|
|
1638
1727
|
let outcome = { outcome: "error" };
|
|
1639
1728
|
let setupTimeMs;
|
|
1729
|
+
let rebuildTimeMs = 0;
|
|
1730
|
+
let evictedQueries = false;
|
|
1731
|
+
this.#pipelineTime.ms = 0;
|
|
1640
1732
|
try {
|
|
1641
1733
|
let pokers;
|
|
1642
1734
|
let updater;
|
|
1643
1735
|
let version;
|
|
1644
1736
|
let numChanges = 0;
|
|
1645
1737
|
try {
|
|
1646
|
-
const advancement = this.#pipelines.advance(timer);
|
|
1738
|
+
const advancement = this.#pipelines.advance(timer, this.#costBasedAdvance(cvr));
|
|
1647
1739
|
version = advancement.version;
|
|
1648
1740
|
numChanges = advancement.numChanges;
|
|
1649
1741
|
lc = lc.withContext("newVersion", version);
|
|
@@ -1651,10 +1743,17 @@ var ViewSyncerService = class {
|
|
|
1651
1743
|
pokers = startPoke(lc, this.#getClients(cvr.version), targetVersion);
|
|
1652
1744
|
lc.debug?.(`applying ${numChanges} to advance to ${version}`);
|
|
1653
1745
|
setupTimeMs = performance.now() - start;
|
|
1654
|
-
await this.#processChanges(lc, await timer.start(), advancement.changes, (rows) => {
|
|
1746
|
+
await this.#processChanges(lc, await timer.start(), this.#timed(advancement.changes), (rows) => {
|
|
1655
1747
|
updater ??= new CVRQueryDrivenUpdater(this.#cvrStore, cvr, version, this.#pipelines.replicaVersion, this.#pipelines);
|
|
1656
1748
|
return updater.received(lc, rows);
|
|
1657
1749
|
}, pokers);
|
|
1750
|
+
for (const { estimatedPushMs, pushTimeMs } of advancement.estimates()) if (pushTimeMs > 0) this.#pushEstimateRatio.record(estimatedPushMs / pushTimeMs);
|
|
1751
|
+
const abandoned = advancement.abandoned();
|
|
1752
|
+
if (abandoned.length > 0) {
|
|
1753
|
+
updater ??= new CVRQueryDrivenUpdater(this.#cvrStore, cvr, version, this.#pipelines.replicaVersion, this.#pipelines);
|
|
1754
|
+
timer.stop();
|
|
1755
|
+
({rebuildTimeMs, evictedQueries} = await this.#rebuildAbandonedQueries(lc, cvr, abandoned, updater, pokers));
|
|
1756
|
+
}
|
|
1658
1757
|
} catch (e) {
|
|
1659
1758
|
setupTimeMs ??= performance.now() - start;
|
|
1660
1759
|
if (e instanceof ResetPipelinesSignal) {
|
|
@@ -1669,19 +1768,23 @@ var ViewSyncerService = class {
|
|
|
1669
1768
|
throw e;
|
|
1670
1769
|
}
|
|
1671
1770
|
assert(pokers && version !== void 0, "advancement state missing");
|
|
1672
|
-
if (updater)
|
|
1771
|
+
if (updater) {
|
|
1772
|
+
this.#cvr = await this.#flushPoked(lc, updater, pokers);
|
|
1773
|
+
if (evictedQueries) this.#scheduleExpireEviction(lc, this.#cvr);
|
|
1774
|
+
}
|
|
1673
1775
|
const finalVersion = (this.#cvr ?? cvr).version;
|
|
1674
1776
|
await startAsyncSpan(tracer, "vs.#advancePipelines.pokeEnd", () => pokers.end(finalVersion));
|
|
1675
1777
|
this.#markVersionServed(version);
|
|
1676
1778
|
const wallTime = performance.now() - start;
|
|
1677
|
-
const totalProcessTime = timer.totalElapsed();
|
|
1779
|
+
const totalProcessTime = timer.totalElapsed() + rebuildTimeMs;
|
|
1678
1780
|
lc.debug?.(`finished processing advancement of ${numChanges} changes ((process: ${totalProcessTime} ms, wall: ${wallTime} ms))`);
|
|
1679
1781
|
this.#transactionAdvanceTime.recordMs(totalProcessTime);
|
|
1680
1782
|
outcome = { outcome: "success" };
|
|
1681
1783
|
return "success";
|
|
1682
1784
|
} finally {
|
|
1683
1785
|
const setupMs = must(setupTimeMs);
|
|
1684
|
-
const processingTimeMs = setupMs + timer.totalElapsed();
|
|
1786
|
+
const processingTimeMs = setupMs + timer.totalElapsed() + rebuildTimeMs;
|
|
1787
|
+
this.#batchTimeMs = setupMs + this.#pipelineTime.ms;
|
|
1685
1788
|
const wallTimeMs = performance.now() - start;
|
|
1686
1789
|
this.#advancementMetrics.record({
|
|
1687
1790
|
processingTimeMs,
|
|
@@ -1691,6 +1794,233 @@ var ViewSyncerService = class {
|
|
|
1691
1794
|
}
|
|
1692
1795
|
});
|
|
1693
1796
|
}
|
|
1797
|
+
/**
|
|
1798
|
+
* What a cost-based advancement needs, or `undefined` if advancements are
|
|
1799
|
+
* bounded by the advancement time limit.
|
|
1800
|
+
*/
|
|
1801
|
+
#costBasedAdvance(cvr) {
|
|
1802
|
+
const shouldCutOff = this.#pushCutoff;
|
|
1803
|
+
if (shouldCutOff === void 0) return;
|
|
1804
|
+
if (this.#pipelines.currentVersion() < cvr.version.stateVersion) return;
|
|
1805
|
+
return {
|
|
1806
|
+
shouldCutOff,
|
|
1807
|
+
rebuildMargin: this.#config.syncRebuildMargin ?? 2
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Hydrates the queries whose pipelines an advancement abandoned, at the
|
|
1812
|
+
* snapshot it advanced to, within the advancement's CVR update and poke.
|
|
1813
|
+
* What the queries contributed to the update before they were abandoned
|
|
1814
|
+
* is retracted first.
|
|
1815
|
+
*
|
|
1816
|
+
* The hydrations are protected as they are in a hydration pass (see
|
|
1817
|
+
* {@link #addAndRemoveQueries}). A query that exceeds the query hydration
|
|
1818
|
+
* timeout, or whose circuit breaker is open, is removed and errored to its
|
|
1819
|
+
* clients. Inactive queries are hydrated last, and those not reached
|
|
1820
|
+
* before the hydration budget is spent are evicted.
|
|
1821
|
+
*
|
|
1822
|
+
* @returns the time spent hydrating, and whether inactive queries were
|
|
1823
|
+
* evicted.
|
|
1824
|
+
*/
|
|
1825
|
+
async #rebuildAbandonedQueries(lc, cvr, abandoned, updater, pokers) {
|
|
1826
|
+
for (const patch of await updater.retractQueries(lc, abandoned.map((q) => q.queryID))) await pokers.addPatch(patch);
|
|
1827
|
+
const hydrationBudget = new HydrationBudget(this.#config.viewSyncerHydrationBudgetMs ?? 0, this.#now);
|
|
1828
|
+
const abandonedByID = new Map(abandoned.map((q) => [q.queryID, q]));
|
|
1829
|
+
const { required, optional } = classifyQueriesForHydration(abandoned.map((q) => must(cvr.queries[q.queryID])));
|
|
1830
|
+
const byRecord = (queries) => queries.map(({ id }) => must(abandonedByID.get(id)));
|
|
1831
|
+
const timer = this.#timer(lc, "batch");
|
|
1832
|
+
const pipelines = this.#pipelines;
|
|
1833
|
+
const circuitBreaker = this.#hydrationCircuitBreaker;
|
|
1834
|
+
const budgetEvicted = [];
|
|
1835
|
+
const timedOut = [];
|
|
1836
|
+
const rejected = [];
|
|
1837
|
+
const rebuilt = {
|
|
1838
|
+
active: 0,
|
|
1839
|
+
inactive: 0
|
|
1840
|
+
};
|
|
1841
|
+
const self = this;
|
|
1842
|
+
let total = 0;
|
|
1843
|
+
function* rebuild(queries, inactive) {
|
|
1844
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1845
|
+
if (inactive && hydrationBudget.exhausted()) {
|
|
1846
|
+
budgetEvicted.push(...queries.slice(i));
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const q = must(queries[i]);
|
|
1850
|
+
const hydrationQuery = {
|
|
1851
|
+
id: q.queryID,
|
|
1852
|
+
ast: q.originalAst,
|
|
1853
|
+
transformationHash: q.transformationHash,
|
|
1854
|
+
name: q.queryName
|
|
1855
|
+
};
|
|
1856
|
+
const breakable = cvr.queries[q.queryID]?.type !== "internal";
|
|
1857
|
+
if (breakable && circuitBreaker.isOpen(q.transformationHash)) {
|
|
1858
|
+
rejected.push(q);
|
|
1859
|
+
self.#recordCircuitBreakerRejection(lc, hydrationQuery);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
let aborted = false;
|
|
1863
|
+
for (const change of pipelines.addQuery(q.transformationHash, q.queryID, q.originalAst, timer.startWithoutYielding(), q.queryName, "unchanged-query-rehydrate")) {
|
|
1864
|
+
if (change === "yield" && breakable && circuitBreaker.exceeded(timer.totalElapsed())) {
|
|
1865
|
+
aborted = true;
|
|
1866
|
+
yield change;
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
yield change;
|
|
1870
|
+
}
|
|
1871
|
+
const elapsed = timer.stop();
|
|
1872
|
+
total += elapsed;
|
|
1873
|
+
if (aborted) {
|
|
1874
|
+
timedOut.push(q);
|
|
1875
|
+
self.#recordHydrationTimeout(lc, hydrationQuery, elapsed);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (inactive) rebuilt.inactive++;
|
|
1879
|
+
else rebuilt.active++;
|
|
1880
|
+
self.#queryRebuilds.add(1, { reason: q.reason });
|
|
1881
|
+
self.#queryRebuildTime.recordMs(elapsed);
|
|
1882
|
+
self.#addQueryMaterializationServerMetric(q.queryID, elapsed);
|
|
1883
|
+
if (elapsed > self.#slowHydrateThreshold) self.#logSlowHydration(lc, hydrationQuery, elapsed);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function* rebuildAll() {
|
|
1887
|
+
yield* rebuild(byRecord(required), false);
|
|
1888
|
+
yield* rebuild(byRecord(optional), true);
|
|
1889
|
+
}
|
|
1890
|
+
await this.#processChanges(lc, timer, this.#timed(rebuildAll()), (rows) => updater.received(lc, rows), pokers);
|
|
1891
|
+
const abortedQueries = [...timedOut, ...rejected];
|
|
1892
|
+
if (abortedQueries.length > 0) {
|
|
1893
|
+
for (const patch of await updater.abortExecutedQueries(lc, abortedQueries.map((q) => q.queryID))) await pokers.addPatch(patch);
|
|
1894
|
+
if (timedOut.length > 0) this.#queryEvictions.add(timedOut.length, { reason: "hydration-timeout" });
|
|
1895
|
+
if (rejected.length > 0) this.#queryEvictions.add(rejected.length, { reason: "hydration-circuit-breaker" });
|
|
1896
|
+
this.#sendHydrationTimeoutErrors(cvr, abortedQueries.map((q) => ({
|
|
1897
|
+
id: q.queryID,
|
|
1898
|
+
name: q.queryName
|
|
1899
|
+
})));
|
|
1900
|
+
}
|
|
1901
|
+
const evictedQueryIDs = budgetEvicted.map((q) => q.queryID);
|
|
1902
|
+
for (const patch of updater.removeTrackedQueries(evictedQueryIDs)) await pokers.addPatch(patch);
|
|
1903
|
+
for (const { queryID } of [...abortedQueries, ...budgetEvicted]) {
|
|
1904
|
+
this.#pipelines.removeQuery(queryID);
|
|
1905
|
+
this.#inspectorDelegate.removeQuery(queryID);
|
|
1906
|
+
this.#queryReplacements.delete(queryID);
|
|
1907
|
+
}
|
|
1908
|
+
this.#recordHydrationBudgetExhaustion(lc, hydrationBudget, rebuilt.active, rebuilt.inactive, evictedQueryIDs);
|
|
1909
|
+
for (const patch of await updater.deleteUnreferencedRows(lc)) await pokers.addPatch(patch);
|
|
1910
|
+
this.#logRebuilds(lc, abandoned, {
|
|
1911
|
+
hydrated: rebuilt.active + rebuilt.inactive,
|
|
1912
|
+
hydrationTimeMs: total,
|
|
1913
|
+
timedOut: timedOut.length,
|
|
1914
|
+
rejectedByCircuitBreaker: rejected.length,
|
|
1915
|
+
evictedForHydrationBudget: budgetEvicted.length
|
|
1916
|
+
});
|
|
1917
|
+
return {
|
|
1918
|
+
rebuildTimeMs: total,
|
|
1919
|
+
evictedQueries: evictedQueryIDs.length > 0
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Says in the log that this client group's batches hydrate queries again,
|
|
1924
|
+
* which can be every batch. Nothing else is logged at `info` for a batch,
|
|
1925
|
+
* so this is at most once a minute, for the batches since the last time,
|
|
1926
|
+
* and not before their hydrations add up to a second. Which queries they
|
|
1927
|
+
* were is in the query stats, by query shape (see `QueryStats`).
|
|
1928
|
+
*/
|
|
1929
|
+
#logRebuilds(lc, abandoned, batch) {
|
|
1930
|
+
const log = this.#rebuildLog;
|
|
1931
|
+
log.batches++;
|
|
1932
|
+
log.queries += abandoned.length;
|
|
1933
|
+
for (const { reason } of abandoned) log[reason]++;
|
|
1934
|
+
if (abandoned.some((q) => q.reason === "read")) log.batchesNotRead++;
|
|
1935
|
+
log.hydrated += batch.hydrated;
|
|
1936
|
+
log.hydrationTimeMs += batch.hydrationTimeMs;
|
|
1937
|
+
log.timedOut += batch.timedOut;
|
|
1938
|
+
log.rejectedByCircuitBreaker += batch.rejectedByCircuitBreaker;
|
|
1939
|
+
log.evictedForHydrationBudget += batch.evictedForHydrationBudget;
|
|
1940
|
+
const now = this.#now();
|
|
1941
|
+
if (log.hydrationTimeMs < REBUILD_LOG_MIN_MS || now - this.#rebuildsLoggedAt < REBUILD_LOG_INTERVAL_MS) return;
|
|
1942
|
+
lc.info?.("Client group hydrated queries again in its batches of changes instead of pushing the changes to them", {
|
|
1943
|
+
clientGroupID: this.id,
|
|
1944
|
+
...log
|
|
1945
|
+
});
|
|
1946
|
+
this.#rebuildsLoggedAt = now;
|
|
1947
|
+
this.#rebuildLog = newRebuildLog();
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Adds the time that the pipelines take to produce `values` to the time
|
|
1951
|
+
* of the batch being processed (see {@link #batchTimeMs}).
|
|
1952
|
+
*/
|
|
1953
|
+
#timed(values) {
|
|
1954
|
+
return this.#pacer ? timeIteration(values, this.#pipelineTime) : values;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* A timer for a batch, or for the hydration of new queries, which has
|
|
1958
|
+
* priority over batches for the worker's time slices (see
|
|
1959
|
+
* `BatchPacer.yieldSlice()`).
|
|
1960
|
+
*/
|
|
1961
|
+
#timer(lc, kind) {
|
|
1962
|
+
const pacer = this.#pacer;
|
|
1963
|
+
return new TimeSliceTimer(lc, pacer && (() => pacer.yieldSlice(kind)));
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Waits as long as the worker's pacer says before the next batch, after
|
|
1967
|
+
* one that took `batchTimeMs`. The wait is outside of the
|
|
1968
|
+
* lock, so connections and query changes are served as usual, at the
|
|
1969
|
+
* pipelines' current snapshot. `version-ready` notifications received in
|
|
1970
|
+
* the meantime are coalesced by the subscription, so the next batch covers
|
|
1971
|
+
* all of them.
|
|
1972
|
+
*/
|
|
1973
|
+
async #paceBatches(pacer, batchTimeMs) {
|
|
1974
|
+
this.#smoothedBatchTimeMs = this.#smoothedBatchTimeMs === void 0 ? batchTimeMs : this.#smoothedBatchTimeMs + BATCH_TIME_SMOOTHING * (batchTimeMs - this.#smoothedBatchTimeMs);
|
|
1975
|
+
const processingMs = this.#smoothedBatchTimeMs * (1 + BATCH_WAIT_JITTER * (Math.random() * 2 - 1));
|
|
1976
|
+
const start = performance.now();
|
|
1977
|
+
let waited = false;
|
|
1978
|
+
for (;;) {
|
|
1979
|
+
const remaining = pacer.waitMs(processingMs) - (performance.now() - start);
|
|
1980
|
+
if (remaining <= 0 || !this.#stateChanges.active) break;
|
|
1981
|
+
waited = true;
|
|
1982
|
+
await new Promise((resolve) => {
|
|
1983
|
+
const timer = setTimeout(resolve, Math.min(remaining, BATCH_WAIT_STEP_MS));
|
|
1984
|
+
this.#endBatchWait = () => {
|
|
1985
|
+
clearTimeout(timer);
|
|
1986
|
+
resolve();
|
|
1987
|
+
};
|
|
1988
|
+
});
|
|
1989
|
+
this.#endBatchWait = void 0;
|
|
1990
|
+
}
|
|
1991
|
+
if (waited) {
|
|
1992
|
+
const waitedMs = performance.now() - start;
|
|
1993
|
+
this.#batchWaitTime.recordMs(waitedMs);
|
|
1994
|
+
this.#logBatchWait(pacer, waitedMs);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Says in the log that this client group is being held back, which the
|
|
1999
|
+
* worker's metrics do not tell one client group from another by. A client
|
|
2000
|
+
* group that is held back is after every batch, so this is at most once
|
|
2001
|
+
* a minute, for the waits since the last time, and not for waits that add
|
|
2002
|
+
* up to less than a second.
|
|
2003
|
+
*/
|
|
2004
|
+
#logBatchWait(pacer, waitedMs) {
|
|
2005
|
+
const log = this.#batchWaitLog;
|
|
2006
|
+
log.waits++;
|
|
2007
|
+
log.waitedMs += waitedMs;
|
|
2008
|
+
const now = this.#now();
|
|
2009
|
+
if (log.waitedMs < BATCH_WAIT_LOG_MIN_MS || now - log.loggedAt < BATCH_WAIT_LOG_INTERVAL_MS) return;
|
|
2010
|
+
this.#lc.info?.("Client group is waiting between batches: its sync worker is over its share target", {
|
|
2011
|
+
clientGroupID: this.id,
|
|
2012
|
+
waits: log.waits,
|
|
2013
|
+
waitedMs: log.waitedMs,
|
|
2014
|
+
lastWaitMs: waitedMs,
|
|
2015
|
+
batchTimeMs: this.#smoothedBatchTimeMs,
|
|
2016
|
+
waitMultiplier: pacer.multiplier
|
|
2017
|
+
});
|
|
2018
|
+
this.#batchWaitLog = {
|
|
2019
|
+
loggedAt: now,
|
|
2020
|
+
waits: 0,
|
|
2021
|
+
waitedMs: 0
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
1694
2024
|
async inspect(selector, msg) {
|
|
1695
2025
|
await this.#runInLockForClient(selector, msg, this.#handleInspect);
|
|
1696
2026
|
}
|
|
@@ -1784,6 +2114,7 @@ var ViewSyncerService = class {
|
|
|
1784
2114
|
this.connContextManager.setSharedRetransformReady(false);
|
|
1785
2115
|
this.#initialized.reject(shutdownBeforeInitializationError());
|
|
1786
2116
|
this.#stateChanges.cancel();
|
|
2117
|
+
this.#endBatchWait?.();
|
|
1787
2118
|
return this.#stopped.promise;
|
|
1788
2119
|
}
|
|
1789
2120
|
async #cleanup(err) {
|
|
@@ -1854,15 +2185,49 @@ function hasExpiredQueries(cvr) {
|
|
|
1854
2185
|
for (const q of Object.values(cvr.queries)) if (expired(ttlClock, q)) return true;
|
|
1855
2186
|
return false;
|
|
1856
2187
|
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Iterates over `values`, adding the time spent producing them to
|
|
2190
|
+
* `total.ms`. The time that the consumer takes between values, including
|
|
2191
|
+
* any that it spends awaiting, is not counted.
|
|
2192
|
+
*/
|
|
2193
|
+
function* timeIteration(values, total) {
|
|
2194
|
+
const iterator = values[Symbol.iterator]();
|
|
2195
|
+
try {
|
|
2196
|
+
for (;;) {
|
|
2197
|
+
const start = performance.now();
|
|
2198
|
+
const result = iterator.next();
|
|
2199
|
+
total.ms += performance.now() - start;
|
|
2200
|
+
if (result.done) return;
|
|
2201
|
+
yield result.value;
|
|
2202
|
+
}
|
|
2203
|
+
} finally {
|
|
2204
|
+
iterator.return?.();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/** How often a client group that is waiting between batches asks again. */
|
|
2208
|
+
var BATCH_WAIT_STEP_MS = 250;
|
|
2209
|
+
/** See `ViewSyncerService.#logBatchWait()`. */
|
|
2210
|
+
var BATCH_WAIT_LOG_INTERVAL_MS = 6e4;
|
|
2211
|
+
var BATCH_WAIT_LOG_MIN_MS = 1e3;
|
|
2212
|
+
/** The weight of the latest batch in a client group's smoothed batch time. */
|
|
2213
|
+
var BATCH_TIME_SMOOTHING = .3;
|
|
2214
|
+
/** How far, as a fraction, a client group's wait is varied at random. */
|
|
2215
|
+
var BATCH_WAIT_JITTER = .3;
|
|
1857
2216
|
var TimeSliceTimer = class {
|
|
1858
2217
|
#total = 0;
|
|
1859
2218
|
#start = 0;
|
|
1860
2219
|
#lc;
|
|
1861
|
-
|
|
2220
|
+
#yieldSlice;
|
|
2221
|
+
/**
|
|
2222
|
+
* @param yieldSlice How the timer yields the process, if not by taking
|
|
2223
|
+
* turns with the rest of the worker's work (see `yieldProcess()`).
|
|
2224
|
+
*/
|
|
2225
|
+
constructor(lc, yieldSlice) {
|
|
1862
2226
|
this.#lc = lc;
|
|
2227
|
+
this.#yieldSlice = yieldSlice;
|
|
1863
2228
|
}
|
|
1864
2229
|
async start() {
|
|
1865
|
-
await yieldProcess(this.#lc);
|
|
2230
|
+
await (this.#yieldSlice ? this.#yieldSlice() : yieldProcess(this.#lc));
|
|
1866
2231
|
return this.startWithoutYielding();
|
|
1867
2232
|
}
|
|
1868
2233
|
startWithoutYielding() {
|
|
@@ -1872,7 +2237,7 @@ var TimeSliceTimer = class {
|
|
|
1872
2237
|
}
|
|
1873
2238
|
async yieldProcess(_msgForTesting) {
|
|
1874
2239
|
this.#stopLap();
|
|
1875
|
-
await yieldProcess(this.#lc);
|
|
2240
|
+
await (this.#yieldSlice ? this.#yieldSlice() : yieldProcess(this.#lc));
|
|
1876
2241
|
this.#startLap();
|
|
1877
2242
|
}
|
|
1878
2243
|
#startLap() {
|