@rocicorp/zero 1.11.0-canary.2 → 1.11.0-canary.4
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 +4 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +12 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/services/litestream/config-v5.yml +2 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +28 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +74 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/hydration-budget.d.ts +33 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.js +62 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +11 -1
- 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 +231 -76
- 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/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { manualSpan, startAsyncSpan, startSpan } from "../../../../otel/src/span
|
|
|
10
10
|
import { ZERO_VERSION_COLUMN_NAME } from "../replicator/schema/constants.js";
|
|
11
11
|
import { transformAndHashQuery } from "../../auth/read-authorizer.js";
|
|
12
12
|
import { randInt } from "../../../../shared/src/rand.js";
|
|
13
|
-
import { getOrCreateCounter, getOrCreateLatencyHistogram, getOrCreateNativeHistogram, getOrCreateUpDownCounter } from "../../observability/metrics.js";
|
|
13
|
+
import { getOrCreateCounter, getOrCreateLatencyHistogram, getOrCreateNativeHistogram, getOrCreateUpDownCounter, getOrCreateValueHistogram } from "../../observability/metrics.js";
|
|
14
14
|
import { rowIDString } from "../../types/row-key.js";
|
|
15
15
|
import "../replicator/schema/replication-state.js";
|
|
16
16
|
import { parseSignature } from "./row-set-signature.js";
|
|
@@ -25,8 +25,9 @@ import { isAuthErrorBody } from "../../auth/auth.js";
|
|
|
25
25
|
import { ClientHandler, startPoke } from "./client-handler.js";
|
|
26
26
|
import { tracer } from "./tracer.js";
|
|
27
27
|
import { CVRStore, ClientNotFoundError } from "./cvr-store.js";
|
|
28
|
-
import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, nextEvictionTime } from "./cvr.js";
|
|
28
|
+
import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, classifyQueriesForHydration, nextEvictionTime } from "./cvr.js";
|
|
29
29
|
import { E2EServingLagTracker } from "./e2e-serving-lag.js";
|
|
30
|
+
import { HydrationBudget } from "./hydration-budget.js";
|
|
30
31
|
import { handleInspect } from "./inspect-handler.js";
|
|
31
32
|
import { QueryCoveringIndex } from "./query-covering.js";
|
|
32
33
|
import { resolver } from "@rocicorp/resolver";
|
|
@@ -90,12 +91,27 @@ var ViewSyncerService = class {
|
|
|
90
91
|
#stopped = resolver();
|
|
91
92
|
#initialized = resolver();
|
|
92
93
|
#cvr;
|
|
93
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Indicates whether the query pipelines have completed initial catch-up and
|
|
96
|
+
* hydration with the CVR snapshot, and are ready for steady-state operation.
|
|
97
|
+
*
|
|
98
|
+
* - When `false`: The syncer is in its initial catch-up phase. The replica
|
|
99
|
+
* advances without diffs until it reaches `cvr.version.stateVersion`,
|
|
100
|
+
* after which `#maybeHydratePipelines` hydrates queries.
|
|
101
|
+
* - When `true`: Pipelines are fully populated and in steady-state; replica
|
|
102
|
+
* changes advance incrementally via `#advancePipelines`, and client query
|
|
103
|
+
* updates are processed via `#syncQueryPipelineSet`.
|
|
104
|
+
*
|
|
105
|
+
* Resets to `false` if `#advancePipelines` returns a `ResetPipelinesSignal`
|
|
106
|
+
* and pipelines must be reset and rehydrated.
|
|
107
|
+
*/
|
|
108
|
+
#pipelinesHydrated = false;
|
|
94
109
|
#servedVersion = null;
|
|
95
110
|
#e2eServingLagTracker = new E2EServingLagTracker();
|
|
96
111
|
#expiredQueriesTimer = 0;
|
|
97
112
|
#authMaintenanceTimer = 0;
|
|
98
113
|
#setTimeout;
|
|
114
|
+
#now;
|
|
99
115
|
#customQueryTransformer;
|
|
100
116
|
#queryReplacements = /* @__PURE__ */ new Map();
|
|
101
117
|
#activeClients = getOrCreateUpDownCounter("sync", "active-clients", "Number of active sync clients");
|
|
@@ -122,10 +138,56 @@ var ViewSyncerService = class {
|
|
|
122
138
|
#pipelineResets = getOrCreateCounter("sync", "pipeline-resets", "Number of pipeline resets");
|
|
123
139
|
#rowSetSignatureDrifts = getOrCreateCounter("sync", "query.row-set-signature-drifts", "Number of times re-hydration of an unchanged query produced a different row-set signature than what is stored in the CVR (forcing a configVersion bump and full re-execution). Expected to be near-zero in steady state; persistent non-zero values indicate non-deterministic query execution (e.g. Cap operator picking different N-row subsets).");
|
|
124
140
|
#sameHashRehydrationVersionBumps = getOrCreateCounter("sync", "query.same-hash-rehydrations-forced-bump", "Number of times query-set reconciliation forced a configVersion bump for already-gotten same-transformation-hash query rehydration because trackQueries would not otherwise bump. Expected to be near-zero; non-zero values indicate pipeline/CVR row-set drift reached query-set reconciliation.");
|
|
141
|
+
#hydrationBudgetExhaustions = getOrCreateCounter("sync", "hydration_budget_exhaustions", {
|
|
142
|
+
description: "Number of hydration passes that exhaust their budget.",
|
|
143
|
+
unit: "{pass}"
|
|
144
|
+
});
|
|
145
|
+
#hydrationBudgetEvictions = getOrCreateCounter("sync", "hydration_budget_evictions", {
|
|
146
|
+
description: "Number of inactive queries removed after hydration budget exhaustion.",
|
|
147
|
+
unit: "{query}"
|
|
148
|
+
});
|
|
149
|
+
#hydrationBudgetElapsed = getOrCreateValueHistogram("sync", "hydration_budget_elapsed", {
|
|
150
|
+
description: "Elapsed milliseconds when optional query hydration stopped.",
|
|
151
|
+
unit: "ms",
|
|
152
|
+
bucketBoundaries: [
|
|
153
|
+
1,
|
|
154
|
+
2,
|
|
155
|
+
5,
|
|
156
|
+
10,
|
|
157
|
+
20,
|
|
158
|
+
50,
|
|
159
|
+
100,
|
|
160
|
+
200,
|
|
161
|
+
500,
|
|
162
|
+
1e3,
|
|
163
|
+
5e3
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
#hydrationBudgetOvershoot = getOrCreateValueHistogram("sync", "hydration_budget_overshoot", {
|
|
167
|
+
description: "Milliseconds elapsed beyond the configured hydration budget.",
|
|
168
|
+
unit: "ms",
|
|
169
|
+
bucketBoundaries: [
|
|
170
|
+
0,
|
|
171
|
+
1,
|
|
172
|
+
2,
|
|
173
|
+
5,
|
|
174
|
+
10,
|
|
175
|
+
20,
|
|
176
|
+
50,
|
|
177
|
+
100,
|
|
178
|
+
200,
|
|
179
|
+
500,
|
|
180
|
+
1e3
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
#queryEvictions = getOrCreateCounter("sync", "query_evictions", {
|
|
184
|
+
description: "Number of inactive queries evicted, grouped by reason.",
|
|
185
|
+
unit: "{query}"
|
|
186
|
+
});
|
|
125
187
|
#inspectorDelegate;
|
|
126
188
|
#config;
|
|
127
189
|
#runPriorityOp;
|
|
128
|
-
constructor(config, lc, shard, taskID, clientGroupID, cvrDb, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, inspectorDelegate, connContextManager, customQueryTransformer, runPriorityOp, keepaliveMs = DEFAULT_KEEPALIVE_MS, setTimeoutFn = setTimeout.bind(globalThis)) {
|
|
190
|
+
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)) {
|
|
129
191
|
this.#config = config;
|
|
130
192
|
this.id = clientGroupID;
|
|
131
193
|
this.connContextManager = connContextManager;
|
|
@@ -140,6 +202,7 @@ var ViewSyncerService = class {
|
|
|
140
202
|
this.#customQueryTransformer = customQueryTransformer;
|
|
141
203
|
this.#cvrStore = new CVRStore(lc, cvrDb, shard, taskID, clientGroupID, () => this.#stateChanges.cancel());
|
|
142
204
|
this.#setTimeout = setTimeoutFn;
|
|
205
|
+
this.#now = now;
|
|
143
206
|
this.#runPriorityOp = runPriorityOp;
|
|
144
207
|
this.keepalive();
|
|
145
208
|
}
|
|
@@ -213,26 +276,18 @@ var ViewSyncerService = class {
|
|
|
213
276
|
lc.info?.(`resetting CVR: ${message}`);
|
|
214
277
|
throw new ClientNotFoundError(message);
|
|
215
278
|
}
|
|
216
|
-
if (this.#
|
|
279
|
+
if (this.#pipelinesHydrated) {
|
|
217
280
|
const result = await this.#advancePipelines(lc, cvr);
|
|
218
281
|
if (result === "success") return;
|
|
219
282
|
lc.info?.(`resetting pipelines: ${result.message}`);
|
|
220
283
|
this.#pipelineResets.add(1, { reason: result.reason });
|
|
221
284
|
this.#pipelines.reset(clientSchema);
|
|
222
|
-
this.#
|
|
285
|
+
this.#pipelinesHydrated = false;
|
|
223
286
|
this.connContextManager.setSharedRetransformReady(false);
|
|
224
287
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
lc.debug?.(`replica@${version} is behind cvr@${cvrVer}`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
lc.info?.(`init pipelines@${version} (cvr@${cvrVer})`);
|
|
232
|
-
const driftedQueryIDs = await this.#hydrateUnchangedQueries(lc, cvr);
|
|
233
|
-
await this.#syncQueryPipelineSet(lc, cvr, "missing", void 0, driftedQueryIDs);
|
|
234
|
-
this.#pipelinesSynced = true;
|
|
235
|
-
this.connContextManager.setSharedRetransformReady(true);
|
|
288
|
+
const backgroundConnCtx = this.connContextManager.getBackgroundConnectionContext();
|
|
289
|
+
if (backgroundConnCtx) await this.#maybeHydratePipelines(lc, cvr, backgroundConnCtx);
|
|
290
|
+
else lc.info?.("No validated background connection; deferring pipeline init");
|
|
236
291
|
});
|
|
237
292
|
}
|
|
238
293
|
if (this.#drainCoordinator.shouldDrain()) this.#drainCoordinator.drainNextIn(this.#totalHydrationTimeMs());
|
|
@@ -246,11 +301,57 @@ var ViewSyncerService = class {
|
|
|
246
301
|
this.#stopped.resolve();
|
|
247
302
|
}
|
|
248
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Ensures that query pipelines are hydrated and ready for steady-state
|
|
306
|
+
* operation.
|
|
307
|
+
*
|
|
308
|
+
* Preconditions:
|
|
309
|
+
* 1. Must be called from within the `#lock`.
|
|
310
|
+
* 2. The caller must provide an active, validated `ConnectionContext` for
|
|
311
|
+
* evaluating custom query transforms and permissions.
|
|
312
|
+
*
|
|
313
|
+
* Readiness prerequisites:
|
|
314
|
+
* - The underlying `QueryPipelineDriver` must be initialized with the schema
|
|
315
|
+
* (i.e. `this.#pipelines.initialized()` is true).
|
|
316
|
+
* - The SQLite replica must have caught up to at least the CVR's
|
|
317
|
+
* `stateVersion` (`version >= cvr.version.stateVersion`).
|
|
318
|
+
*
|
|
319
|
+
* If either prerequisite is not met, this method returns early as a no-op,
|
|
320
|
+
* waiting for the replica to catch up or the driver to initialize.
|
|
321
|
+
*
|
|
322
|
+
* Once ready:
|
|
323
|
+
* - Hydrates unchanged queries from the CVR snapshot without full row diffing.
|
|
324
|
+
* - Syncs missing, errored, or drifted queries via `#syncQueryPipelineSet`.
|
|
325
|
+
* - Sets `#pipelinesHydrated = true` and enables shared retransformations.
|
|
326
|
+
*/
|
|
327
|
+
async #maybeHydratePipelines(lc, cvr, connCtx) {
|
|
328
|
+
if (!this.#pipelines.initialized()) return;
|
|
329
|
+
const version = this.#pipelines.advanceWithoutDiff();
|
|
330
|
+
const cvrVer = versionString(cvr.version);
|
|
331
|
+
if (version < cvr.version.stateVersion) {
|
|
332
|
+
lc.debug?.(`replica@${version} is behind cvr@${cvrVer}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
lc.info?.(`init pipelines@${version} (cvr@${cvrVer})`);
|
|
336
|
+
const hydrationBudget = new HydrationBudget(this.#config.viewSyncerHydrationBudgetMs ?? 0, this.#now);
|
|
337
|
+
const hydrationPassStats = {
|
|
338
|
+
activeHydratedQueries: 0,
|
|
339
|
+
inactiveHydratedQueries: 0
|
|
340
|
+
};
|
|
341
|
+
const driftedQueryIDs = await this.#hydrateUnchangedQueries(lc, cvr, connCtx, hydrationPassStats, hydrationBudget);
|
|
342
|
+
await this.#syncQueryPipelineSet(lc, cvr, "missing", connCtx, driftedQueryIDs, hydrationBudget, hydrationPassStats);
|
|
343
|
+
this.#pipelinesHydrated = true;
|
|
344
|
+
this.connContextManager.setSharedRetransformReady(true);
|
|
345
|
+
}
|
|
249
346
|
#removeExpiredQueries = async (lc, cvr) => {
|
|
250
347
|
if (hasExpiredQueries(cvr)) {
|
|
251
348
|
lc = lc.withContext("method", "#removeExpiredQueries");
|
|
252
349
|
lc.debug?.("Queries have expired");
|
|
253
|
-
if (this.#
|
|
350
|
+
if (this.#pipelinesHydrated) {
|
|
351
|
+
const connCtx = this.connContextManager.getBackgroundConnectionContext();
|
|
352
|
+
if (connCtx) await this.#syncQueryPipelineSet(lc, cvr, "missing", connCtx);
|
|
353
|
+
else lc.info?.("No validated background connection to remove expired queries; deferring");
|
|
354
|
+
}
|
|
254
355
|
}
|
|
255
356
|
this.#scheduleExpireEviction(lc, cvr);
|
|
256
357
|
};
|
|
@@ -466,7 +567,7 @@ var ViewSyncerService = class {
|
|
|
466
567
|
}
|
|
467
568
|
lc.debug?.("Auth changed, re-validating and re-transforming queries");
|
|
468
569
|
const connCtx = this.connContextManager.mustGetConnectionContext(selector);
|
|
469
|
-
if (!this.#
|
|
570
|
+
if (!this.#pipelinesHydrated) {
|
|
470
571
|
if (!await this.#validateConnection(connCtx)) return;
|
|
471
572
|
}
|
|
472
573
|
return await this.#handleConfigUpdate(lc, clientID, {}, cvr, "all", void 0, connCtx);
|
|
@@ -528,7 +629,8 @@ var ViewSyncerService = class {
|
|
|
528
629
|
await pokers.end(newCVR.version);
|
|
529
630
|
});
|
|
530
631
|
}
|
|
531
|
-
if (this.#
|
|
632
|
+
if (!this.#pipelinesHydrated) await this.#maybeHydratePipelines(lc, this.#cvr, connCtx);
|
|
633
|
+
else await this.#syncQueryPipelineSet(lc, this.#cvr, customQueryTransformMode, connCtx);
|
|
532
634
|
return this.#cvr;
|
|
533
635
|
}
|
|
534
636
|
/**
|
|
@@ -651,7 +753,7 @@ var ViewSyncerService = class {
|
|
|
651
753
|
*
|
|
652
754
|
* This must be called from within the #lock.
|
|
653
755
|
*/
|
|
654
|
-
async #hydrateUnchangedQueries(lc, cvr) {
|
|
756
|
+
async #hydrateUnchangedQueries(lc, cvr, connCtx, hydrationPassStats, hydrationBudget) {
|
|
655
757
|
assert(this.#pipelines.initialized(), "pipelines must be initialized");
|
|
656
758
|
const dbVersion = this.#pipelines.currentVersion();
|
|
657
759
|
const cvrVersion = cvr.version;
|
|
@@ -660,33 +762,27 @@ var ViewSyncerService = class {
|
|
|
660
762
|
return /* @__PURE__ */ new Set();
|
|
661
763
|
}
|
|
662
764
|
const gotQueries = Object.entries(cvr.queries).filter(([_, state]) => state.transformationHash !== void 0);
|
|
765
|
+
const { required: requiredGotQueries } = classifyQueriesForHydration(gotQueries.map(([, query]) => query));
|
|
663
766
|
const customQueries = /* @__PURE__ */ new Map();
|
|
664
767
|
const otherQueries = [];
|
|
665
|
-
|
|
666
|
-
for (const
|
|
667
|
-
|
|
668
|
-
inactivatedCount++;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
if (query.type === "custom") customQueries.set(query.id, query);
|
|
672
|
-
else otherQueries.push(query);
|
|
673
|
-
}
|
|
768
|
+
const inactivatedCount = gotQueries.length - requiredGotQueries.length;
|
|
769
|
+
for (const query of requiredGotQueries) if (query.type === "custom") customQueries.set(query.id, query);
|
|
770
|
+
else otherQueries.push(query);
|
|
674
771
|
const transformedQueries = [];
|
|
675
772
|
let customErrorCount = 0;
|
|
676
773
|
let customHashMismatchCount = 0;
|
|
677
774
|
let otherHashMismatchCount = 0;
|
|
678
775
|
if (customQueries.size > 0 && !this.#customQueryTransformer) lc.warn?.("Custom/named queries were requested but no `ZERO_QUERY_URL` is configured for Zero Cache.");
|
|
679
|
-
const backgroundContext = this.connContextManager.mustGetBackgroundConnectionContext();
|
|
680
776
|
const customQueryTransformer = this.#customQueryTransformer;
|
|
681
777
|
if (customQueryTransformer && customQueries.size > 0) {
|
|
682
|
-
const transformedCustomQueries = await this.#runPriorityOp(lc, "#hydrateUnchangedQueries transforming custom queries", () => customQueryTransformer.transform(
|
|
683
|
-
if (transformedCustomQueries.kind === "success" && !transformedCustomQueries.cached) this.connContextManager.validateConnection(
|
|
778
|
+
const transformedCustomQueries = await hydrationBudget.excluding(() => this.#runPriorityOp(lc, "#hydrateUnchangedQueries transforming custom queries", () => customQueryTransformer.transform(connCtx, customQueries.values())));
|
|
779
|
+
if (transformedCustomQueries.kind === "success" && !transformedCustomQueries.cached) this.connContextManager.validateConnection(connCtx, connCtx.revision, transformedCustomQueries.validation);
|
|
684
780
|
if (Array.isArray(transformedCustomQueries.result)) for (const q of transformedCustomQueries.result) if ("error" in q) customErrorCount++;
|
|
685
781
|
else if (q.transformationHash !== customQueries.get(q.id)?.transformationHash) customHashMismatchCount++;
|
|
686
782
|
else transformedQueries.push(q);
|
|
687
783
|
}
|
|
688
784
|
for (const q of otherQueries) {
|
|
689
|
-
const transformed = transformAndHashQuery(lc, q.id, q.ast, must(this.#pipelines.currentPermissions()).permissions ?? { tables: {} },
|
|
785
|
+
const transformed = transformAndHashQuery(lc, q.id, q.ast, must(this.#pipelines.currentPermissions()).permissions ?? { tables: {} }, connCtx.auth?.type === "jwt" ? connCtx.auth : void 0, q.type === "internal");
|
|
690
786
|
if (transformed.transformationHash === q.transformationHash) transformedQueries.push(transformed);
|
|
691
787
|
else otherHashMismatchCount++;
|
|
692
788
|
}
|
|
@@ -716,6 +812,7 @@ var ViewSyncerService = class {
|
|
|
716
812
|
else count++;
|
|
717
813
|
});
|
|
718
814
|
const elapsed = timer.totalElapsed();
|
|
815
|
+
hydrationPassStats.activeHydratedQueries++;
|
|
719
816
|
this.#hydrations.add(1);
|
|
720
817
|
this.#hydrationTime.recordMs(elapsed);
|
|
721
818
|
this.#addQueryMaterializationServerMetric(transformationHash, elapsed);
|
|
@@ -820,7 +917,10 @@ var ViewSyncerService = class {
|
|
|
820
917
|
*
|
|
821
918
|
* This must be called from within the #lock.
|
|
822
919
|
*/
|
|
823
|
-
#syncQueryPipelineSet(lc, cvr, customQueryTransformMode, connCtx, driftedQueryIDs = /* @__PURE__ */ new Set()) {
|
|
920
|
+
#syncQueryPipelineSet(lc, cvr, customQueryTransformMode, connCtx, driftedQueryIDs = /* @__PURE__ */ new Set(), hydrationBudget = new HydrationBudget(this.#config.viewSyncerHydrationBudgetMs ?? 0, this.#now), hydrationPassStats = {
|
|
921
|
+
activeHydratedQueries: 0,
|
|
922
|
+
inactiveHydratedQueries: 0
|
|
923
|
+
}) {
|
|
824
924
|
return startAsyncSpan(tracer, "vs.#syncQueryPipelineSet", async (span) => {
|
|
825
925
|
const start = performance.now();
|
|
826
926
|
span.setAttribute("clientGroupID", this.id);
|
|
@@ -829,38 +929,39 @@ var ViewSyncerService = class {
|
|
|
829
929
|
const now = Date.now();
|
|
830
930
|
const ttlClock = this.#getTTLClock(now);
|
|
831
931
|
const cvrQueryEntires = Object.entries(cvr.queries);
|
|
832
|
-
const customQueries = /* @__PURE__ */ new Map();
|
|
833
|
-
const otherQueries = [];
|
|
834
932
|
const transformedQueries = [];
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
});
|
|
843
|
-
for (const { id, query: origQuery } of otherQueries) {
|
|
844
|
-
assert(id === origQuery.id, "query id mismatch");
|
|
845
|
-
const transformed = transformAndHashQuery(lc, origQuery.id, origQuery.ast, must(this.#pipelines.currentPermissions()).permissions ?? { tables: {} }, resolvedConnCtx.auth?.type === "jwt" ? resolvedConnCtx.auth : void 0, origQuery.type === "internal");
|
|
846
|
-
transformedQueries.push({
|
|
847
|
-
id,
|
|
848
|
-
origQuery,
|
|
849
|
-
transformed
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
if (customQueries.size > 0 && !this.#customQueryTransformer) lc.warn?.("Custom/named queries were requested but no `ZERO_QUERY_URL` is configured for Zero Cache.");
|
|
853
|
-
let erroredQueryIDs;
|
|
854
|
-
const customQueriesToTransform = customQueryTransformMode === "all" ? [...customQueries.values()] : customQueryTransformMode && [...customQueries.values()].filter((q) => !this.#pipelines.queries().has(q.id));
|
|
933
|
+
const naturallyExpiredQueryIDs = new Set(cvrQueryEntires.map(([, query]) => query).filter((query) => expired(ttlClock, query)).map((query) => query.id));
|
|
934
|
+
if (naturallyExpiredQueryIDs.size > 0) this.#queryEvictions.add(naturallyExpiredQueryIDs.size, { reason: "ttl" });
|
|
935
|
+
const hydrationCandidates = cvrQueryEntires.map(([, query]) => query).filter((query) => !naturallyExpiredQueryIDs.has(query.id));
|
|
936
|
+
const { required, optional } = classifyQueriesForHydration(hydrationCandidates);
|
|
937
|
+
const requiredQueryIDs = new Set(required.map((query) => query.id));
|
|
938
|
+
const optionalToHydrate = hydrationBudget.limitMs === 0 ? optional : optional.filter((query) => query.patchVersion !== void 0);
|
|
939
|
+
if (!this.#customQueryTransformer && hydrationCandidates.some((query) => query.type === "custom")) lc.warn?.("Custom/named queries were requested but no `ZERO_QUERY_URL` is configured for Zero Cache.");
|
|
855
940
|
const customQueryTransformer = this.#customQueryTransformer;
|
|
856
|
-
|
|
941
|
+
const erroredQueryIDs = [];
|
|
942
|
+
let transformedCustomQueryCount = 0;
|
|
943
|
+
const transformOtherQueries = (queries) => {
|
|
944
|
+
for (const origQuery of queries) {
|
|
945
|
+
if (origQuery.type === "custom") continue;
|
|
946
|
+
const transformed = transformAndHashQuery(lc, origQuery.id, origQuery.ast, must(this.#pipelines.currentPermissions()).permissions ?? { tables: {} }, connCtx.auth?.type === "jwt" ? connCtx.auth : void 0, origQuery.type === "internal");
|
|
947
|
+
transformedQueries.push({
|
|
948
|
+
id: origQuery.id,
|
|
949
|
+
origQuery,
|
|
950
|
+
transformed
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
const shouldTransformCustomQuery = (query) => customQueryTransformMode === "all" || customQueryTransformMode && !this.#pipelines.queries().has(query.id);
|
|
955
|
+
const transformCustomQueries = async (queries) => {
|
|
956
|
+
if (!customQueryTransformer || queries.length === 0) return;
|
|
957
|
+
transformedCustomQueryCount += queries.length;
|
|
857
958
|
const transformStart = performance.now();
|
|
858
959
|
let transformedCustomQueries;
|
|
859
960
|
try {
|
|
860
|
-
transformedCustomQueries = await this.#runPriorityOp(lc, "#syncQueryPipelineSet transforming custom queries", () => customQueryTransformer.transform(
|
|
961
|
+
transformedCustomQueries = await hydrationBudget.excluding(() => this.#runPriorityOp(lc, "#syncQueryPipelineSet transforming custom queries", () => customQueryTransformer.transform(connCtx, queries)));
|
|
861
962
|
if (transformedCustomQueries.kind === "failed") throw new ProtocolErrorWithLevel(transformedCustomQueries.result, "warn");
|
|
862
963
|
else {
|
|
863
|
-
if (!transformedCustomQueries.cached) this.connContextManager.validateConnection(
|
|
964
|
+
if (!transformedCustomQueries.cached) this.connContextManager.validateConnection(connCtx, connCtx.revision, transformedCustomQueries.validation);
|
|
864
965
|
this.#queryTransformations.add(1, { result: "success" });
|
|
865
966
|
}
|
|
866
967
|
} catch (e) {
|
|
@@ -871,8 +972,9 @@ var ViewSyncerService = class {
|
|
|
871
972
|
this.#queryTransformationTime.recordMs(transformDuration);
|
|
872
973
|
}
|
|
873
974
|
const successfullyTransformedCustomQueries = /* @__PURE__ */ new Map();
|
|
874
|
-
|
|
875
|
-
|
|
975
|
+
const customQueryMap = new Map(queries.map((query) => [query.id, query]));
|
|
976
|
+
erroredQueryIDs.push(...this.#processTransformedCustomQueries(lc, transformedCustomQueries.result, (q) => {
|
|
977
|
+
const origQuery = customQueryMap.get(q.id);
|
|
876
978
|
if (origQuery) {
|
|
877
979
|
successfullyTransformedCustomQueries.set(q.id, q);
|
|
878
980
|
transformedQueries.push({
|
|
@@ -881,7 +983,7 @@ var ViewSyncerService = class {
|
|
|
881
983
|
transformed: q
|
|
882
984
|
});
|
|
883
985
|
}
|
|
884
|
-
},
|
|
986
|
+
}, customQueryMap));
|
|
885
987
|
for (const [queryID, newTransform] of successfullyTransformedCustomQueries) {
|
|
886
988
|
const existingTransformHash = cvr.queries[queryID]?.transformationHash;
|
|
887
989
|
if (existingTransformHash) {
|
|
@@ -894,21 +996,28 @@ var ViewSyncerService = class {
|
|
|
894
996
|
} else this.#queryTransformationNoOps.add(1);
|
|
895
997
|
}
|
|
896
998
|
}
|
|
897
|
-
}
|
|
898
|
-
const
|
|
999
|
+
};
|
|
1000
|
+
const queriesToTransform = [...required, ...optionalToHydrate];
|
|
1001
|
+
transformOtherQueries(queriesToTransform);
|
|
1002
|
+
await transformCustomQueries(queriesToTransform.filter((query) => query.type === "custom" && shouldTransformCustomQuery(query)));
|
|
1003
|
+
const removeQueriesQueryIds = new Set([...naturallyExpiredQueryIDs, ...erroredQueryIDs]);
|
|
899
1004
|
const addQueries = transformedQueries.map(({ id, origQuery, transformed }) => ({
|
|
900
1005
|
id,
|
|
901
1006
|
ast: transformed.transformedAst,
|
|
902
1007
|
transformationHash: transformed.transformationHash,
|
|
903
1008
|
name: origQuery.type === "custom" ? origQuery.name : void 0
|
|
904
1009
|
})).filter((q) => !removeQueriesQueryIds.has(q.id) && this.#pipelines.queries().get(q.id)?.transformationHash !== q.transformationHash);
|
|
905
|
-
|
|
1010
|
+
const hydrationOrder = new Map([...required, ...optional].map((query, index) => [query.id, index]));
|
|
1011
|
+
addQueries.sort((a, b) => must(hydrationOrder.get(a.id)) - must(hydrationOrder.get(b.id)));
|
|
1012
|
+
const requiredAddQueries = addQueries.filter((query) => requiredQueryIDs.has(query.id));
|
|
1013
|
+
const optionalAddQueries = addQueries.filter((query) => !requiredQueryIDs.has(query.id));
|
|
1014
|
+
lc.info?.(`syncQueryPipelineSet: ${cvrQueryEntires.length} CVR queries, ${transformedCustomQueryCount} custom re-transformed, ${erroredQueryIDs.length} errored, ${removeQueriesQueryIds.size} to remove, ${addQueries.length} to add`);
|
|
906
1015
|
for (const q of addQueries) {
|
|
907
1016
|
const orig = cvr.queries[q.id];
|
|
908
1017
|
lc.debug?.("ViewSyncer adding query", q.ast, "transformed from", orig.type === "custom" ? orig.name : orig.ast);
|
|
909
1018
|
}
|
|
910
1019
|
if (addQueries.length > 0 || removeQueriesQueryIds.size > 0) {
|
|
911
|
-
await this.#addAndRemoveQueries(lc, cvr,
|
|
1020
|
+
await this.#addAndRemoveQueries(lc, cvr, requiredAddQueries, optionalAddQueries, Array.from(removeQueriesQueryIds, (id) => ({ id })), hydrationBudget, hydrationPassStats, driftedQueryIDs);
|
|
912
1021
|
if (addQueries.length > 0) this.#viewSyncerHydration.recordMs(performance.now() - start);
|
|
913
1022
|
} else await this.#catchupClients(lc, cvr);
|
|
914
1023
|
});
|
|
@@ -941,8 +1050,27 @@ var ViewSyncerService = class {
|
|
|
941
1050
|
record.count++;
|
|
942
1051
|
if (record.count >= THRASH_THRESHOLD) this.#lc.warn?.(`Query thrashing detected for query ${queryID}. ${record.count} replacements in 60s. This may indicate clients with different auth contexts connecting to the same client group.`);
|
|
943
1052
|
}
|
|
944
|
-
#
|
|
1053
|
+
#recordHydrationBudgetExhaustion(lc, hydrationBudget, activeHydratedQueries, inactiveHydratedQueries, inactiveEvictedQueryIDs) {
|
|
1054
|
+
const elapsedMs = hydrationBudget.exhaustedAtMs;
|
|
1055
|
+
if (elapsedMs === void 0) return;
|
|
1056
|
+
this.#hydrationBudgetExhaustions.add(1);
|
|
1057
|
+
this.#hydrationBudgetEvictions.add(inactiveEvictedQueryIDs.length);
|
|
1058
|
+
this.#hydrationBudgetElapsed.record(elapsedMs);
|
|
1059
|
+
this.#hydrationBudgetOvershoot.record(Math.max(0, elapsedMs - hydrationBudget.limitMs));
|
|
1060
|
+
if (inactiveEvictedQueryIDs.length > 0) this.#queryEvictions.add(inactiveEvictedQueryIDs.length, { reason: "hydration-budget" });
|
|
1061
|
+
lc.info?.("view-syncer hydration budget exhausted", {
|
|
1062
|
+
clientGroupID: this.id,
|
|
1063
|
+
hydrationBudgetMs: hydrationBudget.limitMs,
|
|
1064
|
+
hydrationElapsedMs: elapsedMs,
|
|
1065
|
+
activeHydratedQueries,
|
|
1066
|
+
inactiveHydratedQueries,
|
|
1067
|
+
inactiveEvictedQueries: inactiveEvictedQueryIDs.length,
|
|
1068
|
+
firstEvictedQueryHash: inactiveEvictedQueryIDs[0] ?? null
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
#addAndRemoveQueries(lc, cvr, requiredQueries, optionalQueries, removeQueries, hydrationBudget, hydrationPassStats, driftedQueryIDs = /* @__PURE__ */ new Set()) {
|
|
945
1072
|
return startAsyncSpan(tracer, "vs.#addAndRemoveQueries", async () => {
|
|
1073
|
+
const addQueries = [...requiredQueries, ...optionalQueries];
|
|
946
1074
|
assert(addQueries.length > 0 || removeQueries.length > 0, "Must have queries to add or remove");
|
|
947
1075
|
const start = performance.now();
|
|
948
1076
|
const stateVersion = this.#pipelines.currentVersion();
|
|
@@ -959,6 +1087,7 @@ var ViewSyncerService = class {
|
|
|
959
1087
|
updater.ensureNewVersion();
|
|
960
1088
|
}
|
|
961
1089
|
const { queryPatches, newVersion } = updater.trackQueries(lc, addQueries, removeQueries);
|
|
1090
|
+
if (hydrationBudget.limitMs !== 0 && optionalQueries.length > 0) assert(cmpVersions(cvr.version, newVersion) < 0, "Optional hydration requires a final poke version before row processing so a removal is actually poked");
|
|
962
1091
|
const pokers = startPoke(this.#getClients(), newVersion);
|
|
963
1092
|
for (const patch of queryPatches) await pokers.addPatch(patch);
|
|
964
1093
|
for (const q of removeQueries) {
|
|
@@ -975,10 +1104,17 @@ var ViewSyncerService = class {
|
|
|
975
1104
|
let totalHydratedQueries = 0;
|
|
976
1105
|
let coveredHydratedQueries = 0;
|
|
977
1106
|
let firstCoveredQuery;
|
|
1107
|
+
const hydratedQueryIDs = [];
|
|
1108
|
+
const budgetEvictedQueryIDs = [];
|
|
978
1109
|
const self = this;
|
|
979
1110
|
await yieldProcess(lc);
|
|
980
|
-
function*
|
|
981
|
-
for (
|
|
1111
|
+
function* hydrateQueries(queries, optional, slowHydrateThreshold) {
|
|
1112
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1113
|
+
if (optional && hydrationBudget.exhausted()) {
|
|
1114
|
+
budgetEvictedQueryIDs.push(...queries.slice(i).map((query) => query.id));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const q = must(queries[i]);
|
|
982
1118
|
let queryLC = lc.withContext("hash", q.id).withContext("queryHash", q.id).withContext("transformationHash", q.transformationHash);
|
|
983
1119
|
if (q.name !== void 0) queryLC = queryLC.withContext("queryName", q.name);
|
|
984
1120
|
queryLC.debug?.(`adding pipeline for query`, q.ast);
|
|
@@ -991,6 +1127,9 @@ var ViewSyncerService = class {
|
|
|
991
1127
|
yield* pipelines.addQuery(q.transformationHash, q.id, q.ast, timer.startWithoutYielding(), q.name, "query-set-sync");
|
|
992
1128
|
const elapsed = timer.stop();
|
|
993
1129
|
totalProcessTime += elapsed;
|
|
1130
|
+
hydratedQueryIDs.push(q.id);
|
|
1131
|
+
if (optional) hydrationPassStats.inactiveHydratedQueries++;
|
|
1132
|
+
else hydrationPassStats.activeHydratedQueries++;
|
|
994
1133
|
self.#addQueryMaterializationServerMetric(q.id, elapsed);
|
|
995
1134
|
self.#inspectorDelegate.addQuery(q.id, q.ast);
|
|
996
1135
|
queryCoveringIndex?.add(q.id, {
|
|
@@ -1004,18 +1143,30 @@ var ViewSyncerService = class {
|
|
|
1004
1143
|
transformationHash: q.transformationHash,
|
|
1005
1144
|
...q.name !== void 0 && { name: q.name }
|
|
1006
1145
|
});
|
|
1146
|
+
hydrations.add(1);
|
|
1147
|
+
hydrationTime.recordMs(elapsed);
|
|
1007
1148
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1149
|
+
}
|
|
1150
|
+
function* generateRowChanges(slowHydrateThreshold) {
|
|
1151
|
+
yield* hydrateQueries(requiredQueries, false, slowHydrateThreshold);
|
|
1152
|
+
yield* hydrateQueries(optionalQueries, true, slowHydrateThreshold);
|
|
1010
1153
|
}
|
|
1011
1154
|
await this.#processChanges(lc, timer, generateRowChanges(this.#slowHydrateThreshold), updater, pokers);
|
|
1155
|
+
for (const patch of updater.removeTrackedQueries(budgetEvictedQueryIDs)) await pokers.addPatch(patch);
|
|
1156
|
+
for (const queryID of budgetEvictedQueryIDs) {
|
|
1157
|
+
this.#pipelines.removeQuery(queryID);
|
|
1158
|
+
this.#inspectorDelegate.removeQuery(queryID);
|
|
1159
|
+
this.#queryReplacements.delete(queryID);
|
|
1160
|
+
}
|
|
1161
|
+
this.#recordHydrationBudgetExhaustion(lc, hydrationBudget, hydrationPassStats.activeHydratedQueries, hydrationPassStats.inactiveHydratedQueries, budgetEvictedQueryIDs);
|
|
1012
1162
|
this.#logQueryCoverageShadowSummary(lc, "add", totalHydratedQueries, coveredHydratedQueries, firstCoveredQuery);
|
|
1013
1163
|
await startAsyncSpan(tracer, "vs.#syncQueryPipelineSet.deleteUnreferencedRows", async () => {
|
|
1014
1164
|
for (const patch of await updater.deleteUnreferencedRows(lc)) await pokers.addPatch(patch);
|
|
1015
1165
|
});
|
|
1016
1166
|
this.#cvr = await this.#flushUpdater(lc, updater);
|
|
1167
|
+
if (budgetEvictedQueryIDs.length > 0) this.#scheduleExpireEviction(lc, this.#cvr);
|
|
1017
1168
|
const finalVersion = this.#cvr.version;
|
|
1018
|
-
await this.#catchupClients(lc, cvr, finalVersion,
|
|
1169
|
+
await this.#catchupClients(lc, cvr, finalVersion, hydratedQueryIDs, pokers);
|
|
1019
1170
|
await startAsyncSpan(tracer, "vs.#syncQueryPipelineSet.pokeEnd", () => pokers.end(finalVersion));
|
|
1020
1171
|
this.#markVersionServed(stateVersion);
|
|
1021
1172
|
const wallTime = performance.now() - start;
|
|
@@ -1156,7 +1307,9 @@ var ViewSyncerService = class {
|
|
|
1156
1307
|
*
|
|
1157
1308
|
* Must be called from within the #lock.
|
|
1158
1309
|
*
|
|
1159
|
-
* Returns
|
|
1310
|
+
* Returns 'success' if changes were successfully processed and poked,
|
|
1311
|
+
* or a `ResetPipelinesSignal` if advancement aborted (e.g. schema change,
|
|
1312
|
+
* timeout) and pipelines need to be reset and rehydrated.
|
|
1160
1313
|
*/
|
|
1161
1314
|
#advancePipelines(lc, cvr) {
|
|
1162
1315
|
return startAsyncSpan(tracer, "vs.#advancePipelines", async (span) => {
|
|
@@ -1329,8 +1482,10 @@ function isTransformFailedError(error) {
|
|
|
1329
1482
|
return error.errorBody.kind === "TransformFailed" && !isAuthErrorBody(error.errorBody);
|
|
1330
1483
|
}
|
|
1331
1484
|
/**
|
|
1332
|
-
* A query must be expired for all clients
|
|
1333
|
-
* expired.
|
|
1485
|
+
* Whether a query's TTL has elapsed. A query must be expired for all clients
|
|
1486
|
+
* in order to be considered expired.
|
|
1487
|
+
*
|
|
1488
|
+
* Exported for testing.
|
|
1334
1489
|
*/
|
|
1335
1490
|
function expired(ttlClock, q) {
|
|
1336
1491
|
if (q.type === "internal") return false;
|