@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.
Files changed (130) hide show
  1. package/out/zero/package.js +1 -1
  2. package/out/zero/package.js.map +1 -1
  3. package/out/zero-cache/src/config/zero-config.d.ts +28 -0
  4. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  5. package/out/zero-cache/src/config/zero-config.js +83 -0
  6. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  7. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  8. package/out/zero-cache/src/server/change-streamer.js +2 -1
  9. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  10. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  11. package/out/zero-cache/src/server/main.js +5 -1
  12. package/out/zero-cache/src/server/main.js.map +1 -1
  13. package/out/zero-cache/src/server/replicator.d.ts +4 -0
  14. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  15. package/out/zero-cache/src/server/replicator.js +18 -4
  16. package/out/zero-cache/src/server/replicator.js.map +1 -1
  17. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  18. package/out/zero-cache/src/server/runner/run-worker.js +12 -6
  19. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  20. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  21. package/out/zero-cache/src/server/syncer.js +17 -1
  22. package/out/zero-cache/src/server/syncer.js.map +1 -1
  23. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  24. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  25. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +27 -7
  26. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  27. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +124 -62
  28. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  29. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +9 -3
  30. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  31. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +19 -6
  32. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  33. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +8 -0
  34. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  35. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +131 -62
  36. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  37. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +124 -2
  38. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  39. package/out/zero-cache/src/services/change-streamer/change-streamer.js +33 -2
  40. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  41. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +24 -0
  42. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -0
  43. package/out/zero-cache/src/services/change-streamer/drain-tracker.js +67 -0
  44. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -0
  45. package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts +214 -0
  46. package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts.map +1 -0
  47. package/out/zero-cache/src/services/change-streamer/restore-admission.js +294 -0
  48. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -0
  49. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +13 -1
  50. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
  51. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +7 -6
  52. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  53. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +2 -2
  54. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
  55. package/out/zero-cache/src/services/change-streamer/snapshot.js +2 -2
  56. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  57. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +39 -0
  58. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  59. package/out/zero-cache/src/services/change-streamer/subscribe.js +19 -2
  60. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  61. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +22 -1
  62. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  63. package/out/zero-cache/src/services/change-streamer/subscriber.js +41 -15
  64. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  65. package/out/zero-cache/src/services/http-service.d.ts +6 -0
  66. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  67. package/out/zero-cache/src/services/http-service.js +14 -2
  68. package/out/zero-cache/src/services/http-service.js.map +1 -1
  69. package/out/zero-cache/src/services/life-cycle.d.ts +33 -0
  70. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  71. package/out/zero-cache/src/services/life-cycle.js +56 -10
  72. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  73. package/out/zero-cache/src/services/litestream/commands.d.ts +8 -0
  74. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  75. package/out/zero-cache/src/services/litestream/commands.js +22 -1
  76. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  77. package/out/zero-cache/src/services/replicator/drain-handler.d.ts +74 -0
  78. package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -0
  79. package/out/zero-cache/src/services/replicator/drain-handler.js +185 -0
  80. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -0
  81. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -1
  82. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/replicator/incremental-sync.js +66 -13
  84. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  85. package/out/zero-cache/src/services/replicator/replicator.d.ts +2 -1
  86. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/replicator/replicator.js +2 -2
  88. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  89. package/out/zero-cache/src/services/view-syncer/batch-pacer.d.ts +79 -0
  90. package/out/zero-cache/src/services/view-syncer/batch-pacer.d.ts.map +1 -0
  91. package/out/zero-cache/src/services/view-syncer/batch-pacer.js +249 -0
  92. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -0
  93. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +27 -1
  94. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/cvr.js +107 -17
  96. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  97. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +85 -3
  98. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  99. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +301 -20
  100. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  101. package/out/zero-cache/src/services/view-syncer/push-cost.d.ts +132 -0
  102. package/out/zero-cache/src/services/view-syncer/push-cost.d.ts.map +1 -0
  103. package/out/zero-cache/src/services/view-syncer/push-cost.js +164 -0
  104. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -0
  105. package/out/zero-cache/src/services/view-syncer/push-cutoff.d.ts +55 -0
  106. package/out/zero-cache/src/services/view-syncer/push-cutoff.d.ts.map +1 -0
  107. package/out/zero-cache/src/services/view-syncer/push-cutoff.js +56 -0
  108. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -0
  109. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts +7 -0
  110. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts.map +1 -1
  111. package/out/zero-cache/src/services/view-syncer/query-stats.js +20 -1
  112. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  113. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +136 -5
  114. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  115. package/out/zero-cache/src/services/view-syncer/snapshotter.js +246 -18
  116. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  117. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +8 -2
  118. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  119. package/out/zero-cache/src/services/view-syncer/view-syncer.js +381 -16
  120. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  121. package/out/zero-cache/src/types/processes.d.ts +8 -0
  122. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  123. package/out/zero-cache/src/types/processes.js.map +1 -1
  124. package/out/zero-client/src/client/version.js +1 -1
  125. package/out/zero-react/src/use-query.js +1 -1
  126. package/out/zero-react/src/use-query.js.map +1 -1
  127. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  128. package/out/zero-server/src/queries/process-queries.js +1 -1
  129. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  130. 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
- var slowHydrationLogThrottle = new LogThrottle({ windowMs: 5 * 6e4 });
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 = new TimeSliceTimer(lc);
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 = new TimeSliceTimer(lc);
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 = new TimeSliceTimer(lc);
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) this.#cvr = await this.#flushPoked(lc, updater, pokers);
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
- constructor(lc) {
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() {