@powersync/service-core-tests 0.17.1 → 0.18.0
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/CHANGELOG.md +36 -0
- package/dist/test-utils/AbstractStreamTestContext.d.ts +1 -1
- package/dist/test-utils/AbstractStreamTestContext.js +2 -3
- package/dist/test-utils/AbstractStreamTestContext.js.map +1 -1
- package/dist/test-utils/StorageDataHelpers.d.ts +3 -2
- package/dist/test-utils/StorageDataHelpers.js +40 -2
- package/dist/test-utils/StorageDataHelpers.js.map +1 -1
- package/dist/test-utils/general-utils.d.ts +3 -1
- package/dist/test-utils/general-utils.js +13 -1
- package/dist/test-utils/general-utils.js.map +1 -1
- package/dist/test-utils/stream_utils.d.ts +0 -1
- package/dist/test-utils/stream_utils.js +0 -10
- package/dist/test-utils/stream_utils.js.map +1 -1
- package/dist/tests/register-compacting-tests.js +7 -7
- package/dist/tests/register-compacting-tests.js.map +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.js +96 -3
- package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.js +113 -94
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-sync-tests.js +259 -75
- package/dist/tests/register-sync-tests.js.map +1 -1
- package/package.json +5 -5
- package/src/test-utils/AbstractStreamTestContext.ts +2 -3
- package/src/test-utils/StorageDataHelpers.ts +45 -2
- package/src/test-utils/general-utils.ts +27 -3
- package/src/test-utils/stream_utils.ts +0 -11
- package/src/tests/register-compacting-tests.ts +7 -7
- package/src/tests/register-data-storage-checkpoint-tests.ts +122 -3
- package/src/tests/register-data-storage-data-tests.ts +129 -132
- package/src/tests/register-sync-tests.ts +280 -77
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,7 +12,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import * as timers from 'timers/promises';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
-
import { expect, test } from 'vitest';
|
|
15
|
+
import { expect, test, vi } from 'vitest';
|
|
16
16
|
import * as test_utils from '../test-utils/test-utils-index.js';
|
|
17
17
|
import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
|
|
18
18
|
|
|
@@ -251,29 +251,36 @@ streams:
|
|
|
251
251
|
expect(lines).toMatchSnapshot();
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
test('
|
|
254
|
+
test('carries pending low-priority buckets into a new checkpoint', async () => {
|
|
255
255
|
await using f = await factory();
|
|
256
256
|
|
|
257
257
|
const syncRules = await updateSyncRules(f, {
|
|
258
258
|
content: `
|
|
259
259
|
bucket_definitions:
|
|
260
|
-
|
|
260
|
+
b0a:
|
|
261
261
|
priority: 2
|
|
262
262
|
data:
|
|
263
|
-
- SELECT * FROM test WHERE
|
|
263
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
264
|
+
b0b:
|
|
265
|
+
priority: 3
|
|
266
|
+
data:
|
|
267
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
264
268
|
b1:
|
|
265
269
|
priority: 1
|
|
266
270
|
data:
|
|
267
|
-
- SELECT * FROM test WHERE
|
|
271
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
268
272
|
`
|
|
269
273
|
});
|
|
270
274
|
|
|
271
275
|
const bucketStorage = f.getInstance(syncRules);
|
|
272
276
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
273
277
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
278
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
279
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
280
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
281
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
274
282
|
|
|
275
283
|
await writer.markAllSnapshotDone('0/1');
|
|
276
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
277
284
|
await writer.save({
|
|
278
285
|
sourceTable: testTable,
|
|
279
286
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -283,15 +290,26 @@ bucket_definitions:
|
|
|
283
290
|
},
|
|
284
291
|
afterReplicaId: 'highprio'
|
|
285
292
|
});
|
|
286
|
-
for (let i = 0; i <
|
|
293
|
+
for (let i = 0; i < 999; i++) {
|
|
287
294
|
await writer.save({
|
|
288
295
|
sourceTable: testTable,
|
|
289
296
|
tag: storage.SaveOperationTag.INSERT,
|
|
290
297
|
after: {
|
|
291
|
-
id:
|
|
292
|
-
description: 'low
|
|
298
|
+
id: `first-${i}`,
|
|
299
|
+
description: 'first low-priority bucket'
|
|
293
300
|
},
|
|
294
|
-
afterReplicaId:
|
|
301
|
+
afterReplicaId: `first-${i}`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
for (let i = 0; i < 9_001; i++) {
|
|
305
|
+
await writer.save({
|
|
306
|
+
sourceTable: testTable,
|
|
307
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
308
|
+
after: {
|
|
309
|
+
id: `low-${i}`,
|
|
310
|
+
description: 'last low-priority bucket'
|
|
311
|
+
},
|
|
312
|
+
afterReplicaId: `low-${i}`
|
|
295
313
|
});
|
|
296
314
|
}
|
|
297
315
|
|
|
@@ -311,8 +329,12 @@ bucket_definitions:
|
|
|
311
329
|
isEncodingAsBson: false
|
|
312
330
|
});
|
|
313
331
|
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
|
|
333
|
+
const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
|
|
334
|
+
const checkpoints: any[] = [];
|
|
335
|
+
const checkpointCompletes: any[] = [];
|
|
336
|
+
let afterCheckpointDiff = false;
|
|
337
|
+
let interruptionTriggered = false;
|
|
316
338
|
|
|
317
339
|
for await (let next of stream) {
|
|
318
340
|
if (typeof next == 'string') {
|
|
@@ -320,10 +342,13 @@ bucket_definitions:
|
|
|
320
342
|
}
|
|
321
343
|
if (typeof next === 'object' && next !== null) {
|
|
322
344
|
if ('partial_checkpoint_complete' in next) {
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
partialCheckpoints.push({
|
|
346
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
347
|
+
afterCheckpointDiff
|
|
348
|
+
});
|
|
325
349
|
|
|
326
|
-
|
|
350
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
351
|
+
interruptionTriggered = true;
|
|
327
352
|
await writer.save({
|
|
328
353
|
sourceTable: testTable,
|
|
329
354
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -335,28 +360,76 @@ bucket_definitions:
|
|
|
335
360
|
});
|
|
336
361
|
|
|
337
362
|
await writer.commit('0/2');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// 1000 low-priority items were synchronized.
|
|
341
|
-
expect(sentCheckpoints).toBe(2);
|
|
342
|
-
expect(sentRows).toBeGreaterThan(1000);
|
|
363
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
364
|
+
await timers.setTimeout(50);
|
|
343
365
|
}
|
|
344
366
|
}
|
|
345
367
|
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
346
|
-
|
|
368
|
+
checkpoints.push(next);
|
|
369
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
347
370
|
}
|
|
348
371
|
|
|
349
372
|
if ('data' in next) {
|
|
350
|
-
|
|
373
|
+
dataLines.push({
|
|
374
|
+
bucket: next.data.bucket,
|
|
375
|
+
objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
376
|
+
afterCheckpointDiff
|
|
377
|
+
});
|
|
351
378
|
}
|
|
352
379
|
if ('checkpoint_complete' in next) {
|
|
380
|
+
checkpointCompletes.push(next);
|
|
353
381
|
break;
|
|
354
382
|
}
|
|
355
383
|
}
|
|
356
384
|
}
|
|
357
385
|
|
|
358
|
-
|
|
359
|
-
|
|
386
|
+
// Expected flow (data may be split into any number of chunks):
|
|
387
|
+
//
|
|
388
|
+
// checkpoint
|
|
389
|
+
// data: b1 contains highprio
|
|
390
|
+
// partial_checkpoint_complete: priority 1
|
|
391
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
392
|
+
// partial_checkpoint_complete: priority 2
|
|
393
|
+
// ## add highprio2, interrupting before b0b starts
|
|
394
|
+
// checkpoint_diff
|
|
395
|
+
// data: b1 contains highprio2
|
|
396
|
+
// partial_checkpoint_complete: priority 1
|
|
397
|
+
// data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
|
|
398
|
+
// checkpoint_complete: only for the new checkpoint
|
|
399
|
+
expect(interruptionTriggered).toBe(true);
|
|
400
|
+
expect(checkpoints).toHaveLength(2);
|
|
401
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
402
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
403
|
+
expect(partialCheckpoints).toEqual([
|
|
404
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
405
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
406
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
407
|
+
]);
|
|
408
|
+
expect(checkpointCompletes).toEqual([
|
|
409
|
+
{
|
|
410
|
+
checkpoint_complete: {
|
|
411
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
const objectIdsFor = (bucket: string, afterDiff: boolean) =>
|
|
417
|
+
dataLines
|
|
418
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
419
|
+
.flatMap((line) => line.objectIds);
|
|
420
|
+
|
|
421
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
422
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
423
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
424
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
425
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
426
|
+
|
|
427
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
428
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
429
|
+
|
|
430
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
431
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
|
|
432
|
+
expect(b0bObjectIds).toHaveLength(9_001);
|
|
360
433
|
});
|
|
361
434
|
|
|
362
435
|
test('sync interruptions with unrelated data', async () => {
|
|
@@ -490,37 +563,36 @@ bucket_definitions:
|
|
|
490
563
|
expect(sentRows).toBe(10002);
|
|
491
564
|
});
|
|
492
565
|
|
|
493
|
-
test('
|
|
566
|
+
test('restarts updated low-priority buckets at a new checkpoint', async () => {
|
|
494
567
|
await using f = await factory();
|
|
495
568
|
|
|
496
|
-
// bucket0a -> send all data
|
|
497
|
-
// then interrupt checkpoint with new data for all buckets
|
|
498
|
-
// -> data for all buckets should be sent in the new checkpoint
|
|
499
|
-
|
|
500
569
|
const syncRules = await updateSyncRules(f, {
|
|
501
570
|
content: `
|
|
502
571
|
bucket_definitions:
|
|
503
572
|
b0a:
|
|
504
573
|
priority: 2
|
|
505
574
|
data:
|
|
506
|
-
- SELECT * FROM test WHERE
|
|
575
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
507
576
|
b0b:
|
|
508
|
-
priority:
|
|
577
|
+
priority: 3
|
|
509
578
|
data:
|
|
510
|
-
- SELECT * FROM test WHERE
|
|
579
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
511
580
|
b1:
|
|
512
581
|
priority: 1
|
|
513
582
|
data:
|
|
514
|
-
- SELECT * FROM test WHERE
|
|
583
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
515
584
|
`
|
|
516
585
|
});
|
|
517
586
|
|
|
518
587
|
const bucketStorage = f.getInstance(syncRules);
|
|
519
588
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
520
589
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
590
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
591
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
592
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
593
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
521
594
|
|
|
522
595
|
await writer.markAllSnapshotDone('0/1');
|
|
523
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
524
596
|
await writer.save({
|
|
525
597
|
sourceTable: testTable,
|
|
526
598
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -530,15 +602,26 @@ bucket_definitions:
|
|
|
530
602
|
},
|
|
531
603
|
afterReplicaId: 'highprio'
|
|
532
604
|
});
|
|
605
|
+
for (let i = 0; i < 999; i++) {
|
|
606
|
+
await writer.save({
|
|
607
|
+
sourceTable: testTable,
|
|
608
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
609
|
+
after: {
|
|
610
|
+
id: `first-${i}`,
|
|
611
|
+
description: 'first low-priority bucket'
|
|
612
|
+
},
|
|
613
|
+
afterReplicaId: `first-${i}`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
533
616
|
for (let i = 0; i < 2_000; i++) {
|
|
534
617
|
await writer.save({
|
|
535
618
|
sourceTable: testTable,
|
|
536
619
|
tag: storage.SaveOperationTag.INSERT,
|
|
537
620
|
after: {
|
|
538
|
-
id:
|
|
621
|
+
id: `low-${i}`,
|
|
539
622
|
description: 'low prio'
|
|
540
623
|
},
|
|
541
|
-
afterReplicaId:
|
|
624
|
+
afterReplicaId: `low-${i}`
|
|
542
625
|
});
|
|
543
626
|
}
|
|
544
627
|
|
|
@@ -558,8 +641,12 @@ bucket_definitions:
|
|
|
558
641
|
isEncodingAsBson: false
|
|
559
642
|
});
|
|
560
643
|
|
|
561
|
-
|
|
562
|
-
|
|
644
|
+
const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
|
|
645
|
+
const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
|
|
646
|
+
const checkpoints: any[] = [];
|
|
647
|
+
const checkpointCompletes: any[] = [];
|
|
648
|
+
let afterCheckpointDiff = false;
|
|
649
|
+
let interruptionTriggered = false;
|
|
563
650
|
|
|
564
651
|
for await (let next of stream) {
|
|
565
652
|
if (typeof next == 'string') {
|
|
@@ -567,19 +654,13 @@ bucket_definitions:
|
|
|
567
654
|
}
|
|
568
655
|
if (typeof next === 'object' && next !== null) {
|
|
569
656
|
if ('partial_checkpoint_complete' in next) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
657
|
+
partialCheckpoints.push({
|
|
658
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
659
|
+
afterCheckpointDiff
|
|
660
|
+
});
|
|
575
661
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
sentRows += next.data.data.length;
|
|
579
|
-
|
|
580
|
-
if (sentRows == 1001) {
|
|
581
|
-
// Save new data to interrupt the low-priority sync.
|
|
582
|
-
// Add another high-priority row. This should interrupt the long-running low-priority sync.
|
|
662
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
663
|
+
interruptionTriggered = true;
|
|
583
664
|
await writer.save({
|
|
584
665
|
sourceTable: testTable,
|
|
585
666
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -590,51 +671,87 @@ bucket_definitions:
|
|
|
590
671
|
afterReplicaId: 'highprio2'
|
|
591
672
|
});
|
|
592
673
|
|
|
593
|
-
// Also add a low-priority row
|
|
594
674
|
await writer.save({
|
|
595
675
|
sourceTable: testTable,
|
|
596
676
|
tag: storage.SaveOperationTag.INSERT,
|
|
597
677
|
after: {
|
|
598
|
-
id: '
|
|
678
|
+
id: 'low-2000',
|
|
599
679
|
description: 'Another low-priority row'
|
|
600
680
|
},
|
|
601
|
-
afterReplicaId: '
|
|
681
|
+
afterReplicaId: 'low-2000'
|
|
602
682
|
});
|
|
603
683
|
|
|
604
684
|
await writer.commit('0/2');
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (sentRows >= 1000 && sentRows <= 2001) {
|
|
608
|
-
// pause for a bit to give the stream time to process interruptions.
|
|
609
|
-
// This covers the data batch above and the next one.
|
|
685
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
610
686
|
await timers.setTimeout(50);
|
|
611
687
|
}
|
|
612
688
|
}
|
|
689
|
+
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
690
|
+
checkpoints.push(next);
|
|
691
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if ('data' in next) {
|
|
695
|
+
dataLines.push({
|
|
696
|
+
bucket: next.data.bucket,
|
|
697
|
+
objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
698
|
+
afterCheckpointDiff
|
|
699
|
+
});
|
|
700
|
+
}
|
|
613
701
|
if ('checkpoint_complete' in next) {
|
|
614
|
-
|
|
702
|
+
checkpointCompletes.push(next);
|
|
615
703
|
break;
|
|
616
704
|
}
|
|
617
705
|
}
|
|
618
706
|
}
|
|
619
707
|
|
|
620
|
-
// Expected
|
|
708
|
+
// Expected flow (data may be split into any number of chunks):
|
|
621
709
|
//
|
|
622
|
-
// checkpoint
|
|
623
|
-
// data
|
|
624
|
-
// partial_checkpoint_complete
|
|
625
|
-
// data (
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// checkpoint_diff
|
|
629
|
-
// data
|
|
630
|
-
// partial_checkpoint_complete
|
|
631
|
-
// data
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
expect(
|
|
637
|
-
expect(
|
|
710
|
+
// checkpoint
|
|
711
|
+
// data: b1 contains highprio
|
|
712
|
+
// partial_checkpoint_complete: priority 1
|
|
713
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
714
|
+
// partial_checkpoint_complete: priority 2
|
|
715
|
+
// ## add highprio2 and low-2000, interrupting before b0b starts
|
|
716
|
+
// checkpoint_diff
|
|
717
|
+
// data: b1 contains highprio2
|
|
718
|
+
// partial_checkpoint_complete: priority 1
|
|
719
|
+
// data: b0b contains all 2,001 low-* rows
|
|
720
|
+
// checkpoint_complete: only for the new checkpoint
|
|
721
|
+
expect(interruptionTriggered).toBe(true);
|
|
722
|
+
expect(checkpoints).toHaveLength(2);
|
|
723
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
724
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
725
|
+
expect(partialCheckpoints).toEqual([
|
|
726
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
727
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
728
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
729
|
+
]);
|
|
730
|
+
expect(checkpointCompletes).toEqual([
|
|
731
|
+
{
|
|
732
|
+
checkpoint_complete: {
|
|
733
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
]);
|
|
737
|
+
|
|
738
|
+
const objectIdsFor = (bucket: string, afterDiff: boolean) =>
|
|
739
|
+
dataLines
|
|
740
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
741
|
+
.flatMap((line) => line.objectIds);
|
|
742
|
+
|
|
743
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
744
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
745
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
746
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
747
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
748
|
+
|
|
749
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
750
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
751
|
+
|
|
752
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
753
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
|
|
754
|
+
expect(b0bObjectIds).toHaveLength(2_001);
|
|
638
755
|
});
|
|
639
756
|
|
|
640
757
|
test('sends checkpoint complete line for empty checkpoint', async () => {
|
|
@@ -1090,6 +1207,92 @@ bucket_definitions:
|
|
|
1090
1207
|
expect(expLines).toMatchSnapshot();
|
|
1091
1208
|
});
|
|
1092
1209
|
|
|
1210
|
+
test('checksum invalidation skips the candidate checkpoint', async (context) => {
|
|
1211
|
+
await using f = await factory();
|
|
1212
|
+
|
|
1213
|
+
const syncRules = await updateSyncRules(f, {
|
|
1214
|
+
content: BASIC_SYNC_RULES
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
const bucketStorage = await f.getInstance(syncRules);
|
|
1218
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1219
|
+
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1220
|
+
|
|
1221
|
+
await writer.markAllSnapshotDone('0/1');
|
|
1222
|
+
await writer.save({
|
|
1223
|
+
sourceTable: testTable,
|
|
1224
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1225
|
+
after: {
|
|
1226
|
+
id: 't1',
|
|
1227
|
+
description: 'Test 1'
|
|
1228
|
+
},
|
|
1229
|
+
afterReplicaId: 't1'
|
|
1230
|
+
});
|
|
1231
|
+
await writer.commit('0/1');
|
|
1232
|
+
|
|
1233
|
+
let resolveInvalidatedChecksum!: () => void;
|
|
1234
|
+
const invalidatedChecksum = new Promise<void>((resolve) => {
|
|
1235
|
+
resolveInvalidatedChecksum = resolve;
|
|
1236
|
+
});
|
|
1237
|
+
const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
|
|
1238
|
+
resolveInvalidatedChecksum();
|
|
1239
|
+
throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
const stream = sync.streamResponse({
|
|
1243
|
+
syncContext,
|
|
1244
|
+
bucketStorage,
|
|
1245
|
+
syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
|
|
1246
|
+
params: {
|
|
1247
|
+
buckets: [],
|
|
1248
|
+
include_checksum: true,
|
|
1249
|
+
raw_data: true
|
|
1250
|
+
},
|
|
1251
|
+
tracker,
|
|
1252
|
+
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
1253
|
+
isEncodingAsBson: false
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
1257
|
+
context.onTestFinished(() => {
|
|
1258
|
+
iter.return?.();
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
const linesPromise = getCheckpointLines(iter, { consume: true });
|
|
1262
|
+
await invalidatedChecksum;
|
|
1263
|
+
|
|
1264
|
+
// The first candidate was discarded before CheckpointLine.advance(). A later
|
|
1265
|
+
// checkpoint must therefore be calculated from the original connection state.
|
|
1266
|
+
await writer.save({
|
|
1267
|
+
sourceTable: testTable,
|
|
1268
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1269
|
+
after: {
|
|
1270
|
+
id: 't2',
|
|
1271
|
+
description: 'Test 2'
|
|
1272
|
+
},
|
|
1273
|
+
afterReplicaId: 't2'
|
|
1274
|
+
});
|
|
1275
|
+
await writer.commit('0/2');
|
|
1276
|
+
|
|
1277
|
+
const lines = await linesPromise;
|
|
1278
|
+
expect(checksumSpy).toHaveBeenCalledTimes(2);
|
|
1279
|
+
expect(lines[0]).toEqual({
|
|
1280
|
+
checkpoint: expect.objectContaining({
|
|
1281
|
+
last_op_id: '2'
|
|
1282
|
+
})
|
|
1283
|
+
});
|
|
1284
|
+
expect(lines).not.toContainEqual({
|
|
1285
|
+
checkpoint: expect.objectContaining({
|
|
1286
|
+
last_op_id: '1'
|
|
1287
|
+
})
|
|
1288
|
+
});
|
|
1289
|
+
expect(lines.at(-1)).toEqual({
|
|
1290
|
+
checkpoint_complete: expect.objectContaining({
|
|
1291
|
+
last_op_id: '2'
|
|
1292
|
+
})
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1093
1296
|
test('compacting data - invalidate checkpoint', async (context) => {
|
|
1094
1297
|
// This tests a case of a compact operation invalidating a checkpoint in the
|
|
1095
1298
|
// middle of syncing data.
|
|
@@ -1403,7 +1606,7 @@ bucket_definitions:
|
|
|
1403
1606
|
heads: { '1': '1/0' }
|
|
1404
1607
|
}
|
|
1405
1608
|
])
|
|
1406
|
-
).get('test')!;
|
|
1609
|
+
).writeCheckpoints.get('test')!;
|
|
1407
1610
|
|
|
1408
1611
|
const params: sync.SyncStreamParameters = {
|
|
1409
1612
|
syncContext,
|