@powersync/service-core-tests 0.17.2 → 0.18.1
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 +52 -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/storage-combinations.d.ts +13 -0
- package/dist/test-utils/storage-combinations.js +26 -0
- package/dist/test-utils/storage-combinations.js.map +1 -0
- 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/test-utils/test-utils-index.d.ts +1 -0
- package/dist/test-utils/test-utils-index.js +1 -0
- package/dist/test-utils/test-utils-index.js.map +1 -1
- package/dist/tests/register-compacting-tests.js +15 -15
- package/dist/tests/register-compacting-tests.js.map +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.js +132 -14
- package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.js +116 -96
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-parameter-compacting-tests.js +3 -2
- package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
- package/dist/tests/register-sync-tests.js +267 -82
- package/dist/tests/register-sync-tests.js.map +1 -1
- package/dist/tests/util.d.ts +6 -0
- package/dist/tests/util.js +12 -0
- package/dist/tests/util.js.map +1 -1
- package/package.json +4 -4
- 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/storage-combinations.ts +51 -0
- package/src/test-utils/stream_utils.ts +0 -11
- package/src/test-utils/test-utils-index.ts +1 -0
- package/src/tests/register-compacting-tests.ts +15 -15
- package/src/tests/register-data-storage-checkpoint-tests.ts +161 -14
- package/src/tests/register-data-storage-data-tests.ts +132 -134
- package/src/tests/register-parameter-compacting-tests.ts +3 -2
- package/src/tests/register-sync-tests.ts +288 -84
- package/src/tests/util.ts +13 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,9 +3,10 @@ import { JSONBig } from '@powersync/service-jsonbig';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import * as timers from 'timers/promises';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { expect, test } from 'vitest';
|
|
6
|
+
import { expect, test, vi } from 'vitest';
|
|
7
7
|
import * as test_utils from '../test-utils/test-utils-index.js';
|
|
8
8
|
import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
|
|
9
|
+
import { compactActive } from './util.js';
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
11
12
|
const BASIC_SYNC_RULES = `
|
|
@@ -205,26 +206,33 @@ streams:
|
|
|
205
206
|
const lines = await consumeCheckpointLines(stream);
|
|
206
207
|
expect(lines).toMatchSnapshot();
|
|
207
208
|
});
|
|
208
|
-
test('
|
|
209
|
+
test('carries pending low-priority buckets into a new checkpoint', async () => {
|
|
209
210
|
await using f = await factory();
|
|
210
211
|
const syncRules = await updateSyncRules(f, {
|
|
211
212
|
content: `
|
|
212
213
|
bucket_definitions:
|
|
213
|
-
|
|
214
|
+
b0a:
|
|
214
215
|
priority: 2
|
|
215
216
|
data:
|
|
216
|
-
- SELECT * FROM test WHERE
|
|
217
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
218
|
+
b0b:
|
|
219
|
+
priority: 3
|
|
220
|
+
data:
|
|
221
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
217
222
|
b1:
|
|
218
223
|
priority: 1
|
|
219
224
|
data:
|
|
220
|
-
- SELECT * FROM test WHERE
|
|
225
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
221
226
|
`
|
|
222
227
|
});
|
|
223
228
|
const bucketStorage = f.getInstance(syncRules);
|
|
224
229
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
225
230
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
231
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
232
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
233
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
234
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
226
235
|
await writer.markAllSnapshotDone('0/1');
|
|
227
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
228
236
|
await writer.save({
|
|
229
237
|
sourceTable: testTable,
|
|
230
238
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -234,15 +242,26 @@ bucket_definitions:
|
|
|
234
242
|
},
|
|
235
243
|
afterReplicaId: 'highprio'
|
|
236
244
|
});
|
|
237
|
-
for (let i = 0; i <
|
|
245
|
+
for (let i = 0; i < 999; i++) {
|
|
238
246
|
await writer.save({
|
|
239
247
|
sourceTable: testTable,
|
|
240
248
|
tag: storage.SaveOperationTag.INSERT,
|
|
241
249
|
after: {
|
|
242
|
-
id:
|
|
243
|
-
description: 'low
|
|
250
|
+
id: `first-${i}`,
|
|
251
|
+
description: 'first low-priority bucket'
|
|
244
252
|
},
|
|
245
|
-
afterReplicaId:
|
|
253
|
+
afterReplicaId: `first-${i}`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
for (let i = 0; i < 9_001; i++) {
|
|
257
|
+
await writer.save({
|
|
258
|
+
sourceTable: testTable,
|
|
259
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
260
|
+
after: {
|
|
261
|
+
id: `low-${i}`,
|
|
262
|
+
description: 'last low-priority bucket'
|
|
263
|
+
},
|
|
264
|
+
afterReplicaId: `low-${i}`
|
|
246
265
|
});
|
|
247
266
|
}
|
|
248
267
|
await writer.commit('0/1');
|
|
@@ -259,17 +278,24 @@ bucket_definitions:
|
|
|
259
278
|
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
260
279
|
isEncodingAsBson: false
|
|
261
280
|
});
|
|
262
|
-
|
|
263
|
-
|
|
281
|
+
const dataLines = [];
|
|
282
|
+
const partialCheckpoints = [];
|
|
283
|
+
const checkpoints = [];
|
|
284
|
+
const checkpointCompletes = [];
|
|
285
|
+
let afterCheckpointDiff = false;
|
|
286
|
+
let interruptionTriggered = false;
|
|
264
287
|
for await (let next of stream) {
|
|
265
288
|
if (typeof next == 'string') {
|
|
266
289
|
next = JSON.parse(next);
|
|
267
290
|
}
|
|
268
291
|
if (typeof next === 'object' && next !== null) {
|
|
269
292
|
if ('partial_checkpoint_complete' in next) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
293
|
+
partialCheckpoints.push({
|
|
294
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
295
|
+
afterCheckpointDiff
|
|
296
|
+
});
|
|
297
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
298
|
+
interruptionTriggered = true;
|
|
273
299
|
await writer.save({
|
|
274
300
|
sourceTable: testTable,
|
|
275
301
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -280,27 +306,69 @@ bucket_definitions:
|
|
|
280
306
|
afterReplicaId: 'highprio2'
|
|
281
307
|
});
|
|
282
308
|
await writer.commit('0/2');
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Low-priority sync from the first checkpoint was interrupted. This should not happen before
|
|
286
|
-
// 1000 low-priority items were synchronized.
|
|
287
|
-
expect(sentCheckpoints).toBe(2);
|
|
288
|
-
expect(sentRows).toBeGreaterThan(1000);
|
|
309
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
310
|
+
await timers.setTimeout(50);
|
|
289
311
|
}
|
|
290
312
|
}
|
|
291
313
|
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
292
|
-
|
|
314
|
+
checkpoints.push(next);
|
|
315
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
293
316
|
}
|
|
294
317
|
if ('data' in next) {
|
|
295
|
-
|
|
318
|
+
dataLines.push({
|
|
319
|
+
bucket: next.data.bucket,
|
|
320
|
+
objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
321
|
+
afterCheckpointDiff
|
|
322
|
+
});
|
|
296
323
|
}
|
|
297
324
|
if ('checkpoint_complete' in next) {
|
|
325
|
+
checkpointCompletes.push(next);
|
|
298
326
|
break;
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
}
|
|
302
|
-
|
|
303
|
-
|
|
330
|
+
// Expected flow (data may be split into any number of chunks):
|
|
331
|
+
//
|
|
332
|
+
// checkpoint
|
|
333
|
+
// data: b1 contains highprio
|
|
334
|
+
// partial_checkpoint_complete: priority 1
|
|
335
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
336
|
+
// partial_checkpoint_complete: priority 2
|
|
337
|
+
// ## add highprio2, interrupting before b0b starts
|
|
338
|
+
// checkpoint_diff
|
|
339
|
+
// data: b1 contains highprio2
|
|
340
|
+
// partial_checkpoint_complete: priority 1
|
|
341
|
+
// data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
|
|
342
|
+
// checkpoint_complete: only for the new checkpoint
|
|
343
|
+
expect(interruptionTriggered).toBe(true);
|
|
344
|
+
expect(checkpoints).toHaveLength(2);
|
|
345
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
346
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
347
|
+
expect(partialCheckpoints).toEqual([
|
|
348
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
349
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
350
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
351
|
+
]);
|
|
352
|
+
expect(checkpointCompletes).toEqual([
|
|
353
|
+
{
|
|
354
|
+
checkpoint_complete: {
|
|
355
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
]);
|
|
359
|
+
const objectIdsFor = (bucket, afterDiff) => dataLines
|
|
360
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
361
|
+
.flatMap((line) => line.objectIds);
|
|
362
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
363
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
364
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
365
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
366
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
367
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
368
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
369
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
370
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
|
|
371
|
+
expect(b0bObjectIds).toHaveLength(9_001);
|
|
304
372
|
});
|
|
305
373
|
test('sync interruptions with unrelated data', async () => {
|
|
306
374
|
await using f = await factory();
|
|
@@ -420,33 +488,33 @@ bucket_definitions:
|
|
|
420
488
|
expect(sentCheckpoints).toBe(2);
|
|
421
489
|
expect(sentRows).toBe(10002);
|
|
422
490
|
});
|
|
423
|
-
test('
|
|
491
|
+
test('restarts updated low-priority buckets at a new checkpoint', async () => {
|
|
424
492
|
await using f = await factory();
|
|
425
|
-
// bucket0a -> send all data
|
|
426
|
-
// then interrupt checkpoint with new data for all buckets
|
|
427
|
-
// -> data for all buckets should be sent in the new checkpoint
|
|
428
493
|
const syncRules = await updateSyncRules(f, {
|
|
429
494
|
content: `
|
|
430
495
|
bucket_definitions:
|
|
431
496
|
b0a:
|
|
432
497
|
priority: 2
|
|
433
498
|
data:
|
|
434
|
-
- SELECT * FROM test WHERE
|
|
499
|
+
- SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
|
|
435
500
|
b0b:
|
|
436
|
-
priority:
|
|
501
|
+
priority: 3
|
|
437
502
|
data:
|
|
438
|
-
- SELECT * FROM test WHERE
|
|
503
|
+
- SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
|
|
439
504
|
b1:
|
|
440
505
|
priority: 1
|
|
441
506
|
data:
|
|
442
|
-
- SELECT * FROM test WHERE
|
|
507
|
+
- SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
|
|
443
508
|
`
|
|
444
509
|
});
|
|
445
510
|
const bucketStorage = f.getInstance(syncRules);
|
|
446
511
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
447
512
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
513
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
514
|
+
const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
|
|
515
|
+
const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
|
|
516
|
+
const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
|
|
448
517
|
await writer.markAllSnapshotDone('0/1');
|
|
449
|
-
// Initial data: Add one priority row and 10k low-priority rows.
|
|
450
518
|
await writer.save({
|
|
451
519
|
sourceTable: testTable,
|
|
452
520
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -456,15 +524,26 @@ bucket_definitions:
|
|
|
456
524
|
},
|
|
457
525
|
afterReplicaId: 'highprio'
|
|
458
526
|
});
|
|
527
|
+
for (let i = 0; i < 999; i++) {
|
|
528
|
+
await writer.save({
|
|
529
|
+
sourceTable: testTable,
|
|
530
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
531
|
+
after: {
|
|
532
|
+
id: `first-${i}`,
|
|
533
|
+
description: 'first low-priority bucket'
|
|
534
|
+
},
|
|
535
|
+
afterReplicaId: `first-${i}`
|
|
536
|
+
});
|
|
537
|
+
}
|
|
459
538
|
for (let i = 0; i < 2_000; i++) {
|
|
460
539
|
await writer.save({
|
|
461
540
|
sourceTable: testTable,
|
|
462
541
|
tag: storage.SaveOperationTag.INSERT,
|
|
463
542
|
after: {
|
|
464
|
-
id:
|
|
543
|
+
id: `low-${i}`,
|
|
465
544
|
description: 'low prio'
|
|
466
545
|
},
|
|
467
|
-
afterReplicaId:
|
|
546
|
+
afterReplicaId: `low-${i}`
|
|
468
547
|
});
|
|
469
548
|
}
|
|
470
549
|
await writer.commit('0/1');
|
|
@@ -481,25 +560,24 @@ bucket_definitions:
|
|
|
481
560
|
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
482
561
|
isEncodingAsBson: false
|
|
483
562
|
});
|
|
484
|
-
|
|
485
|
-
|
|
563
|
+
const dataLines = [];
|
|
564
|
+
const partialCheckpoints = [];
|
|
565
|
+
const checkpoints = [];
|
|
566
|
+
const checkpointCompletes = [];
|
|
567
|
+
let afterCheckpointDiff = false;
|
|
568
|
+
let interruptionTriggered = false;
|
|
486
569
|
for await (let next of stream) {
|
|
487
570
|
if (typeof next == 'string') {
|
|
488
571
|
next = JSON.parse(next);
|
|
489
572
|
}
|
|
490
573
|
if (typeof next === 'object' && next !== null) {
|
|
491
574
|
if ('partial_checkpoint_complete' in next) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
lines.push({ data: { ...next.data, data: undefined } });
|
|
499
|
-
sentRows += next.data.data.length;
|
|
500
|
-
if (sentRows == 1001) {
|
|
501
|
-
// Save new data to interrupt the low-priority sync.
|
|
502
|
-
// Add another high-priority row. This should interrupt the long-running low-priority sync.
|
|
575
|
+
partialCheckpoints.push({
|
|
576
|
+
priority: next.partial_checkpoint_complete.priority,
|
|
577
|
+
afterCheckpointDiff
|
|
578
|
+
});
|
|
579
|
+
if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
|
|
580
|
+
interruptionTriggered = true;
|
|
503
581
|
await writer.save({
|
|
504
582
|
sourceTable: testTable,
|
|
505
583
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -509,48 +587,79 @@ bucket_definitions:
|
|
|
509
587
|
},
|
|
510
588
|
afterReplicaId: 'highprio2'
|
|
511
589
|
});
|
|
512
|
-
// Also add a low-priority row
|
|
513
590
|
await writer.save({
|
|
514
591
|
sourceTable: testTable,
|
|
515
592
|
tag: storage.SaveOperationTag.INSERT,
|
|
516
593
|
after: {
|
|
517
|
-
id: '
|
|
594
|
+
id: 'low-2000',
|
|
518
595
|
description: 'Another low-priority row'
|
|
519
596
|
},
|
|
520
|
-
afterReplicaId: '
|
|
597
|
+
afterReplicaId: 'low-2000'
|
|
521
598
|
});
|
|
522
599
|
await writer.commit('0/2');
|
|
523
|
-
|
|
524
|
-
if (sentRows >= 1000 && sentRows <= 2001) {
|
|
525
|
-
// pause for a bit to give the stream time to process interruptions.
|
|
526
|
-
// This covers the data batch above and the next one.
|
|
600
|
+
// Let the checkpoint watcher observe the update before requesting the next priority.
|
|
527
601
|
await timers.setTimeout(50);
|
|
528
602
|
}
|
|
529
603
|
}
|
|
604
|
+
if ('checkpoint' in next || 'checkpoint_diff' in next) {
|
|
605
|
+
checkpoints.push(next);
|
|
606
|
+
afterCheckpointDiff = 'checkpoint_diff' in next;
|
|
607
|
+
}
|
|
608
|
+
if ('data' in next) {
|
|
609
|
+
dataLines.push({
|
|
610
|
+
bucket: next.data.bucket,
|
|
611
|
+
objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
|
|
612
|
+
afterCheckpointDiff
|
|
613
|
+
});
|
|
614
|
+
}
|
|
530
615
|
if ('checkpoint_complete' in next) {
|
|
531
|
-
|
|
616
|
+
checkpointCompletes.push(next);
|
|
532
617
|
break;
|
|
533
618
|
}
|
|
534
619
|
}
|
|
535
620
|
}
|
|
536
|
-
// Expected
|
|
621
|
+
// Expected flow (data may be split into any number of chunks):
|
|
537
622
|
//
|
|
538
|
-
// checkpoint
|
|
539
|
-
// data
|
|
540
|
-
// partial_checkpoint_complete
|
|
541
|
-
// data (
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// checkpoint_diff
|
|
545
|
-
// data
|
|
546
|
-
// partial_checkpoint_complete
|
|
547
|
-
// data
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
expect(
|
|
553
|
-
expect(
|
|
623
|
+
// checkpoint
|
|
624
|
+
// data: b1 contains highprio
|
|
625
|
+
// partial_checkpoint_complete: priority 1
|
|
626
|
+
// data: b0a contains all 999 first-* rows (1,000 operations including highprio)
|
|
627
|
+
// partial_checkpoint_complete: priority 2
|
|
628
|
+
// ## add highprio2 and low-2000, interrupting before b0b starts
|
|
629
|
+
// checkpoint_diff
|
|
630
|
+
// data: b1 contains highprio2
|
|
631
|
+
// partial_checkpoint_complete: priority 1
|
|
632
|
+
// data: b0b contains all 2,001 low-* rows
|
|
633
|
+
// checkpoint_complete: only for the new checkpoint
|
|
634
|
+
expect(interruptionTriggered).toBe(true);
|
|
635
|
+
expect(checkpoints).toHaveLength(2);
|
|
636
|
+
expect(checkpoints[0]).toHaveProperty('checkpoint');
|
|
637
|
+
expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
|
|
638
|
+
expect(partialCheckpoints).toEqual([
|
|
639
|
+
{ priority: 1, afterCheckpointDiff: false },
|
|
640
|
+
{ priority: 2, afterCheckpointDiff: false },
|
|
641
|
+
{ priority: 1, afterCheckpointDiff: true }
|
|
642
|
+
]);
|
|
643
|
+
expect(checkpointCompletes).toEqual([
|
|
644
|
+
{
|
|
645
|
+
checkpoint_complete: {
|
|
646
|
+
last_op_id: checkpoints[1].checkpoint_diff.last_op_id
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
]);
|
|
650
|
+
const objectIdsFor = (bucket, afterDiff) => dataLines
|
|
651
|
+
.filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
|
|
652
|
+
.flatMap((line) => line.objectIds);
|
|
653
|
+
expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
|
|
654
|
+
const b0aObjectIds = objectIdsFor(b0aBucket, false);
|
|
655
|
+
expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
|
|
656
|
+
expect(b0aObjectIds).toHaveLength(999);
|
|
657
|
+
expect(objectIdsFor(b0bBucket, false)).toEqual([]);
|
|
658
|
+
expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
|
|
659
|
+
expect(objectIdsFor(b0aBucket, true)).toEqual([]);
|
|
660
|
+
const b0bObjectIds = objectIdsFor(b0bBucket, true);
|
|
661
|
+
expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
|
|
662
|
+
expect(b0bObjectIds).toHaveLength(2_001);
|
|
554
663
|
});
|
|
555
664
|
test('sends checkpoint complete line for empty checkpoint', async () => {
|
|
556
665
|
await using f = await factory();
|
|
@@ -931,6 +1040,82 @@ bucket_definitions:
|
|
|
931
1040
|
const expLines = await getCheckpointLines(iter);
|
|
932
1041
|
expect(expLines).toMatchSnapshot();
|
|
933
1042
|
});
|
|
1043
|
+
test('checksum invalidation skips the candidate checkpoint', async (context) => {
|
|
1044
|
+
await using f = await factory();
|
|
1045
|
+
const syncRules = await updateSyncRules(f, {
|
|
1046
|
+
content: BASIC_SYNC_RULES
|
|
1047
|
+
});
|
|
1048
|
+
const bucketStorage = await f.getInstance(syncRules);
|
|
1049
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1050
|
+
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1051
|
+
await writer.markAllSnapshotDone('0/1');
|
|
1052
|
+
await writer.save({
|
|
1053
|
+
sourceTable: testTable,
|
|
1054
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1055
|
+
after: {
|
|
1056
|
+
id: 't1',
|
|
1057
|
+
description: 'Test 1'
|
|
1058
|
+
},
|
|
1059
|
+
afterReplicaId: 't1'
|
|
1060
|
+
});
|
|
1061
|
+
await writer.commit('0/1');
|
|
1062
|
+
let resolveInvalidatedChecksum;
|
|
1063
|
+
const invalidatedChecksum = new Promise((resolve) => {
|
|
1064
|
+
resolveInvalidatedChecksum = resolve;
|
|
1065
|
+
});
|
|
1066
|
+
const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
|
|
1067
|
+
resolveInvalidatedChecksum();
|
|
1068
|
+
throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
|
|
1069
|
+
});
|
|
1070
|
+
const stream = sync.streamResponse({
|
|
1071
|
+
syncContext,
|
|
1072
|
+
bucketStorage,
|
|
1073
|
+
syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
|
|
1074
|
+
params: {
|
|
1075
|
+
buckets: [],
|
|
1076
|
+
include_checksum: true,
|
|
1077
|
+
raw_data: true
|
|
1078
|
+
},
|
|
1079
|
+
tracker,
|
|
1080
|
+
token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
|
|
1081
|
+
isEncodingAsBson: false
|
|
1082
|
+
});
|
|
1083
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
1084
|
+
context.onTestFinished(() => {
|
|
1085
|
+
iter.return?.();
|
|
1086
|
+
});
|
|
1087
|
+
const linesPromise = getCheckpointLines(iter, { consume: true });
|
|
1088
|
+
await invalidatedChecksum;
|
|
1089
|
+
// The first candidate was discarded before CheckpointLine.advance(). A later
|
|
1090
|
+
// checkpoint must therefore be calculated from the original connection state.
|
|
1091
|
+
await writer.save({
|
|
1092
|
+
sourceTable: testTable,
|
|
1093
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1094
|
+
after: {
|
|
1095
|
+
id: 't2',
|
|
1096
|
+
description: 'Test 2'
|
|
1097
|
+
},
|
|
1098
|
+
afterReplicaId: 't2'
|
|
1099
|
+
});
|
|
1100
|
+
await writer.commit('0/2');
|
|
1101
|
+
const lines = await linesPromise;
|
|
1102
|
+
expect(checksumSpy).toHaveBeenCalledTimes(2);
|
|
1103
|
+
expect(lines[0]).toEqual({
|
|
1104
|
+
checkpoint: expect.objectContaining({
|
|
1105
|
+
last_op_id: '2'
|
|
1106
|
+
})
|
|
1107
|
+
});
|
|
1108
|
+
expect(lines).not.toContainEqual({
|
|
1109
|
+
checkpoint: expect.objectContaining({
|
|
1110
|
+
last_op_id: '1'
|
|
1111
|
+
})
|
|
1112
|
+
});
|
|
1113
|
+
expect(lines.at(-1)).toEqual({
|
|
1114
|
+
checkpoint_complete: expect.objectContaining({
|
|
1115
|
+
last_op_id: '2'
|
|
1116
|
+
})
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
934
1119
|
test('compacting data - invalidate checkpoint', async (context) => {
|
|
935
1120
|
// This tests a case of a compact operation invalidating a checkpoint in the
|
|
936
1121
|
// middle of syncing data.
|
|
@@ -943,6 +1128,7 @@ bucket_definitions:
|
|
|
943
1128
|
const bucketStorage = await f.getInstance(syncRules);
|
|
944
1129
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
945
1130
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1131
|
+
const bucket = bucketRequest(syncRules.syncConfigContent[0], 'mybucket[]').bucket;
|
|
946
1132
|
await writer.markAllSnapshotDone('0/1');
|
|
947
1133
|
await writer.save({
|
|
948
1134
|
sourceTable: testTable,
|
|
@@ -1010,10 +1196,7 @@ bucket_definitions:
|
|
|
1010
1196
|
afterReplicaId: 't2'
|
|
1011
1197
|
});
|
|
1012
1198
|
await writer.commit('0/2');
|
|
1013
|
-
await
|
|
1014
|
-
minBucketChanges: 1,
|
|
1015
|
-
minChangeRatio: 0
|
|
1016
|
-
});
|
|
1199
|
+
await compactActive(f, { compactBuckets: [bucket] });
|
|
1017
1200
|
const lines2 = await getCheckpointLines(iter, { consume: true });
|
|
1018
1201
|
// Snapshot test checks for changes in general.
|
|
1019
1202
|
// The tests after that documents the specific things we're looking for
|
|
@@ -1084,6 +1267,7 @@ bucket_definitions:
|
|
|
1084
1267
|
const bucketStorage = await f.getInstance(syncRules);
|
|
1085
1268
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1086
1269
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
1270
|
+
const highPriorityBucket = bucketRequest(syncRules.syncConfigContent[0], 'high_priority[]').bucket;
|
|
1087
1271
|
await writer.markAllSnapshotDone('0/1');
|
|
1088
1272
|
await writer.save({
|
|
1089
1273
|
sourceTable: testTable,
|
|
@@ -1160,9 +1344,10 @@ bucket_definitions:
|
|
|
1160
1344
|
afterReplicaId: 'high2'
|
|
1161
1345
|
});
|
|
1162
1346
|
await writer.commit('0/2');
|
|
1163
|
-
await
|
|
1164
|
-
|
|
1165
|
-
|
|
1347
|
+
await compactActive(f, {
|
|
1348
|
+
// Explicitly compact the high-priority bucket: V3 schedules background
|
|
1349
|
+
// compaction, while this test needs compaction at this exact point.
|
|
1350
|
+
compactBuckets: [highPriorityBucket]
|
|
1166
1351
|
});
|
|
1167
1352
|
const lines = await getCheckpointLines(iter, { consume: true });
|
|
1168
1353
|
const nextCheckpointIndex = lines.findIndex((line) => line?.checkpoint_diff != null);
|
|
@@ -1200,7 +1385,7 @@ bucket_definitions:
|
|
|
1200
1385
|
user_id: 'test',
|
|
1201
1386
|
heads: { '1': '1/0' }
|
|
1202
1387
|
}
|
|
1203
|
-
])).get('test');
|
|
1388
|
+
])).writeCheckpoints.get('test');
|
|
1204
1389
|
const params = {
|
|
1205
1390
|
syncContext,
|
|
1206
1391
|
bucketStorage,
|