@syncular/server 0.0.6-202 → 0.0.6-205
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialect/types.d.ts +1 -0
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +7 -11
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +544 -502
- package/dist/pull.js.map +1 -1
- package/dist/subscriptions/cache.d.ts +3 -0
- package/dist/subscriptions/cache.d.ts.map +1 -1
- package/dist/subscriptions/cache.js +44 -0
- package/dist/subscriptions/cache.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +62 -35
- package/dist/subscriptions/resolve.js.map +1 -1
- package/package.json +2 -2
- package/src/dialect/types.ts +1 -0
- package/src/handlers/create-handler.ts +15 -15
- package/src/pull.ts +737 -660
- package/src/subscriptions/cache.ts +58 -0
- package/src/subscriptions/resolve.test.ts +71 -1
- package/src/subscriptions/resolve.ts +65 -38
package/dist/pull.js
CHANGED
|
@@ -9,6 +9,8 @@ const DEFAULT_MAX_SNAPSHOT_BUNDLE_ROW_FRAME_BYTES = 512 * 1024;
|
|
|
9
9
|
const MAX_ADAPTIVE_SNAPSHOT_BUNDLE_ROW_FRAME_BYTES = 4 * 1024 * 1024;
|
|
10
10
|
const DEFAULT_INLINE_SNAPSHOT_ROW_FRAME_BYTES = 256 * 1024;
|
|
11
11
|
const EMPTY_SNAPSHOT_ROW_FRAMES = encodeSnapshotRows([]);
|
|
12
|
+
const MAX_PULL_TRANSACTION_RETRIES = 2;
|
|
13
|
+
const PULL_TRANSACTION_RETRY_DELAY_MS = 15;
|
|
12
14
|
function createPullBootstrapTimings() {
|
|
13
15
|
return {
|
|
14
16
|
snapshotQueryMs: 0,
|
|
@@ -209,6 +211,16 @@ function sanitizeLimit(value, defaultValue, min, max) {
|
|
|
209
211
|
return defaultValue;
|
|
210
212
|
return Math.max(min, Math.min(max, value));
|
|
211
213
|
}
|
|
214
|
+
function isSerializablePullError(error) {
|
|
215
|
+
const withCode = error;
|
|
216
|
+
return (withCode.code === '40001' ||
|
|
217
|
+
error.message.toLowerCase().includes('could not serialize access'));
|
|
218
|
+
}
|
|
219
|
+
async function delay(ms) {
|
|
220
|
+
await new Promise((resolve) => {
|
|
221
|
+
setTimeout(resolve, ms);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
212
224
|
/**
|
|
213
225
|
* Merge all scope values into a flat ScopeValues for cursor tracking.
|
|
214
226
|
*/
|
|
@@ -341,8 +353,6 @@ export async function pull(args) {
|
|
|
341
353
|
const limitSnapshotRows = sanitizeLimit(request.limitSnapshotRows, 1000, 1, 20000);
|
|
342
354
|
const maxSnapshotPages = sanitizeLimit(request.maxSnapshotPages, 4, 1, 50);
|
|
343
355
|
const dedupeRows = request.dedupeRows === true;
|
|
344
|
-
const pendingExternalChunkWrites = [];
|
|
345
|
-
const bootstrapTimings = createPullBootstrapTimings();
|
|
346
356
|
// Resolve effective scopes for each subscription
|
|
347
357
|
const resolved = await resolveEffectiveScopesForSubscriptions({
|
|
348
358
|
db,
|
|
@@ -351,509 +361,491 @@ export async function pull(args) {
|
|
|
351
361
|
handlers: args.handlers,
|
|
352
362
|
scopeCache: args.scopeCache ?? defaultScopeCache,
|
|
353
363
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const subResponses = [];
|
|
363
|
-
const activeSubscriptions = [];
|
|
364
|
-
const nextCursors = [];
|
|
365
|
-
// Detect external data changes (synthetic commits from notifyExternalDataChange)
|
|
366
|
-
// Compute minimum cursor across all active subscriptions to scope the query.
|
|
367
|
-
let minSubCursor = Number.MAX_SAFE_INTEGER;
|
|
368
|
-
const activeTables = new Set();
|
|
369
|
-
for (const sub of resolved) {
|
|
370
|
-
if (sub.status === 'revoked' ||
|
|
371
|
-
Object.keys(sub.scopes).length === 0)
|
|
372
|
-
continue;
|
|
373
|
-
activeTables.add(sub.table);
|
|
374
|
-
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
375
|
-
if (cursor >= 0 && cursor < minSubCursor) {
|
|
376
|
-
minSubCursor = cursor;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const maxExternalCommitByTable = minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
380
|
-
? await readLatestExternalCommitByTable(trx, {
|
|
381
|
-
partitionId,
|
|
382
|
-
afterCursor: minSubCursor,
|
|
383
|
-
tables: Array.from(activeTables),
|
|
384
|
-
})
|
|
385
|
-
: new Map();
|
|
386
|
-
for (const sub of resolved) {
|
|
387
|
-
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
388
|
-
// Validate table handler exists (throws if not registered)
|
|
389
|
-
if (!args.handlers.byTable.has(sub.table)) {
|
|
390
|
-
throw new Error(`Unknown table: ${sub.table}`);
|
|
391
|
-
}
|
|
392
|
-
if (sub.status === 'revoked' ||
|
|
393
|
-
Object.keys(sub.scopes).length === 0) {
|
|
394
|
-
subResponses.push({
|
|
395
|
-
id: sub.id,
|
|
396
|
-
status: 'revoked',
|
|
397
|
-
scopes: {},
|
|
398
|
-
bootstrap: false,
|
|
399
|
-
nextCursor: cursor,
|
|
400
|
-
commits: [],
|
|
364
|
+
for (let attemptIndex = 0; attemptIndex < MAX_PULL_TRANSACTION_RETRIES; attemptIndex += 1) {
|
|
365
|
+
const pendingExternalChunkWrites = [];
|
|
366
|
+
const bootstrapTimings = createPullBootstrapTimings();
|
|
367
|
+
try {
|
|
368
|
+
const result = await dialect.executeInTransaction(db, async (trx) => {
|
|
369
|
+
await dialect.setRepeatableRead(trx);
|
|
370
|
+
const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
|
|
371
|
+
partitionId,
|
|
401
372
|
});
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
cursor
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
sub.
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
tables,
|
|
422
|
-
tableIndex: 0,
|
|
423
|
-
rowCursor: null,
|
|
424
|
-
};
|
|
425
|
-
const requestedState = sub.bootstrapState ?? null;
|
|
426
|
-
const state = requestedState &&
|
|
427
|
-
typeof requestedState.asOfCommitSeq === 'number' &&
|
|
428
|
-
Array.isArray(requestedState.tables) &&
|
|
429
|
-
typeof requestedState.tableIndex === 'number'
|
|
430
|
-
? requestedState
|
|
431
|
-
: initState;
|
|
432
|
-
// If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
|
|
433
|
-
const effectiveState = state.asOfCommitSeq < minCommitSeq - 1 ? initState : state;
|
|
434
|
-
const tableName = effectiveState.tables[effectiveState.tableIndex];
|
|
435
|
-
// No tables (or ran past the end): treat bootstrap as complete.
|
|
436
|
-
if (!tableName) {
|
|
437
|
-
subResponses.push({
|
|
438
|
-
id: sub.id,
|
|
439
|
-
status: 'active',
|
|
440
|
-
scopes: effectiveScopes,
|
|
441
|
-
bootstrap: true,
|
|
442
|
-
bootstrapState: null,
|
|
443
|
-
nextCursor: effectiveState.asOfCommitSeq,
|
|
444
|
-
commits: [],
|
|
445
|
-
snapshots: [],
|
|
446
|
-
});
|
|
447
|
-
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
448
|
-
continue;
|
|
373
|
+
const minCommitSeq = await dialect.readMinCommitSeq(trx, {
|
|
374
|
+
partitionId,
|
|
375
|
+
});
|
|
376
|
+
const subResponses = [];
|
|
377
|
+
const activeSubscriptions = [];
|
|
378
|
+
const nextCursors = [];
|
|
379
|
+
// Detect external data changes (synthetic commits from notifyExternalDataChange)
|
|
380
|
+
// Compute minimum cursor across all active subscriptions to scope the query.
|
|
381
|
+
let minSubCursor = Number.MAX_SAFE_INTEGER;
|
|
382
|
+
const activeTables = new Set();
|
|
383
|
+
for (const sub of resolved) {
|
|
384
|
+
if (sub.status === 'revoked' ||
|
|
385
|
+
Object.keys(sub.scopes).length === 0)
|
|
386
|
+
continue;
|
|
387
|
+
activeTables.add(sub.table);
|
|
388
|
+
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
389
|
+
if (cursor >= 0 && cursor < minSubCursor) {
|
|
390
|
+
minSubCursor = cursor;
|
|
391
|
+
}
|
|
449
392
|
}
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
393
|
+
const maxExternalCommitByTable = minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
394
|
+
? await readLatestExternalCommitByTable(trx, {
|
|
395
|
+
partitionId,
|
|
396
|
+
afterCursor: minSubCursor,
|
|
397
|
+
tables: Array.from(activeTables),
|
|
398
|
+
})
|
|
399
|
+
: new Map();
|
|
400
|
+
for (const sub of resolved) {
|
|
401
|
+
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
402
|
+
// Validate table handler exists (throws if not registered)
|
|
403
|
+
if (!args.handlers.byTable.has(sub.table)) {
|
|
404
|
+
throw new Error(`Unknown table: ${sub.table}`);
|
|
405
|
+
}
|
|
406
|
+
if (sub.status === 'revoked' ||
|
|
407
|
+
Object.keys(sub.scopes).length === 0) {
|
|
408
|
+
subResponses.push({
|
|
409
|
+
id: sub.id,
|
|
410
|
+
status: 'revoked',
|
|
411
|
+
scopes: {},
|
|
412
|
+
bootstrap: false,
|
|
413
|
+
nextCursor: cursor,
|
|
414
|
+
commits: [],
|
|
460
415
|
});
|
|
461
|
-
|
|
416
|
+
continue;
|
|
462
417
|
}
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
418
|
+
const effectiveScopes = sub.scopes;
|
|
419
|
+
activeSubscriptions.push({ scopes: effectiveScopes });
|
|
420
|
+
const latestExternalCommitForTable = maxExternalCommitByTable.get(sub.table);
|
|
421
|
+
const needsBootstrap = sub.bootstrapState != null ||
|
|
422
|
+
cursor < 0 ||
|
|
423
|
+
cursor > maxCommitSeq ||
|
|
424
|
+
(minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
|
|
425
|
+
(latestExternalCommitForTable !== undefined &&
|
|
426
|
+
latestExternalCommitForTable > cursor);
|
|
427
|
+
if (needsBootstrap) {
|
|
428
|
+
const tables = getServerBootstrapOrderFor(args.handlers, sub.table).map((handler) => handler.table);
|
|
429
|
+
const preferInlineBootstrapSnapshot = cursor >= 0 ||
|
|
430
|
+
sub.bootstrapState != null ||
|
|
431
|
+
(latestExternalCommitForTable !== undefined &&
|
|
432
|
+
latestExternalCommitForTable > cursor);
|
|
433
|
+
const initState = {
|
|
434
|
+
asOfCommitSeq: maxCommitSeq,
|
|
435
|
+
tables,
|
|
436
|
+
tableIndex: 0,
|
|
437
|
+
rowCursor: null,
|
|
438
|
+
};
|
|
439
|
+
const requestedState = sub.bootstrapState ?? null;
|
|
440
|
+
const state = requestedState &&
|
|
441
|
+
typeof requestedState.asOfCommitSeq === 'number' &&
|
|
442
|
+
Array.isArray(requestedState.tables) &&
|
|
443
|
+
typeof requestedState.tableIndex === 'number'
|
|
444
|
+
? requestedState
|
|
445
|
+
: initState;
|
|
446
|
+
// If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
|
|
447
|
+
const effectiveState = state.asOfCommitSeq < minCommitSeq - 1
|
|
448
|
+
? initState
|
|
449
|
+
: state;
|
|
450
|
+
const tableName = effectiveState.tables[effectiveState.tableIndex];
|
|
451
|
+
// No tables (or ran past the end): treat bootstrap as complete.
|
|
452
|
+
if (!tableName) {
|
|
453
|
+
subResponses.push({
|
|
454
|
+
id: sub.id,
|
|
455
|
+
status: 'active',
|
|
456
|
+
scopes: effectiveScopes,
|
|
457
|
+
bootstrap: true,
|
|
458
|
+
bootstrapState: null,
|
|
459
|
+
nextCursor: effectiveState.asOfCommitSeq,
|
|
460
|
+
commits: [],
|
|
461
|
+
snapshots: [],
|
|
462
|
+
});
|
|
463
|
+
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const snapshots = [];
|
|
467
|
+
let nextState = effectiveState;
|
|
468
|
+
const cacheKey = `${partitionId}:${await scopesToSnapshotChunkScopeKey(effectiveScopes)}`;
|
|
469
|
+
const flushSnapshotBundle = async (bundle) => {
|
|
470
|
+
if (bundle.inlineRows) {
|
|
471
|
+
snapshots.push({
|
|
472
|
+
table: bundle.table,
|
|
473
|
+
rows: bundle.inlineRows,
|
|
474
|
+
isFirstPage: bundle.isFirstPage,
|
|
475
|
+
isLastPage: bundle.isLastPage,
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const nowIso = new Date().toISOString();
|
|
480
|
+
const bundleRowLimit = Math.max(1, limitSnapshotRows * bundle.pageCount);
|
|
481
|
+
const cacheLookupStartedAt = Date.now();
|
|
482
|
+
const cached = await readSnapshotChunkRefByPageKey(trx, {
|
|
483
|
+
partitionId,
|
|
484
|
+
scopeKey: cacheKey,
|
|
485
|
+
scope: bundle.table,
|
|
486
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
487
|
+
rowCursor: bundle.startCursor,
|
|
488
|
+
rowLimit: bundleRowLimit,
|
|
489
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
490
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
491
|
+
nowIso,
|
|
492
|
+
});
|
|
493
|
+
bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
|
|
494
|
+
let chunkRef = cached;
|
|
495
|
+
if (!chunkRef) {
|
|
496
|
+
const expiresAt = new Date(Date.now() + Math.max(1000, bundle.ttlMs)).toISOString();
|
|
497
|
+
if (args.chunkStorage) {
|
|
498
|
+
const snapshot = {
|
|
499
|
+
table: bundle.table,
|
|
500
|
+
rows: [],
|
|
501
|
+
chunks: [],
|
|
502
|
+
isFirstPage: bundle.isFirstPage,
|
|
503
|
+
isLastPage: bundle.isLastPage,
|
|
504
|
+
};
|
|
505
|
+
snapshots.push(snapshot);
|
|
506
|
+
pendingExternalChunkWrites.push({
|
|
507
|
+
snapshot,
|
|
508
|
+
cacheLookup: {
|
|
509
|
+
partitionId,
|
|
510
|
+
scopeKey: cacheKey,
|
|
511
|
+
scope: bundle.table,
|
|
512
|
+
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
513
|
+
rowCursor: bundle.startCursor,
|
|
514
|
+
rowLimit: bundleRowLimit,
|
|
515
|
+
},
|
|
516
|
+
rowFrameParts: [...bundle.rowFrameParts],
|
|
517
|
+
expiresAt,
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const encodedChunk = await encodeCompressedSnapshotChunk(bundle.rowFrameParts);
|
|
522
|
+
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
523
|
+
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
524
|
+
const chunkId = randomId();
|
|
525
|
+
const chunkPersistStartedAt = Date.now();
|
|
526
|
+
chunkRef = await insertSnapshotChunk(trx, {
|
|
527
|
+
chunkId,
|
|
493
528
|
partitionId,
|
|
494
529
|
scopeKey: cacheKey,
|
|
495
530
|
scope: bundle.table,
|
|
496
531
|
asOfCommitSeq: effectiveState.asOfCommitSeq,
|
|
497
532
|
rowCursor: bundle.startCursor,
|
|
498
533
|
rowLimit: bundleRowLimit,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
534
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
535
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
536
|
+
sha256: encodedChunk.sha256,
|
|
537
|
+
body: encodedChunk.body,
|
|
538
|
+
expiresAt,
|
|
539
|
+
});
|
|
540
|
+
bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
|
|
541
|
+
}
|
|
542
|
+
snapshots.push({
|
|
543
|
+
table: bundle.table,
|
|
544
|
+
rows: [],
|
|
545
|
+
chunks: [chunkRef],
|
|
546
|
+
isFirstPage: bundle.isFirstPage,
|
|
547
|
+
isLastPage: bundle.isLastPage,
|
|
502
548
|
});
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
549
|
+
};
|
|
550
|
+
let activeBundle = null;
|
|
551
|
+
for (let pageIndex = 0; pageIndex < maxSnapshotPages; pageIndex++) {
|
|
552
|
+
if (!nextState)
|
|
553
|
+
break;
|
|
554
|
+
const nextTableName = nextState.tables[nextState.tableIndex];
|
|
555
|
+
if (!nextTableName) {
|
|
556
|
+
if (activeBundle) {
|
|
557
|
+
activeBundle.isLastPage = true;
|
|
558
|
+
await flushSnapshotBundle(activeBundle);
|
|
559
|
+
activeBundle = null;
|
|
560
|
+
}
|
|
561
|
+
nextState = null;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
const tableHandler = args.handlers.byTable.get(nextTableName);
|
|
565
|
+
if (!tableHandler) {
|
|
566
|
+
throw new Error(`Unknown table: ${nextTableName}`);
|
|
567
|
+
}
|
|
568
|
+
if (!activeBundle ||
|
|
569
|
+
activeBundle.table !== nextTableName) {
|
|
570
|
+
if (activeBundle) {
|
|
571
|
+
await flushSnapshotBundle(activeBundle);
|
|
572
|
+
}
|
|
573
|
+
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
574
|
+
activeBundle = {
|
|
575
|
+
table: nextTableName,
|
|
576
|
+
startCursor: nextState.rowCursor,
|
|
577
|
+
isFirstPage: nextState.rowCursor == null,
|
|
578
|
+
isLastPage: false,
|
|
579
|
+
pageCount: 0,
|
|
580
|
+
ttlMs: tableHandler.snapshotChunkTtlMs ??
|
|
581
|
+
24 * 60 * 60 * 1000,
|
|
582
|
+
rowFrameByteLength: bundleHeader.length,
|
|
583
|
+
rowFrameParts: [bundleHeader],
|
|
584
|
+
inlineRows: null,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const snapshotQueryStartedAt = Date.now();
|
|
588
|
+
const page = await tableHandler.snapshot({
|
|
589
|
+
db: trx,
|
|
590
|
+
actorId: args.auth.actorId,
|
|
591
|
+
auth: args.auth,
|
|
592
|
+
scopeValues: effectiveScopes,
|
|
593
|
+
cursor: nextState.rowCursor,
|
|
594
|
+
limit: limitSnapshotRows,
|
|
595
|
+
}, sub.params);
|
|
596
|
+
bootstrapTimings.snapshotQueryMs += Math.max(0, Date.now() - snapshotQueryStartedAt);
|
|
597
|
+
const rowFrameEncodeStartedAt = Date.now();
|
|
598
|
+
const rowFrames = encodeSnapshotRowFrames(page.rows ?? []);
|
|
599
|
+
bootstrapTimings.rowFrameEncodeMs += Math.max(0, Date.now() - rowFrameEncodeStartedAt);
|
|
600
|
+
const bundleMaxBytes = resolveSnapshotBundleMaxBytes({
|
|
601
|
+
configuredMaxBytes: tableHandler.snapshotBundleMaxBytes,
|
|
602
|
+
pageRowCount: page.rows?.length ?? 0,
|
|
603
|
+
pageRowFrameBytes: rowFrames.length,
|
|
604
|
+
});
|
|
605
|
+
if (activeBundle.pageCount > 0 &&
|
|
606
|
+
activeBundle.rowFrameByteLength + rowFrames.length >
|
|
607
|
+
bundleMaxBytes) {
|
|
608
|
+
await flushSnapshotBundle(activeBundle);
|
|
609
|
+
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
610
|
+
activeBundle = {
|
|
611
|
+
table: nextTableName,
|
|
612
|
+
startCursor: nextState.rowCursor,
|
|
613
|
+
isFirstPage: nextState.rowCursor == null,
|
|
614
|
+
isLastPage: false,
|
|
615
|
+
pageCount: 0,
|
|
616
|
+
ttlMs: tableHandler.snapshotChunkTtlMs ??
|
|
617
|
+
24 * 60 * 60 * 1000,
|
|
618
|
+
rowFrameByteLength: bundleHeader.length,
|
|
619
|
+
rowFrameParts: [bundleHeader],
|
|
620
|
+
inlineRows: null,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (preferInlineBootstrapSnapshot &&
|
|
624
|
+
activeBundle.pageCount === 0 &&
|
|
625
|
+
page.nextCursor == null &&
|
|
626
|
+
rowFrames.length <=
|
|
627
|
+
DEFAULT_INLINE_SNAPSHOT_ROW_FRAME_BYTES) {
|
|
628
|
+
activeBundle.inlineRows = page.rows ?? [];
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
activeBundle.inlineRows = null;
|
|
632
|
+
}
|
|
633
|
+
activeBundle.rowFrameParts.push(rowFrames);
|
|
634
|
+
activeBundle.rowFrameByteLength += rowFrames.length;
|
|
635
|
+
activeBundle.pageCount += 1;
|
|
636
|
+
if (page.nextCursor != null) {
|
|
637
|
+
nextState = {
|
|
638
|
+
...nextState,
|
|
639
|
+
rowCursor: page.nextCursor,
|
|
640
|
+
};
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
541
643
|
activeBundle.isLastPage = true;
|
|
542
644
|
await flushSnapshotBundle(activeBundle);
|
|
543
645
|
activeBundle = null;
|
|
646
|
+
if (nextState.tableIndex + 1 < nextState.tables.length) {
|
|
647
|
+
nextState = {
|
|
648
|
+
...nextState,
|
|
649
|
+
tableIndex: nextState.tableIndex + 1,
|
|
650
|
+
rowCursor: null,
|
|
651
|
+
};
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
nextState = null;
|
|
655
|
+
break;
|
|
544
656
|
}
|
|
545
|
-
nextState = null;
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
const tableHandler = args.handlers.byTable.get(nextTableName);
|
|
549
|
-
if (!tableHandler) {
|
|
550
|
-
throw new Error(`Unknown table: ${nextTableName}`);
|
|
551
|
-
}
|
|
552
|
-
if (!activeBundle || activeBundle.table !== nextTableName) {
|
|
553
657
|
if (activeBundle) {
|
|
554
658
|
await flushSnapshotBundle(activeBundle);
|
|
555
659
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
const snapshotQueryStartedAt = Date.now();
|
|
570
|
-
const page = await tableHandler.snapshot({
|
|
571
|
-
db: trx,
|
|
572
|
-
actorId: args.auth.actorId,
|
|
573
|
-
auth: args.auth,
|
|
574
|
-
scopeValues: effectiveScopes,
|
|
575
|
-
cursor: nextState.rowCursor,
|
|
576
|
-
limit: limitSnapshotRows,
|
|
577
|
-
}, sub.params);
|
|
578
|
-
bootstrapTimings.snapshotQueryMs += Math.max(0, Date.now() - snapshotQueryStartedAt);
|
|
579
|
-
const rowFrameEncodeStartedAt = Date.now();
|
|
580
|
-
const rowFrames = encodeSnapshotRowFrames(page.rows ?? []);
|
|
581
|
-
bootstrapTimings.rowFrameEncodeMs += Math.max(0, Date.now() - rowFrameEncodeStartedAt);
|
|
582
|
-
const bundleMaxBytes = resolveSnapshotBundleMaxBytes({
|
|
583
|
-
configuredMaxBytes: tableHandler.snapshotBundleMaxBytes,
|
|
584
|
-
pageRowCount: page.rows?.length ?? 0,
|
|
585
|
-
pageRowFrameBytes: rowFrames.length,
|
|
586
|
-
});
|
|
587
|
-
if (activeBundle.pageCount > 0 &&
|
|
588
|
-
activeBundle.rowFrameByteLength + rowFrames.length >
|
|
589
|
-
bundleMaxBytes) {
|
|
590
|
-
await flushSnapshotBundle(activeBundle);
|
|
591
|
-
const bundleHeader = EMPTY_SNAPSHOT_ROW_FRAMES;
|
|
592
|
-
activeBundle = {
|
|
593
|
-
table: nextTableName,
|
|
594
|
-
startCursor: nextState.rowCursor,
|
|
595
|
-
isFirstPage: nextState.rowCursor == null,
|
|
596
|
-
isLastPage: false,
|
|
597
|
-
pageCount: 0,
|
|
598
|
-
ttlMs: tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
|
|
599
|
-
rowFrameByteLength: bundleHeader.length,
|
|
600
|
-
rowFrameParts: [bundleHeader],
|
|
601
|
-
inlineRows: null,
|
|
602
|
-
};
|
|
660
|
+
subResponses.push({
|
|
661
|
+
id: sub.id,
|
|
662
|
+
status: 'active',
|
|
663
|
+
scopes: effectiveScopes,
|
|
664
|
+
bootstrap: true,
|
|
665
|
+
bootstrapState: nextState,
|
|
666
|
+
nextCursor: effectiveState.asOfCommitSeq,
|
|
667
|
+
commits: [],
|
|
668
|
+
snapshots,
|
|
669
|
+
});
|
|
670
|
+
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
671
|
+
continue;
|
|
603
672
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
673
|
+
// Incremental pull for this subscription. The dialect row query
|
|
674
|
+
// carries the scanned commit-window max when matching rows exist,
|
|
675
|
+
// so we only need a separate commit-window scan when the row query
|
|
676
|
+
// returns no matches at all.
|
|
677
|
+
const incrementalRows = [];
|
|
678
|
+
let maxScannedCommitSeq = cursor;
|
|
679
|
+
for await (const row of dialect.iterateIncrementalPullRows(trx, {
|
|
680
|
+
partitionId,
|
|
681
|
+
table: sub.table,
|
|
682
|
+
scopes: effectiveScopes,
|
|
683
|
+
cursor,
|
|
684
|
+
limitCommits,
|
|
685
|
+
})) {
|
|
686
|
+
incrementalRows.push(row);
|
|
687
|
+
maxScannedCommitSeq = Math.max(maxScannedCommitSeq, row.scanned_max_commit_seq ?? row.commit_seq);
|
|
609
688
|
}
|
|
610
|
-
|
|
611
|
-
|
|
689
|
+
if (incrementalRows.length === 0) {
|
|
690
|
+
const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
|
|
691
|
+
partitionId,
|
|
692
|
+
cursor,
|
|
693
|
+
limitCommits,
|
|
694
|
+
tables: [sub.table],
|
|
695
|
+
});
|
|
696
|
+
maxScannedCommitSeq =
|
|
697
|
+
scannedCommitSeqs.length > 0
|
|
698
|
+
? scannedCommitSeqs[scannedCommitSeqs.length - 1]
|
|
699
|
+
: cursor;
|
|
700
|
+
if (scannedCommitSeqs.length === 0) {
|
|
701
|
+
subResponses.push({
|
|
702
|
+
id: sub.id,
|
|
703
|
+
status: 'active',
|
|
704
|
+
scopes: effectiveScopes,
|
|
705
|
+
bootstrap: false,
|
|
706
|
+
nextCursor: cursor,
|
|
707
|
+
commits: [],
|
|
708
|
+
});
|
|
709
|
+
nextCursors.push(cursor);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
612
712
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
713
|
+
let nextCursor = cursor;
|
|
714
|
+
if (dedupeRows) {
|
|
715
|
+
const latestByRowKey = new Map();
|
|
716
|
+
for (const r of incrementalRows) {
|
|
717
|
+
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
718
|
+
const rowKey = `${r.table}\u0000${r.row_id}`;
|
|
719
|
+
const change = {
|
|
720
|
+
table: r.table,
|
|
721
|
+
row_id: r.row_id,
|
|
722
|
+
op: r.op,
|
|
723
|
+
row_json: r.row_json,
|
|
724
|
+
row_version: r.row_version,
|
|
725
|
+
scopes: r.scopes,
|
|
726
|
+
};
|
|
727
|
+
// Move row keys to insertion tail so Map iteration yields
|
|
728
|
+
// "latest change wins" order without a full array sort.
|
|
729
|
+
if (latestByRowKey.has(rowKey)) {
|
|
730
|
+
latestByRowKey.delete(rowKey);
|
|
731
|
+
}
|
|
732
|
+
latestByRowKey.set(rowKey, {
|
|
733
|
+
commitSeq: r.commit_seq,
|
|
734
|
+
createdAt: r.created_at,
|
|
735
|
+
actorId: r.actor_id,
|
|
736
|
+
change,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
740
|
+
if (latestByRowKey.size === 0) {
|
|
741
|
+
subResponses.push({
|
|
742
|
+
id: sub.id,
|
|
743
|
+
status: 'active',
|
|
744
|
+
scopes: effectiveScopes,
|
|
745
|
+
bootstrap: false,
|
|
746
|
+
nextCursor,
|
|
747
|
+
commits: [],
|
|
748
|
+
});
|
|
749
|
+
nextCursors.push(nextCursor);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const commits = [];
|
|
753
|
+
for (const item of latestByRowKey.values()) {
|
|
754
|
+
const lastCommit = commits[commits.length - 1];
|
|
755
|
+
if (!lastCommit ||
|
|
756
|
+
lastCommit.commitSeq !== item.commitSeq) {
|
|
757
|
+
commits.push({
|
|
758
|
+
commitSeq: item.commitSeq,
|
|
759
|
+
createdAt: item.createdAt,
|
|
760
|
+
actorId: item.actorId,
|
|
761
|
+
changes: [item.change],
|
|
762
|
+
});
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
lastCommit.changes.push(item.change);
|
|
766
|
+
}
|
|
767
|
+
subResponses.push({
|
|
768
|
+
id: sub.id,
|
|
769
|
+
status: 'active',
|
|
770
|
+
scopes: effectiveScopes,
|
|
771
|
+
bootstrap: false,
|
|
772
|
+
nextCursor,
|
|
773
|
+
commits,
|
|
774
|
+
});
|
|
775
|
+
nextCursors.push(nextCursor);
|
|
618
776
|
continue;
|
|
619
777
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
778
|
+
const commitsBySeq = new Map();
|
|
779
|
+
const commitSeqs = [];
|
|
780
|
+
for (const r of incrementalRows) {
|
|
781
|
+
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
782
|
+
const seq = r.commit_seq;
|
|
783
|
+
let commit = commitsBySeq.get(seq);
|
|
784
|
+
if (!commit) {
|
|
785
|
+
commit = {
|
|
786
|
+
commitSeq: seq,
|
|
787
|
+
createdAt: r.created_at,
|
|
788
|
+
actorId: r.actor_id,
|
|
789
|
+
changes: [],
|
|
790
|
+
};
|
|
791
|
+
commitsBySeq.set(seq, commit);
|
|
792
|
+
commitSeqs.push(seq);
|
|
793
|
+
}
|
|
794
|
+
const change = {
|
|
795
|
+
table: r.table,
|
|
796
|
+
row_id: r.row_id,
|
|
797
|
+
op: r.op,
|
|
798
|
+
row_json: r.row_json,
|
|
799
|
+
row_version: r.row_version,
|
|
800
|
+
scopes: r.scopes,
|
|
628
801
|
};
|
|
629
|
-
|
|
802
|
+
commit.changes.push(change);
|
|
630
803
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
nextCursor: effectiveState.asOfCommitSeq,
|
|
644
|
-
commits: [],
|
|
645
|
-
snapshots,
|
|
646
|
-
});
|
|
647
|
-
nextCursors.push(effectiveState.asOfCommitSeq);
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
// Incremental pull for this subscription
|
|
651
|
-
// Read the commit window for this table up-front so the subscription cursor
|
|
652
|
-
// can advance past commits that don't match the requested scopes.
|
|
653
|
-
const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
|
|
654
|
-
partitionId,
|
|
655
|
-
cursor,
|
|
656
|
-
limitCommits,
|
|
657
|
-
tables: [sub.table],
|
|
658
|
-
});
|
|
659
|
-
const maxScannedCommitSeq = scannedCommitSeqs.length > 0
|
|
660
|
-
? scannedCommitSeqs[scannedCommitSeqs.length - 1]
|
|
661
|
-
: cursor;
|
|
662
|
-
if (scannedCommitSeqs.length === 0) {
|
|
663
|
-
subResponses.push({
|
|
664
|
-
id: sub.id,
|
|
665
|
-
status: 'active',
|
|
666
|
-
scopes: effectiveScopes,
|
|
667
|
-
bootstrap: false,
|
|
668
|
-
nextCursor: cursor,
|
|
669
|
-
commits: [],
|
|
670
|
-
});
|
|
671
|
-
nextCursors.push(cursor);
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
let nextCursor = cursor;
|
|
675
|
-
if (dedupeRows) {
|
|
676
|
-
const latestByRowKey = new Map();
|
|
677
|
-
for await (const r of dialect.iterateIncrementalPullRows(trx, {
|
|
678
|
-
partitionId,
|
|
679
|
-
table: sub.table,
|
|
680
|
-
scopes: effectiveScopes,
|
|
681
|
-
cursor,
|
|
682
|
-
limitCommits,
|
|
683
|
-
})) {
|
|
684
|
-
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
685
|
-
const rowKey = `${r.table}\u0000${r.row_id}`;
|
|
686
|
-
const change = {
|
|
687
|
-
table: r.table,
|
|
688
|
-
row_id: r.row_id,
|
|
689
|
-
op: r.op,
|
|
690
|
-
row_json: r.row_json,
|
|
691
|
-
row_version: r.row_version,
|
|
692
|
-
scopes: r.scopes,
|
|
693
|
-
};
|
|
694
|
-
// Move row keys to insertion tail so Map iteration yields
|
|
695
|
-
// "latest change wins" order without a full array sort.
|
|
696
|
-
if (latestByRowKey.has(rowKey)) {
|
|
697
|
-
latestByRowKey.delete(rowKey);
|
|
804
|
+
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
805
|
+
if (commitSeqs.length === 0) {
|
|
806
|
+
subResponses.push({
|
|
807
|
+
id: sub.id,
|
|
808
|
+
status: 'active',
|
|
809
|
+
scopes: effectiveScopes,
|
|
810
|
+
bootstrap: false,
|
|
811
|
+
nextCursor,
|
|
812
|
+
commits: [],
|
|
813
|
+
});
|
|
814
|
+
nextCursors.push(nextCursor);
|
|
815
|
+
continue;
|
|
698
816
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
change,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
707
|
-
if (latestByRowKey.size === 0) {
|
|
817
|
+
const commits = commitSeqs
|
|
818
|
+
.map((seq) => commitsBySeq.get(seq))
|
|
819
|
+
.filter((c) => !!c)
|
|
820
|
+
.filter((c) => c.changes.length > 0);
|
|
708
821
|
subResponses.push({
|
|
709
822
|
id: sub.id,
|
|
710
823
|
status: 'active',
|
|
711
824
|
scopes: effectiveScopes,
|
|
712
825
|
bootstrap: false,
|
|
713
826
|
nextCursor,
|
|
714
|
-
commits
|
|
827
|
+
commits,
|
|
715
828
|
});
|
|
716
829
|
nextCursors.push(nextCursor);
|
|
717
|
-
continue;
|
|
718
830
|
}
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
lastCommit.changes.push(item.change);
|
|
732
|
-
}
|
|
733
|
-
subResponses.push({
|
|
734
|
-
id: sub.id,
|
|
735
|
-
status: 'active',
|
|
736
|
-
scopes: effectiveScopes,
|
|
737
|
-
bootstrap: false,
|
|
738
|
-
nextCursor,
|
|
739
|
-
commits,
|
|
740
|
-
});
|
|
741
|
-
nextCursors.push(nextCursor);
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
const commitsBySeq = new Map();
|
|
745
|
-
const commitSeqs = [];
|
|
746
|
-
for await (const r of dialect.iterateIncrementalPullRows(trx, {
|
|
747
|
-
partitionId,
|
|
748
|
-
table: sub.table,
|
|
749
|
-
scopes: effectiveScopes,
|
|
750
|
-
cursor,
|
|
751
|
-
limitCommits,
|
|
752
|
-
})) {
|
|
753
|
-
nextCursor = Math.max(nextCursor, r.commit_seq);
|
|
754
|
-
const seq = r.commit_seq;
|
|
755
|
-
let commit = commitsBySeq.get(seq);
|
|
756
|
-
if (!commit) {
|
|
757
|
-
commit = {
|
|
758
|
-
commitSeq: seq,
|
|
759
|
-
createdAt: r.created_at,
|
|
760
|
-
actorId: r.actor_id,
|
|
761
|
-
changes: [],
|
|
762
|
-
};
|
|
763
|
-
commitsBySeq.set(seq, commit);
|
|
764
|
-
commitSeqs.push(seq);
|
|
765
|
-
}
|
|
766
|
-
const change = {
|
|
767
|
-
table: r.table,
|
|
768
|
-
row_id: r.row_id,
|
|
769
|
-
op: r.op,
|
|
770
|
-
row_json: r.row_json,
|
|
771
|
-
row_version: r.row_version,
|
|
772
|
-
scopes: r.scopes,
|
|
831
|
+
const effectiveScopes = mergeScopes(activeSubscriptions);
|
|
832
|
+
const clientCursor = nextCursors.length > 0
|
|
833
|
+
? Math.min(...nextCursors)
|
|
834
|
+
: maxCommitSeq;
|
|
835
|
+
return {
|
|
836
|
+
response: {
|
|
837
|
+
ok: true,
|
|
838
|
+
subscriptions: subResponses,
|
|
839
|
+
},
|
|
840
|
+
effectiveScopes,
|
|
841
|
+
clientCursor,
|
|
773
842
|
};
|
|
774
|
-
commit.changes.push(change);
|
|
775
|
-
}
|
|
776
|
-
nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
|
|
777
|
-
if (commitSeqs.length === 0) {
|
|
778
|
-
subResponses.push({
|
|
779
|
-
id: sub.id,
|
|
780
|
-
status: 'active',
|
|
781
|
-
scopes: effectiveScopes,
|
|
782
|
-
bootstrap: false,
|
|
783
|
-
nextCursor,
|
|
784
|
-
commits: [],
|
|
785
|
-
});
|
|
786
|
-
nextCursors.push(nextCursor);
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
const commits = commitSeqs
|
|
790
|
-
.map((seq) => commitsBySeq.get(seq))
|
|
791
|
-
.filter((c) => !!c)
|
|
792
|
-
.filter((c) => c.changes.length > 0);
|
|
793
|
-
subResponses.push({
|
|
794
|
-
id: sub.id,
|
|
795
|
-
status: 'active',
|
|
796
|
-
scopes: effectiveScopes,
|
|
797
|
-
bootstrap: false,
|
|
798
|
-
nextCursor,
|
|
799
|
-
commits,
|
|
800
843
|
});
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
response: {
|
|
807
|
-
ok: true,
|
|
808
|
-
subscriptions: subResponses,
|
|
809
|
-
},
|
|
810
|
-
effectiveScopes,
|
|
811
|
-
clientCursor,
|
|
812
|
-
};
|
|
813
|
-
});
|
|
814
|
-
const chunkStorage = args.chunkStorage;
|
|
815
|
-
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
816
|
-
await runWithConcurrency(pendingExternalChunkWrites, 4, async (pending) => {
|
|
817
|
-
const cacheLookupStartedAt = Date.now();
|
|
818
|
-
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
819
|
-
partitionId: pending.cacheLookup.partitionId,
|
|
820
|
-
scopeKey: pending.cacheLookup.scopeKey,
|
|
821
|
-
scope: pending.cacheLookup.scope,
|
|
822
|
-
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
823
|
-
rowCursor: pending.cacheLookup.rowCursor,
|
|
824
|
-
rowLimit: pending.cacheLookup.rowLimit,
|
|
825
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
826
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
827
|
-
});
|
|
828
|
-
bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
|
|
829
|
-
if (!chunkRef) {
|
|
830
|
-
if (chunkStorage.storeChunkStream) {
|
|
831
|
-
const { stream: bodyStream, byteLength, sha256, gzipMs, hashMs, } = await encodeCompressedSnapshotChunkToStream(pending.rowFrameParts);
|
|
832
|
-
bootstrapTimings.chunkGzipMs += gzipMs;
|
|
833
|
-
bootstrapTimings.chunkHashMs += hashMs;
|
|
834
|
-
const chunkPersistStartedAt = Date.now();
|
|
835
|
-
chunkRef = await chunkStorage.storeChunkStream({
|
|
836
|
-
partitionId: pending.cacheLookup.partitionId,
|
|
837
|
-
scopeKey: pending.cacheLookup.scopeKey,
|
|
838
|
-
scope: pending.cacheLookup.scope,
|
|
839
|
-
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
840
|
-
rowCursor: pending.cacheLookup.rowCursor,
|
|
841
|
-
rowLimit: pending.cacheLookup.rowLimit,
|
|
842
|
-
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
843
|
-
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
844
|
-
sha256,
|
|
845
|
-
byteLength,
|
|
846
|
-
bodyStream,
|
|
847
|
-
expiresAt: pending.expiresAt,
|
|
848
|
-
});
|
|
849
|
-
bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
const encodedChunk = await encodeCompressedSnapshotChunk(pending.rowFrameParts);
|
|
853
|
-
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
854
|
-
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
855
|
-
const chunkPersistStartedAt = Date.now();
|
|
856
|
-
chunkRef = await chunkStorage.storeChunk({
|
|
844
|
+
const chunkStorage = args.chunkStorage;
|
|
845
|
+
if (chunkStorage && pendingExternalChunkWrites.length > 0) {
|
|
846
|
+
await runWithConcurrency(pendingExternalChunkWrites, 4, async (pending) => {
|
|
847
|
+
const cacheLookupStartedAt = Date.now();
|
|
848
|
+
let chunkRef = await readSnapshotChunkRefByPageKey(db, {
|
|
857
849
|
partitionId: pending.cacheLookup.partitionId,
|
|
858
850
|
scopeKey: pending.cacheLookup.scopeKey,
|
|
859
851
|
scope: pending.cacheLookup.scope,
|
|
@@ -862,43 +854,93 @@ export async function pull(args) {
|
|
|
862
854
|
rowLimit: pending.cacheLookup.rowLimit,
|
|
863
855
|
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
864
856
|
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
865
|
-
sha256: encodedChunk.sha256,
|
|
866
|
-
body: encodedChunk.body,
|
|
867
|
-
expiresAt: pending.expiresAt,
|
|
868
857
|
});
|
|
869
|
-
bootstrapTimings.
|
|
870
|
-
|
|
858
|
+
bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
|
|
859
|
+
if (!chunkRef) {
|
|
860
|
+
if (chunkStorage.storeChunkStream) {
|
|
861
|
+
const { stream: bodyStream, byteLength, sha256, gzipMs, hashMs, } = await encodeCompressedSnapshotChunkToStream(pending.rowFrameParts);
|
|
862
|
+
bootstrapTimings.chunkGzipMs += gzipMs;
|
|
863
|
+
bootstrapTimings.chunkHashMs += hashMs;
|
|
864
|
+
const chunkPersistStartedAt = Date.now();
|
|
865
|
+
chunkRef = await chunkStorage.storeChunkStream({
|
|
866
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
867
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
868
|
+
scope: pending.cacheLookup.scope,
|
|
869
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
870
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
871
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
872
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
873
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
874
|
+
sha256,
|
|
875
|
+
byteLength,
|
|
876
|
+
bodyStream,
|
|
877
|
+
expiresAt: pending.expiresAt,
|
|
878
|
+
});
|
|
879
|
+
bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
const encodedChunk = await encodeCompressedSnapshotChunk(pending.rowFrameParts);
|
|
883
|
+
bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
|
|
884
|
+
bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
|
|
885
|
+
const chunkPersistStartedAt = Date.now();
|
|
886
|
+
chunkRef = await chunkStorage.storeChunk({
|
|
887
|
+
partitionId: pending.cacheLookup.partitionId,
|
|
888
|
+
scopeKey: pending.cacheLookup.scopeKey,
|
|
889
|
+
scope: pending.cacheLookup.scope,
|
|
890
|
+
asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
|
|
891
|
+
rowCursor: pending.cacheLookup.rowCursor,
|
|
892
|
+
rowLimit: pending.cacheLookup.rowLimit,
|
|
893
|
+
encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
894
|
+
compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
895
|
+
sha256: encodedChunk.sha256,
|
|
896
|
+
body: encodedChunk.body,
|
|
897
|
+
expiresAt: pending.expiresAt,
|
|
898
|
+
});
|
|
899
|
+
bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
pending.snapshot.chunks = [chunkRef];
|
|
903
|
+
});
|
|
871
904
|
}
|
|
872
|
-
|
|
873
|
-
|
|
905
|
+
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
906
|
+
const stats = summarizePullResponse(result.response);
|
|
907
|
+
span.setAttribute('status', 'ok');
|
|
908
|
+
span.setAttribute('duration_ms', durationMs);
|
|
909
|
+
span.setAttribute('subscription_count', stats.subscriptionCount);
|
|
910
|
+
span.setAttribute('commit_count', stats.commitCount);
|
|
911
|
+
span.setAttribute('change_count', stats.changeCount);
|
|
912
|
+
span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
|
|
913
|
+
span.setAttributes({
|
|
914
|
+
bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
|
|
915
|
+
bootstrap_row_frame_encode_ms: bootstrapTimings.rowFrameEncodeMs,
|
|
916
|
+
bootstrap_chunk_cache_lookup_ms: bootstrapTimings.chunkCacheLookupMs,
|
|
917
|
+
bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
|
|
918
|
+
bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
|
|
919
|
+
bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
|
|
920
|
+
});
|
|
921
|
+
span.setStatus('ok');
|
|
922
|
+
recordPullMetrics({
|
|
923
|
+
status: 'ok',
|
|
924
|
+
dedupeRows,
|
|
925
|
+
durationMs,
|
|
926
|
+
stats,
|
|
927
|
+
});
|
|
928
|
+
return {
|
|
929
|
+
...result,
|
|
930
|
+
bootstrapTimings,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
if (error instanceof Error &&
|
|
935
|
+
attemptIndex < MAX_PULL_TRANSACTION_RETRIES - 1 &&
|
|
936
|
+
isSerializablePullError(error)) {
|
|
937
|
+
await delay(PULL_TRANSACTION_RETRY_DELAY_MS * (attemptIndex + 1));
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
874
942
|
}
|
|
875
|
-
|
|
876
|
-
const stats = summarizePullResponse(result.response);
|
|
877
|
-
span.setAttribute('status', 'ok');
|
|
878
|
-
span.setAttribute('duration_ms', durationMs);
|
|
879
|
-
span.setAttribute('subscription_count', stats.subscriptionCount);
|
|
880
|
-
span.setAttribute('commit_count', stats.commitCount);
|
|
881
|
-
span.setAttribute('change_count', stats.changeCount);
|
|
882
|
-
span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
|
|
883
|
-
span.setAttributes({
|
|
884
|
-
bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
|
|
885
|
-
bootstrap_row_frame_encode_ms: bootstrapTimings.rowFrameEncodeMs,
|
|
886
|
-
bootstrap_chunk_cache_lookup_ms: bootstrapTimings.chunkCacheLookupMs,
|
|
887
|
-
bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
|
|
888
|
-
bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
|
|
889
|
-
bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
|
|
890
|
-
});
|
|
891
|
-
span.setStatus('ok');
|
|
892
|
-
recordPullMetrics({
|
|
893
|
-
status: 'ok',
|
|
894
|
-
dedupeRows,
|
|
895
|
-
durationMs,
|
|
896
|
-
stats,
|
|
897
|
-
});
|
|
898
|
-
return {
|
|
899
|
-
...result,
|
|
900
|
-
bootstrapTimings,
|
|
901
|
-
};
|
|
943
|
+
throw new Error('Pull transaction retry loop exhausted unexpectedly');
|
|
902
944
|
}
|
|
903
945
|
catch (error) {
|
|
904
946
|
const durationMs = Math.max(0, Date.now() - startedAtMs);
|