@rocicorp/zero 1.11.0-canary.35 → 1.11.0-canary.36
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 +20 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +52 -0
- package/out/zero-cache/src/config/zero-config.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/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-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
|
@@ -22,6 +22,8 @@ import "../../types/row-key.js";
|
|
|
22
22
|
import { getSubscriptionState } from "../replicator/schema/replication-state.js";
|
|
23
23
|
import { checkClientSchema } from "./client-schema.js";
|
|
24
24
|
import { planWarningMessage } from "./plan-warnings.js";
|
|
25
|
+
import { PushCutoff } from "./push-cutoff.js";
|
|
26
|
+
import { PushCostEstimator, ReadCostEstimator, shouldRebuildInsteadOfReading, shouldStartOnRebuild } from "./push-cost.js";
|
|
25
27
|
import { queryShape } from "./query-shape.js";
|
|
26
28
|
import { rowIDSignatureUnit } from "./row-set-signature.js";
|
|
27
29
|
import { ResetPipelinesSignal } from "./snapshotter.js";
|
|
@@ -114,6 +116,22 @@ var PipelineDriver = class {
|
|
|
114
116
|
#pushedRowChanges = null;
|
|
115
117
|
#hydrateContext = null;
|
|
116
118
|
#advanceContext = null;
|
|
119
|
+
/**
|
|
120
|
+
* Whether a {@link CostBasedAdvance} has been set up and has not
|
|
121
|
+
* completed. It may already have removed pipelines, but the sources only
|
|
122
|
+
* move to the new snapshot when the iteration over its changes completes,
|
|
123
|
+
* so nothing can be hydrated until then. If the iteration is given up on
|
|
124
|
+
* or throws, they never do, and nothing can be hydrated until the
|
|
125
|
+
* pipelines are {@link reset()}.
|
|
126
|
+
*/
|
|
127
|
+
#costBasedAdvanceIncomplete = false;
|
|
128
|
+
/**
|
|
129
|
+
* The queries abandoned by a {@link CostBasedAdvance} that have not been
|
|
130
|
+
* added again since.
|
|
131
|
+
*/
|
|
132
|
+
#abandoned = /* @__PURE__ */ new Map();
|
|
133
|
+
#pushCosts = new PushCostEstimator();
|
|
134
|
+
#readCosts = new ReadCostEstimator();
|
|
117
135
|
#replicaVersion = null;
|
|
118
136
|
#primaryKeys = null;
|
|
119
137
|
#permissions = null;
|
|
@@ -175,6 +193,8 @@ var PipelineDriver = class {
|
|
|
175
193
|
this.#pipelines.delete(queryID);
|
|
176
194
|
this.#destroyPipeline(queryID, pipeline, "reset");
|
|
177
195
|
}
|
|
196
|
+
this.#abandoned.clear();
|
|
197
|
+
this.#costBasedAdvanceIncomplete = false;
|
|
178
198
|
this.#tables.clear();
|
|
179
199
|
this.#allTableNames.clear();
|
|
180
200
|
this.#rowSetSignatures.clear();
|
|
@@ -230,11 +250,16 @@ var PipelineDriver = class {
|
|
|
230
250
|
* {@link reset()} before hydrating. ({@link advance()} detects this when
|
|
231
251
|
* the diff encounters the RESET op; this path skips the diff and so must
|
|
232
252
|
* check explicitly.)
|
|
253
|
+
*
|
|
254
|
+
* @param releasePreviousSnapshot Whether to release the snapshot that the
|
|
255
|
+
* driver was at, which is otherwise left open until the next
|
|
256
|
+
* advance (see `Snapshotter.releasePrevious()`).
|
|
233
257
|
*/
|
|
234
|
-
advanceWithoutDiff() {
|
|
258
|
+
advanceWithoutDiff(releasePreviousSnapshot = false) {
|
|
235
259
|
const { prev, curr } = this.#snapshotter.advanceWithoutDiff();
|
|
236
260
|
if (curr.schemaChangedSince(prev.version)) throw new ResetPipelinesSignal(`schema changed between ${prev.version} and ${curr.version}`, "schema-change");
|
|
237
261
|
for (const table of this.#tables.values()) table.setDB(curr.db.db);
|
|
262
|
+
if (releasePreviousSnapshot) this.#snapshotter.releasePrevious();
|
|
238
263
|
return curr.version;
|
|
239
264
|
}
|
|
240
265
|
#ensureCostModelExistsIfEnabled(db) {
|
|
@@ -266,6 +291,15 @@ var PipelineDriver = class {
|
|
|
266
291
|
return this.#pipelines;
|
|
267
292
|
}
|
|
268
293
|
/**
|
|
294
|
+
* The queries whose pipelines a {@link CostBasedAdvance} removed and that
|
|
295
|
+
* have not been hydrated again since, by query ID. They are not in
|
|
296
|
+
* {@link queries()}. A query whose hydration is started but does not
|
|
297
|
+
* finish is still one of them.
|
|
298
|
+
*/
|
|
299
|
+
abandonedQueries() {
|
|
300
|
+
return this.#abandoned;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
269
303
|
* Stats from the hydration of the pipeline for `queryID`, or `undefined` if
|
|
270
304
|
* the query has no pipeline.
|
|
271
305
|
*/
|
|
@@ -327,7 +361,7 @@ var PipelineDriver = class {
|
|
|
327
361
|
for (const source of this.#tables.values()) total += source.pendingBytes;
|
|
328
362
|
return total;
|
|
329
363
|
}
|
|
330
|
-
#logQueryPipelineLifecycle({ zeroEvent, pipelineRunID, queryHash, transformationHash, queryName, hydrationReason, stopReason, hydrationTimeMs, hydrationRowCount, hydrationRowsRead, pipelineLifetimeMs }) {
|
|
364
|
+
#logQueryPipelineLifecycle({ zeroEvent, pipelineRunID, queryHash, transformationHash, queryName, hydrationReason, stopReason, hydrationTimeMs, hydrationRowCount, hydrationRowsRead, pipelineLifetimeMs, abandonReason, estimatedPushMs, pushTimeMs, pushedChanges, level = "info" }) {
|
|
331
365
|
let lc = this.#lc.withContext("zeroEvent", zeroEvent).withContext("pipelineRunID", pipelineRunID).withContext("queryHash", queryHash).withContext("transformationHash", transformationHash);
|
|
332
366
|
if (queryName !== void 0) lc = lc.withContext("queryName", queryName);
|
|
333
367
|
if (hydrationReason !== void 0) lc = lc.withContext("hydrationReason", hydrationReason);
|
|
@@ -336,7 +370,11 @@ var PipelineDriver = class {
|
|
|
336
370
|
if (hydrationRowCount !== void 0) lc = lc.withContext("hydrationRowCount", hydrationRowCount);
|
|
337
371
|
if (hydrationRowsRead !== void 0) lc = lc.withContext("hydrationRowsRead", hydrationRowsRead);
|
|
338
372
|
if (pipelineLifetimeMs !== void 0) lc = lc.withContext("pipelineLifetimeMs", pipelineLifetimeMs);
|
|
339
|
-
lc.
|
|
373
|
+
if (abandonReason !== void 0) lc = lc.withContext("abandonReason", abandonReason);
|
|
374
|
+
if (estimatedPushMs !== void 0) lc = lc.withContext("estimatedPushMs", estimatedPushMs);
|
|
375
|
+
if (pushTimeMs !== void 0) lc = lc.withContext("pushTimeMs", pushTimeMs);
|
|
376
|
+
if (pushedChanges !== void 0) lc = lc.withContext("pushedChanges", pushedChanges);
|
|
377
|
+
lc[level]?.("query pipeline lifecycle");
|
|
340
378
|
}
|
|
341
379
|
/**
|
|
342
380
|
* A `{scalar: true}` that cannot be honored degrades silently to a plain
|
|
@@ -354,13 +392,16 @@ var PipelineDriver = class {
|
|
|
354
392
|
#disableCorrelatedPredicatePushdown() {
|
|
355
393
|
return this.#config?.enableCorrelatedPredicatePushdown === false;
|
|
356
394
|
}
|
|
357
|
-
#resolveScalarSubqueries(ast) {
|
|
395
|
+
#resolveScalarSubqueries(ast, tables) {
|
|
358
396
|
const companionInputs = [];
|
|
359
397
|
const executor = (subqueryAST, childField) => {
|
|
360
398
|
const input = buildPipeline(subqueryAST, {
|
|
361
399
|
enableNotExists: true,
|
|
362
400
|
disableCorrelatedPredicatePushdown: this.#disableCorrelatedPredicatePushdown(),
|
|
363
|
-
getSource: (name) =>
|
|
401
|
+
getSource: (name) => {
|
|
402
|
+
tables.add(name);
|
|
403
|
+
return this.#getSource(name);
|
|
404
|
+
},
|
|
364
405
|
createStorage: () => this.#createStorage(),
|
|
365
406
|
decorateSourceInput: (input) => input,
|
|
366
407
|
decorateInput: (input) => input,
|
|
@@ -409,17 +450,20 @@ var PipelineDriver = class {
|
|
|
409
450
|
this.removeQuery(queryID, "replace-query");
|
|
410
451
|
const pipelineRunID = randomID();
|
|
411
452
|
const shape = queryShape(query);
|
|
453
|
+
const level = this.#abandoned.has(queryID) ? "debug" : "info";
|
|
412
454
|
this.#logQueryPipelineLifecycle({
|
|
413
455
|
zeroEvent: "query-pipeline-hydrate-start",
|
|
414
456
|
pipelineRunID,
|
|
415
457
|
queryHash: queryID,
|
|
416
458
|
transformationHash,
|
|
417
459
|
queryName,
|
|
418
|
-
hydrationReason
|
|
460
|
+
hydrationReason,
|
|
461
|
+
level
|
|
419
462
|
});
|
|
420
463
|
const debugDelegate = runtimeDebugFlags.trackRowsVended ? new Debug(true) : void 0;
|
|
421
464
|
const costModel = this.#ensureCostModelExistsIfEnabled(this.#snapshotter.current().db.db);
|
|
422
465
|
assert(this.#advanceContext === null, "Cannot hydrate while advance is in progress");
|
|
466
|
+
assert(!this.#costBasedAdvanceIncomplete, "Cannot hydrate before the advance has completed");
|
|
423
467
|
this.#hydrateContext = { timer };
|
|
424
468
|
const rowsReadAtStart = this.#totalRowsRead();
|
|
425
469
|
let hydrationFinished = false;
|
|
@@ -428,10 +472,11 @@ var PipelineDriver = class {
|
|
|
428
472
|
let planWarnings = [];
|
|
429
473
|
const planWarningThresholds = this.#planWarningThresholds;
|
|
430
474
|
let builtInputs = [];
|
|
475
|
+
const tables = /* @__PURE__ */ new Set();
|
|
431
476
|
try {
|
|
432
477
|
this.#rowSetSignatures.set(queryID, 0n);
|
|
433
478
|
this.#changedRowSetSignatures.add(queryID);
|
|
434
|
-
const { ast: resolvedQuery, companions: companionMeta, companionInputs, ignoredScalarHints } = this.#resolveScalarSubqueries(query);
|
|
479
|
+
const { ast: resolvedQuery, companions: companionMeta, companionInputs, ignoredScalarHints } = this.#resolveScalarSubqueries(query, tables);
|
|
435
480
|
builtInputs = [...companionInputs];
|
|
436
481
|
this.#warnIgnoredScalarHints(queryID, ignoredScalarHints);
|
|
437
482
|
const input = buildPipeline(resolvedQuery, {
|
|
@@ -439,7 +484,10 @@ var PipelineDriver = class {
|
|
|
439
484
|
enableNotExists: true,
|
|
440
485
|
disableCorrelatedPredicatePushdown: this.#disableCorrelatedPredicatePushdown(),
|
|
441
486
|
enablePlannerAwarePushdown: this.#config?.enablePlannerAwarePushdown !== false,
|
|
442
|
-
getSource: (name) =>
|
|
487
|
+
getSource: (name) => {
|
|
488
|
+
tables.add(name);
|
|
489
|
+
return this.#getSource(name);
|
|
490
|
+
},
|
|
443
491
|
createStorage: () => this.#createStorage(),
|
|
444
492
|
decorateSourceInput: (input, _queryID) => new MeasurePushOperator(new QueryFailureLoggingOperator(this.#lc, input, queryID, transformationHash, queryName), queryID, this.#metricsDelegate, "query-update-server"),
|
|
445
493
|
decorateInput: (input) => input,
|
|
@@ -524,8 +572,10 @@ var PipelineDriver = class {
|
|
|
524
572
|
shape,
|
|
525
573
|
transformationHash,
|
|
526
574
|
...queryName !== void 0 && { queryName },
|
|
527
|
-
companions: liveCompanions
|
|
575
|
+
companions: liveCompanions,
|
|
576
|
+
tables
|
|
528
577
|
});
|
|
578
|
+
this.#abandoned.delete(queryID);
|
|
529
579
|
hydrationFinished = true;
|
|
530
580
|
this.#queryStats?.recordHydration({
|
|
531
581
|
queryName,
|
|
@@ -546,7 +596,8 @@ var PipelineDriver = class {
|
|
|
546
596
|
hydrationReason,
|
|
547
597
|
hydrationTimeMs,
|
|
548
598
|
hydrationRowCount,
|
|
549
|
-
hydrationRowsRead
|
|
599
|
+
hydrationRowsRead,
|
|
600
|
+
level
|
|
550
601
|
});
|
|
551
602
|
} catch (e) {
|
|
552
603
|
hydrationFailed = true;
|
|
@@ -606,15 +657,24 @@ var PipelineDriver = class {
|
|
|
606
657
|
* was not added.
|
|
607
658
|
*/
|
|
608
659
|
removeQuery(queryID, stopReason = "remove-query") {
|
|
660
|
+
this.#removeQuery(queryID, stopReason);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* @param abandoned If an advancement is abandoning the query's pipeline,
|
|
664
|
+
* the record of that, which is logged.
|
|
665
|
+
*/
|
|
666
|
+
#removeQuery(queryID, stopReason, abandoned) {
|
|
609
667
|
const pipeline = this.#pipelines.get(queryID);
|
|
610
668
|
if (pipeline) {
|
|
611
669
|
this.#pipelines.delete(queryID);
|
|
612
|
-
this.#destroyPipeline(queryID, pipeline, stopReason);
|
|
670
|
+
this.#destroyPipeline(queryID, pipeline, stopReason, abandoned);
|
|
613
671
|
}
|
|
614
672
|
this.#rowSetSignatures.delete(queryID);
|
|
615
673
|
this.#changedRowSetSignatures.delete(queryID);
|
|
674
|
+
if (stopReason !== "replace-query") this.#abandoned.delete(queryID);
|
|
675
|
+
if (stopReason === "remove-query") this.#pushCosts.forget(queryID);
|
|
616
676
|
}
|
|
617
|
-
#destroyPipeline(queryID, pipeline, stopReason) {
|
|
677
|
+
#destroyPipeline(queryID, pipeline, stopReason, abandoned) {
|
|
618
678
|
this.#logQueryPipelineLifecycle({
|
|
619
679
|
zeroEvent: "query-pipeline-stop",
|
|
620
680
|
pipelineRunID: pipeline.pipelineRunID,
|
|
@@ -625,7 +685,14 @@ var PipelineDriver = class {
|
|
|
625
685
|
stopReason,
|
|
626
686
|
hydrationTimeMs: pipeline.hydrationTimeMs,
|
|
627
687
|
hydrationRowCount: pipeline.hydrationRowCount,
|
|
628
|
-
pipelineLifetimeMs: Date.now() - pipeline.pipelineReadyAtMs
|
|
688
|
+
pipelineLifetimeMs: Date.now() - pipeline.pipelineReadyAtMs,
|
|
689
|
+
...abandoned && {
|
|
690
|
+
abandonReason: abandoned.reason,
|
|
691
|
+
estimatedPushMs: abandoned.estimatedPushMs,
|
|
692
|
+
pushTimeMs: abandoned.pushTimeMs,
|
|
693
|
+
pushedChanges: abandoned.changes,
|
|
694
|
+
level: "debug"
|
|
695
|
+
}
|
|
629
696
|
});
|
|
630
697
|
pipeline.input.destroy();
|
|
631
698
|
for (const companion of pipeline.companions) companion.input.destroy();
|
|
@@ -689,20 +756,41 @@ var PipelineDriver = class {
|
|
|
689
756
|
* measure the progress of the advancement and abort with a
|
|
690
757
|
* {@link ResetPipelinesSignal} if it is estimated to take longer
|
|
691
758
|
* than a hydration.
|
|
759
|
+
* @param costBased If given, the advancement is bounded query by query
|
|
760
|
+
* instead (see {@link CostBasedAdvance}).
|
|
692
761
|
* @return The resulting row changes for all added queries. Note that the
|
|
693
762
|
* `changes` must be iterated over in their entirety in order to
|
|
694
763
|
* advance the database snapshot.
|
|
764
|
+
* @throws A {@link ResetPipelinesSignal} if `costBased` is given and the
|
|
765
|
+
* changes include a schema change, a truncation or a change of
|
|
766
|
+
* the permissions. It is thrown before any change is pushed or
|
|
767
|
+
* any pipeline is removed, as the caller resets the pipelines
|
|
768
|
+
* either way. Without `costBased`, it is iterating over the
|
|
769
|
+
* `changes` that throws it, on getting to that change.
|
|
695
770
|
*/
|
|
696
|
-
advance(timer) {
|
|
771
|
+
advance(timer, costBased) {
|
|
697
772
|
assert(this.initialized(), "Pipeline driver must be initialized before advancing");
|
|
698
|
-
|
|
773
|
+
let diff;
|
|
774
|
+
let context;
|
|
775
|
+
if (costBased === void 0) diff = this.#snapshotter.advance(this.#tableSpecs, this.#allTableNames, this.#tables, "divergent");
|
|
776
|
+
else {
|
|
777
|
+
const { prev, curr } = this.#snapshotter.advanceWithoutDiff();
|
|
778
|
+
const counts = curr.countChangesSince(prev.version);
|
|
779
|
+
const reset = this.#snapshotter.pendingReset(this.#tableSpecs, counts);
|
|
780
|
+
if (reset) throw reset;
|
|
781
|
+
context = this.#startCostBasedAdvance(costBased, curr.version > prev.version, counts);
|
|
782
|
+
diff = this.#snapshotter.diff(this.#tableSpecs, this.#allTableNames, this.#tables, "divergent", counts, context.abandoned.length > 0 ? this.#tablesRead() : void 0);
|
|
783
|
+
this.#costBasedAdvanceIncomplete = true;
|
|
784
|
+
}
|
|
699
785
|
const { prev, curr, changes } = diff;
|
|
700
786
|
this.#lc.debug?.(`advance ${prev.version} => ${curr.version}: ${changes} changes`);
|
|
701
787
|
const progress = { changesProcessed: 0 };
|
|
702
788
|
return {
|
|
703
789
|
version: curr.version,
|
|
704
790
|
numChanges: changes,
|
|
705
|
-
changes: this.#trackRowSetSignatures(this.#advance(diff, timer, changes, progress)),
|
|
791
|
+
changes: this.#trackRowSetSignatures(this.#advance(diff, timer, changes, progress, context)),
|
|
792
|
+
abandoned: () => context?.abandoned ?? [],
|
|
793
|
+
estimates: () => context?.estimates ?? [],
|
|
706
794
|
progress: () => ({
|
|
707
795
|
changesScanned: diff.changesScanned,
|
|
708
796
|
changesProcessed: progress.changesProcessed,
|
|
@@ -710,7 +798,109 @@ var PipelineDriver = class {
|
|
|
710
798
|
})
|
|
711
799
|
};
|
|
712
800
|
}
|
|
713
|
-
|
|
801
|
+
/** The tables that the pipelines read. */
|
|
802
|
+
#tablesRead() {
|
|
803
|
+
const tables = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const pipeline of this.#pipelines.values()) for (const table of pipeline.tables) tables.add(table);
|
|
805
|
+
return tables;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Removes the pipelines of the queries that are estimated to be cheaper to
|
|
809
|
+
* hydrate again than to push the advancement's changes to: first each
|
|
810
|
+
* query for what pushing costs it, and then the ones that are left for
|
|
811
|
+
* what reading the changes costs them together.
|
|
812
|
+
*
|
|
813
|
+
* @param canAbandon Whether pipelines can be removed, by this or by the
|
|
814
|
+
* cutoff.
|
|
815
|
+
* @param counts The rows that changed since the previous snapshot.
|
|
816
|
+
*/
|
|
817
|
+
#startCostBasedAdvance({ shouldCutOff, rebuildMargin }, canAbandon, counts) {
|
|
818
|
+
const changes = counts.byTable;
|
|
819
|
+
const estimatedPushMs = /* @__PURE__ */ new Map();
|
|
820
|
+
const abandoned = [];
|
|
821
|
+
if (canAbandon) {
|
|
822
|
+
for (const [queryID, pipeline] of this.#pipelines) {
|
|
823
|
+
const estimate = this.#pushCosts.estimate(queryID, pipeline.tables, changes);
|
|
824
|
+
if (estimate === void 0) continue;
|
|
825
|
+
if (shouldStartOnRebuild(estimate, pipeline.hydrationTimeMs, rebuildMargin)) this.#abandon(abandoned, "estimate", pipeline, {
|
|
826
|
+
queryID,
|
|
827
|
+
pushTimeMs: 0,
|
|
828
|
+
changes: 0,
|
|
829
|
+
hydrationTimeMs: pipeline.hydrationTimeMs,
|
|
830
|
+
estimatedPushMs: estimate
|
|
831
|
+
});
|
|
832
|
+
else estimatedPushMs.set(queryID, estimate);
|
|
833
|
+
}
|
|
834
|
+
this.#abandonInsteadOfReading(counts, rebuildMargin, estimatedPushMs, abandoned);
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
cutoff: new PushCutoff(canAbandon ? shouldCutOff : () => false),
|
|
838
|
+
entries: counts.total,
|
|
839
|
+
estimatedPushMs,
|
|
840
|
+
abandoned,
|
|
841
|
+
estimates: []
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Removes the pipelines that are left, if reading the advancement's
|
|
846
|
+
* changes and pushing them to those is estimated to cost more than
|
|
847
|
+
* hydrating them again. The changes are then not read at all.
|
|
848
|
+
*/
|
|
849
|
+
#abandonInsteadOfReading(counts, rebuildMargin, estimatedPushMs, abandoned) {
|
|
850
|
+
if (this.#pipelines.size === 0) return;
|
|
851
|
+
let entriesOfTablesRead = 0;
|
|
852
|
+
for (const table of this.#tablesRead()) entriesOfTablesRead += counts.byTable.get(table) ?? 0;
|
|
853
|
+
const queries = Array.from(this.#pipelines, ([queryID, pipeline]) => ({
|
|
854
|
+
queryID,
|
|
855
|
+
pipeline,
|
|
856
|
+
estimatedPushMs: estimatedPushMs.get(queryID),
|
|
857
|
+
hydrationTimeMs: pipeline.hydrationTimeMs
|
|
858
|
+
}));
|
|
859
|
+
const estimatedReadMs = this.#readCosts.estimate({
|
|
860
|
+
walk: counts.total,
|
|
861
|
+
parse: abandoned.length > 0 ? entriesOfTablesRead : counts.total,
|
|
862
|
+
lookup: entriesOfTablesRead
|
|
863
|
+
});
|
|
864
|
+
if (!shouldRebuildInsteadOfReading(estimatedReadMs, queries, rebuildMargin)) return;
|
|
865
|
+
let pushMs = 0;
|
|
866
|
+
let hydrationMs = 0;
|
|
867
|
+
for (const query of queries) {
|
|
868
|
+
pushMs += query.estimatedPushMs ?? 0;
|
|
869
|
+
hydrationMs += query.hydrationTimeMs;
|
|
870
|
+
}
|
|
871
|
+
this.#lc.debug?.("Not reading a batch of changes: hydrating its queries again is estimated to cost less", {
|
|
872
|
+
entries: counts.total,
|
|
873
|
+
entriesOfTablesRead,
|
|
874
|
+
estimatedReadMs,
|
|
875
|
+
estimatedPushMs: pushMs,
|
|
876
|
+
hydrationTimeMs: hydrationMs,
|
|
877
|
+
rebuildMargin,
|
|
878
|
+
queries: queries.length
|
|
879
|
+
});
|
|
880
|
+
for (const { queryID, pipeline, ...estimate } of queries) this.#abandon(abandoned, "read", pipeline, {
|
|
881
|
+
queryID,
|
|
882
|
+
pushTimeMs: 0,
|
|
883
|
+
changes: 0,
|
|
884
|
+
...estimate
|
|
885
|
+
});
|
|
886
|
+
estimatedPushMs.clear();
|
|
887
|
+
}
|
|
888
|
+
#abandon(abandoned, reason, pipeline, progress) {
|
|
889
|
+
const { transformationHash, transformedAst, originalAst, queryName } = pipeline;
|
|
890
|
+
const query = {
|
|
891
|
+
...progress,
|
|
892
|
+
reason,
|
|
893
|
+
transformationHash,
|
|
894
|
+
transformedAst,
|
|
895
|
+
originalAst,
|
|
896
|
+
...queryName !== void 0 && { queryName }
|
|
897
|
+
};
|
|
898
|
+
this.#removeQuery(progress.queryID, "abandon-advance", query);
|
|
899
|
+
this.#abandoned.set(progress.queryID, query);
|
|
900
|
+
abandoned.push(query);
|
|
901
|
+
this.#queryStats?.recordAbandoned(pipeline, reason);
|
|
902
|
+
}
|
|
903
|
+
*#advance(diff, timer, numChanges, progress, costBased) {
|
|
714
904
|
assert(this.#hydrateContext === null, "Cannot advance while hydration is in progress");
|
|
715
905
|
const totalHydrationTimeMs = this.totalHydrationTimeMs();
|
|
716
906
|
const advanceContext = {
|
|
@@ -726,11 +916,13 @@ var PipelineDriver = class {
|
|
|
726
916
|
pos: 0,
|
|
727
917
|
currentTable: void 0,
|
|
728
918
|
queryStats: /* @__PURE__ */ new Map(),
|
|
919
|
+
costBased,
|
|
729
920
|
pushTimeMs: 0,
|
|
730
921
|
stage: "diff"
|
|
731
922
|
};
|
|
732
923
|
this.#advanceContext = advanceContext;
|
|
733
924
|
let timedOut = false;
|
|
925
|
+
let readToEnd = true;
|
|
734
926
|
try {
|
|
735
927
|
advanceContext.reservedRows = this.#reserveDeferredWrites(numChanges);
|
|
736
928
|
const deferWrites = advanceContext.reservedRows !== void 0;
|
|
@@ -740,7 +932,14 @@ var PipelineDriver = class {
|
|
|
740
932
|
for (const { table, prevValues: probedPrevValues, nextValue, rowKey } of diff) {
|
|
741
933
|
advanceContext.stage = "between";
|
|
742
934
|
advanceContext.completedChanges = diff.changesScanned - 1;
|
|
743
|
-
if (this.#
|
|
935
|
+
if (this.#checkAdvance(advanceContext)) yield "yield";
|
|
936
|
+
if (costBased) {
|
|
937
|
+
this.#cutOffQueries(advanceContext, costBased, diff, table);
|
|
938
|
+
if (this.#pipelines.size === 0) {
|
|
939
|
+
readToEnd = false;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
744
943
|
const start = timer.totalElapsed();
|
|
745
944
|
advanceContext.currentChangeStartMs = start;
|
|
746
945
|
advanceContext.currentTable = table;
|
|
@@ -779,7 +978,7 @@ var PipelineDriver = class {
|
|
|
779
978
|
advanceContext.completedChanges = diff.changesScanned;
|
|
780
979
|
}
|
|
781
980
|
}
|
|
782
|
-
this.#
|
|
981
|
+
this.#checkAdvance(advanceContext, false);
|
|
783
982
|
if (holds === "fits") holds = this.#holdPendingRows(advanceContext);
|
|
784
983
|
} finally {
|
|
785
984
|
advanceContext.pushTimeMs += timer.totalElapsed() - start;
|
|
@@ -793,9 +992,26 @@ var PipelineDriver = class {
|
|
|
793
992
|
advanceContext.completedChanges = diff.changesScanned;
|
|
794
993
|
const { curr } = diff;
|
|
795
994
|
for (const table of this.#tables.values()) table.setDB(curr.db.db);
|
|
995
|
+
this.#costBasedAdvanceIncomplete = false;
|
|
796
996
|
this.#ensureCostModelExistsIfEnabled(curr.db.db);
|
|
797
997
|
this.#lc.debug?.(`Advanced to ${curr.version}`);
|
|
798
998
|
this.#logSlowAdvances(advanceContext);
|
|
999
|
+
if (costBased) {
|
|
1000
|
+
this.#recordPushCosts(advanceContext, costBased, diff.changesScannedByTable);
|
|
1001
|
+
if (readToEnd && diff.changes > 0 && diff.stats) {
|
|
1002
|
+
const { changeLogReadMs, parseMs, rowReadMs } = diff.stats;
|
|
1003
|
+
this.#readCosts.record({
|
|
1004
|
+
walk: changeLogReadMs,
|
|
1005
|
+
parse: parseMs,
|
|
1006
|
+
lookup: rowReadMs
|
|
1007
|
+
}, {
|
|
1008
|
+
walk: costBased.entries,
|
|
1009
|
+
parse: diff.changesScanned,
|
|
1010
|
+
lookup: diff.changesLookedUp
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
this.#snapshotter.releasePrevious();
|
|
1014
|
+
}
|
|
799
1015
|
} catch (e) {
|
|
800
1016
|
if (e instanceof ResetPipelinesSignal && e.reason === "advancement-timeout") {
|
|
801
1017
|
timedOut = true;
|
|
@@ -809,6 +1025,70 @@ var PipelineDriver = class {
|
|
|
809
1025
|
this.#recordAdvanceStats(advanceContext, timedOut);
|
|
810
1026
|
}
|
|
811
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Checks the progress of the advancement. Returns whether it should yield.
|
|
1030
|
+
*
|
|
1031
|
+
* An advancement is bounded in one of two ways, never both. A
|
|
1032
|
+
* {@link CostBasedAdvance} cuts queries off one by one between changes
|
|
1033
|
+
* (see {@link #cutOffQueries}) and always runs to completion.
|
|
1034
|
+
* Without one, {@link #shouldAdvanceYieldMaybeAbortAdvance} aborts the
|
|
1035
|
+
* advancement with a {@link ResetPipelinesSignal} if it takes too long.
|
|
1036
|
+
*/
|
|
1037
|
+
#checkAdvance(advanceContext, checkYield = true) {
|
|
1038
|
+
if (advanceContext.costBased === void 0) return this.#shouldAdvanceYieldMaybeAbortAdvance(checkYield);
|
|
1039
|
+
return checkYield && advanceContext.timer.elapsedLap() > this.#yieldThresholdMs();
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Removes the pipelines of the queries that the cutoff says to.
|
|
1043
|
+
*
|
|
1044
|
+
* @param nextTable The table of the change that `diff` has just yielded,
|
|
1045
|
+
* which has not been pushed yet.
|
|
1046
|
+
*/
|
|
1047
|
+
#cutOffQueries({ queryStats }, costBased, diff, nextTable) {
|
|
1048
|
+
const cutOff = costBased.cutoff.takeCutOff((queryID) => {
|
|
1049
|
+
const pipeline = this.#pipelines.get(queryID);
|
|
1050
|
+
const stats = queryStats.get(queryID);
|
|
1051
|
+
return pipeline === void 0 || stats === void 0 ? void 0 : {
|
|
1052
|
+
queryID,
|
|
1053
|
+
pushTimeMs: stats.timeMs,
|
|
1054
|
+
changes: stats.changes,
|
|
1055
|
+
hydrationTimeMs: pipeline.hydrationTimeMs
|
|
1056
|
+
};
|
|
1057
|
+
});
|
|
1058
|
+
if (cutOff.length === 0) return;
|
|
1059
|
+
const entriesByTable = new Map(diff.changesScannedByTable);
|
|
1060
|
+
entriesByTable.set(nextTable, (entriesByTable.get(nextTable) ?? 1) - 1);
|
|
1061
|
+
for (const progress of cutOff) {
|
|
1062
|
+
const { queryID } = progress;
|
|
1063
|
+
const pipeline = must(this.#pipelines.get(queryID));
|
|
1064
|
+
this.#pushCosts.record(queryID, pipeline.tables, queryStats.get(queryID)?.timeMsByTable, entriesByTable);
|
|
1065
|
+
this.#queryStats?.recordAdvance(pipeline, {
|
|
1066
|
+
timeMs: progress.pushTimeMs,
|
|
1067
|
+
changes: progress.changes,
|
|
1068
|
+
timedOut: false
|
|
1069
|
+
});
|
|
1070
|
+
this.#abandon(costBased.abandoned, "cutoff", pipeline, {
|
|
1071
|
+
...progress,
|
|
1072
|
+
estimatedPushMs: costBased.estimatedPushMs.get(queryID)
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Learns from a completed advancement what its changes cost the queries
|
|
1078
|
+
* that were pushed all of them.
|
|
1079
|
+
*/
|
|
1080
|
+
#recordPushCosts({ queryStats }, { estimatedPushMs, estimates }, entriesByTable) {
|
|
1081
|
+
for (const [queryID, pipeline] of this.#pipelines) {
|
|
1082
|
+
const stats = queryStats.get(queryID);
|
|
1083
|
+
this.#pushCosts.record(queryID, pipeline.tables, stats?.timeMsByTable, entriesByTable);
|
|
1084
|
+
const estimate = estimatedPushMs.get(queryID);
|
|
1085
|
+
if (estimate !== void 0) estimates.push({
|
|
1086
|
+
queryID,
|
|
1087
|
+
estimatedPushMs: estimate,
|
|
1088
|
+
pushTimeMs: stats?.timeMs ?? 0
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
812
1092
|
/** Accounts the time each query took in an advancement to its shape. */
|
|
813
1093
|
#recordAdvanceStats({ queryStats }, timedOut) {
|
|
814
1094
|
if (this.#queryStats === void 0) return;
|
|
@@ -883,6 +1163,7 @@ var PipelineDriver = class {
|
|
|
883
1163
|
timeMsByTable: /* @__PURE__ */ new Map()
|
|
884
1164
|
}));
|
|
885
1165
|
stats.timeMs += timeMs;
|
|
1166
|
+
advance.costBased?.cutoff.recordPush(queryID);
|
|
886
1167
|
if (stats.lastPos !== advance.pos) {
|
|
887
1168
|
stats.lastPos = advance.pos;
|
|
888
1169
|
stats.changes++;
|
|
@@ -1000,7 +1281,7 @@ var PipelineDriver = class {
|
|
|
1000
1281
|
}
|
|
1001
1282
|
#shouldYield() {
|
|
1002
1283
|
if (this.#hydrateContext) return this.#hydrateContext.timer.elapsedLap() > this.#yieldThresholdMs();
|
|
1003
|
-
if (this.#advanceContext) return this.#
|
|
1284
|
+
if (this.#advanceContext) return this.#checkAdvance(this.#advanceContext);
|
|
1004
1285
|
throw new Error("shouldYield called outside of hydration or advancement");
|
|
1005
1286
|
}
|
|
1006
1287
|
/**
|