@powersync/service-core 0.4.1 → 0.5.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 +16 -0
- package/dist/entry/cli-entry.js +2 -1
- package/dist/entry/cli-entry.js.map +1 -1
- package/dist/entry/commands/compact-action.d.ts +2 -0
- package/dist/entry/commands/compact-action.js +49 -0
- package/dist/entry/commands/compact-action.js.map +1 -0
- package/dist/entry/entry-index.d.ts +1 -0
- package/dist/entry/entry-index.js +1 -0
- package/dist/entry/entry-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +9 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +9 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +25 -1
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/storage/mongo/MongoCompactor.d.ts +38 -0
- package/dist/storage/mongo/MongoCompactor.js +278 -0
- package/dist/storage/mongo/MongoCompactor.js.map +1 -0
- package/dist/storage/mongo/MongoSyncBucketStorage.d.ts +3 -2
- package/dist/storage/mongo/MongoSyncBucketStorage.js +19 -13
- package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/mongo/models.d.ts +5 -4
- package/dist/storage/mongo/models.js.map +1 -1
- package/dist/storage/mongo/util.d.ts +3 -0
- package/dist/storage/mongo/util.js +22 -0
- package/dist/storage/mongo/util.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +9 -0
- package/dist/sync/RequestTracker.js +19 -0
- package/dist/sync/RequestTracker.js.map +1 -0
- package/dist/sync/sync.d.ts +2 -0
- package/dist/sync/sync.js +51 -18
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +2 -1
- package/dist/sync/util.js +2 -3
- package/dist/sync/util.js.map +1 -1
- package/package.json +4 -4
- package/src/entry/cli-entry.ts +2 -1
- package/src/entry/commands/compact-action.ts +55 -0
- package/src/entry/entry-index.ts +1 -0
- package/src/routes/endpoints/socket-route.ts +9 -1
- package/src/routes/endpoints/sync-stream.ts +10 -1
- package/src/storage/BucketStorage.ts +29 -1
- package/src/storage/mongo/MongoCompactor.ts +356 -0
- package/src/storage/mongo/MongoSyncBucketStorage.ts +25 -14
- package/src/storage/mongo/models.ts +5 -4
- package/src/storage/mongo/util.ts +25 -0
- package/src/sync/RequestTracker.ts +21 -0
- package/src/sync/sync.ts +61 -17
- package/src/sync/util.ts +6 -2
- package/test/src/__snapshots__/sync.test.ts.snap +85 -0
- package/test/src/bucket_validation.test.ts +142 -0
- package/test/src/bucket_validation.ts +116 -0
- package/test/src/compacting.test.ts +207 -0
- package/test/src/data_storage.test.ts +19 -60
- package/test/src/slow_tests.test.ts +144 -102
- package/test/src/sync.test.ts +176 -28
- package/test/src/util.ts +65 -1
- package/test/src/wal_stream_utils.ts +13 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { SqlSyncRules } from '@powersync/service-sync-rules';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { makeTestTable, MONGO_STORAGE_FACTORY } from './util.js';
|
|
4
|
+
import { oneFromAsync } from './wal_stream_utils.js';
|
|
5
|
+
import { MongoCompactOptions } from '@/storage/mongo/MongoCompactor.js';
|
|
6
|
+
import { reduceBucket, validateCompactedBucket, validateBucket } from './bucket_validation.js';
|
|
7
|
+
|
|
8
|
+
const TEST_TABLE = makeTestTable('test', ['id']);
|
|
9
|
+
|
|
10
|
+
// Test with the default options - large batch sizes
|
|
11
|
+
describe('compacting buckets - default options', () => compactTests({}));
|
|
12
|
+
|
|
13
|
+
// Also test with the miniumum batch sizes, forcing usage of multiple batches internally
|
|
14
|
+
describe('compacting buckets - batched', () =>
|
|
15
|
+
compactTests({ clearBatchLimit: 2, moveBatchLimit: 1, moveBatchQueryLimit: 1 }));
|
|
16
|
+
|
|
17
|
+
function compactTests(compactOptions: MongoCompactOptions) {
|
|
18
|
+
const factory = MONGO_STORAGE_FACTORY;
|
|
19
|
+
|
|
20
|
+
test('compacting (1)', async () => {
|
|
21
|
+
const sync_rules = SqlSyncRules.fromYaml(`
|
|
22
|
+
bucket_definitions:
|
|
23
|
+
global:
|
|
24
|
+
data: [select * from test]
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
const storage = (await factory()).getInstance({ id: 1, sync_rules, slot_name: 'test' });
|
|
28
|
+
|
|
29
|
+
const result = await storage.startBatch({}, async (batch) => {
|
|
30
|
+
await batch.save({
|
|
31
|
+
sourceTable: TEST_TABLE,
|
|
32
|
+
tag: 'insert',
|
|
33
|
+
after: {
|
|
34
|
+
id: 't1'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await batch.save({
|
|
39
|
+
sourceTable: TEST_TABLE,
|
|
40
|
+
tag: 'insert',
|
|
41
|
+
after: {
|
|
42
|
+
id: 't2'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await batch.save({
|
|
47
|
+
sourceTable: TEST_TABLE,
|
|
48
|
+
tag: 'update',
|
|
49
|
+
after: {
|
|
50
|
+
id: 't2'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const checkpoint = result!.flushed_op;
|
|
56
|
+
|
|
57
|
+
const batchBefore = await oneFromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
58
|
+
const dataBefore = batchBefore.batch.data;
|
|
59
|
+
|
|
60
|
+
expect(dataBefore).toMatchObject([
|
|
61
|
+
{
|
|
62
|
+
checksum: 2634521662,
|
|
63
|
+
object_id: 't1',
|
|
64
|
+
op: 'PUT',
|
|
65
|
+
op_id: '1'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
checksum: 4243212114,
|
|
69
|
+
object_id: 't2',
|
|
70
|
+
op: 'PUT',
|
|
71
|
+
op_id: '2'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
checksum: 4243212114,
|
|
75
|
+
object_id: 't2',
|
|
76
|
+
op: 'PUT',
|
|
77
|
+
op_id: '3'
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
await storage.compact(compactOptions);
|
|
82
|
+
|
|
83
|
+
const batchAfter = await oneFromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
84
|
+
const dataAfter = batchAfter.batch.data;
|
|
85
|
+
|
|
86
|
+
expect(batchAfter.targetOp).toEqual(3n);
|
|
87
|
+
expect(dataAfter).toMatchObject([
|
|
88
|
+
{
|
|
89
|
+
checksum: 2634521662,
|
|
90
|
+
object_id: 't1',
|
|
91
|
+
op: 'PUT',
|
|
92
|
+
op_id: '1'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
checksum: 4243212114,
|
|
96
|
+
op: 'MOVE',
|
|
97
|
+
op_id: '2'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
checksum: 4243212114,
|
|
101
|
+
object_id: 't2',
|
|
102
|
+
op: 'PUT',
|
|
103
|
+
op_id: '3'
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
validateCompactedBucket(dataBefore, dataAfter);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('compacting (2)', async () => {
|
|
111
|
+
const sync_rules = SqlSyncRules.fromYaml(`
|
|
112
|
+
bucket_definitions:
|
|
113
|
+
global:
|
|
114
|
+
data: [select * from test]
|
|
115
|
+
`);
|
|
116
|
+
|
|
117
|
+
const storage = (await factory()).getInstance({ id: 1, sync_rules, slot_name: 'test' });
|
|
118
|
+
|
|
119
|
+
const result = await storage.startBatch({}, async (batch) => {
|
|
120
|
+
await batch.save({
|
|
121
|
+
sourceTable: TEST_TABLE,
|
|
122
|
+
tag: 'insert',
|
|
123
|
+
after: {
|
|
124
|
+
id: 't1'
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await batch.save({
|
|
129
|
+
sourceTable: TEST_TABLE,
|
|
130
|
+
tag: 'insert',
|
|
131
|
+
after: {
|
|
132
|
+
id: 't2'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await batch.save({
|
|
137
|
+
sourceTable: TEST_TABLE,
|
|
138
|
+
tag: 'delete',
|
|
139
|
+
before: {
|
|
140
|
+
id: 't1'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await batch.save({
|
|
145
|
+
sourceTable: TEST_TABLE,
|
|
146
|
+
tag: 'update',
|
|
147
|
+
after: {
|
|
148
|
+
id: 't2'
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const checkpoint = result!.flushed_op;
|
|
154
|
+
|
|
155
|
+
const batchBefore = await oneFromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
156
|
+
const dataBefore = batchBefore.batch.data;
|
|
157
|
+
|
|
158
|
+
expect(dataBefore).toMatchObject([
|
|
159
|
+
{
|
|
160
|
+
checksum: 2634521662,
|
|
161
|
+
object_id: 't1',
|
|
162
|
+
op: 'PUT',
|
|
163
|
+
op_id: '1'
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
checksum: 4243212114,
|
|
167
|
+
object_id: 't2',
|
|
168
|
+
op: 'PUT',
|
|
169
|
+
op_id: '2'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
checksum: 4228978084,
|
|
173
|
+
object_id: 't1',
|
|
174
|
+
op: 'REMOVE',
|
|
175
|
+
op_id: '3'
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
checksum: 4243212114,
|
|
179
|
+
object_id: 't2',
|
|
180
|
+
op: 'PUT',
|
|
181
|
+
op_id: '4'
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
await storage.compact(compactOptions);
|
|
186
|
+
|
|
187
|
+
const batchAfter = await oneFromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
188
|
+
const dataAfter = batchAfter.batch.data;
|
|
189
|
+
|
|
190
|
+
expect(batchAfter.targetOp).toEqual(4n);
|
|
191
|
+
expect(dataAfter).toMatchObject([
|
|
192
|
+
{
|
|
193
|
+
checksum: -1778190028,
|
|
194
|
+
op: 'CLEAR',
|
|
195
|
+
op_id: '3'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
checksum: 4243212114,
|
|
199
|
+
object_id: 't2',
|
|
200
|
+
op: 'PUT',
|
|
201
|
+
op_id: '4'
|
|
202
|
+
}
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
validateCompactedBucket(dataBefore, dataAfter);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import { RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
2
|
-
import * as bson from 'bson';
|
|
3
2
|
import { describe, expect, test } from 'vitest';
|
|
4
|
-
import { SourceTable } from '../../src/storage/SourceTable.js';
|
|
5
|
-
import { hashData } from '../../src/util/utils.js';
|
|
6
|
-
import { MONGO_STORAGE_FACTORY, StorageFactory } from './util.js';
|
|
7
|
-
import { SyncBucketData } from '../../src/util/protocol-types.js';
|
|
8
3
|
import { BucketDataBatchOptions } from '../../src/storage/BucketStorage.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
function makeTestTable(name: string, columns?: string[] | undefined) {
|
|
12
|
-
const relId = hashData('table', name, (columns ?? ['id']).join(','));
|
|
13
|
-
const id = new bson.ObjectId('6544e3899293153fa7b38331');
|
|
14
|
-
return new SourceTable(
|
|
15
|
-
id,
|
|
16
|
-
SourceTable.DEFAULT_TAG,
|
|
17
|
-
relId,
|
|
18
|
-
SourceTable.DEFAULT_SCHEMA,
|
|
19
|
-
name,
|
|
20
|
-
(columns ?? ['id']).map((column) => ({ name: column, typeOid: 25 })),
|
|
21
|
-
true
|
|
22
|
-
);
|
|
23
|
-
}
|
|
4
|
+
import { getBatchData, getBatchMeta, makeTestTable, MONGO_STORAGE_FACTORY, StorageFactory } from './util.js';
|
|
5
|
+
import { fromAsync, oneFromAsync } from './wal_stream_utils.js';
|
|
24
6
|
|
|
25
7
|
const TEST_TABLE = makeTestTable('test', ['id']);
|
|
26
8
|
|
|
@@ -236,7 +218,7 @@ bucket_definitions:
|
|
|
236
218
|
const checkpoint = result!.flushed_op;
|
|
237
219
|
|
|
238
220
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
239
|
-
const data = batch[0].data.map((d) => {
|
|
221
|
+
const data = batch[0].batch.data.map((d) => {
|
|
240
222
|
return {
|
|
241
223
|
op: d.op,
|
|
242
224
|
object_id: d.object_id,
|
|
@@ -504,7 +486,7 @@ bucket_definitions:
|
|
|
504
486
|
});
|
|
505
487
|
const checkpoint = result!.flushed_op;
|
|
506
488
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
507
|
-
const data = batch[0].data.map((d) => {
|
|
489
|
+
const data = batch[0].batch.data.map((d) => {
|
|
508
490
|
return {
|
|
509
491
|
op: d.op,
|
|
510
492
|
object_id: d.object_id
|
|
@@ -568,7 +550,7 @@ bucket_definitions:
|
|
|
568
550
|
const checkpoint = result!.flushed_op;
|
|
569
551
|
|
|
570
552
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
571
|
-
const data = batch[0].data.map((d) => {
|
|
553
|
+
const data = batch[0].batch.data.map((d) => {
|
|
572
554
|
return {
|
|
573
555
|
op: d.op,
|
|
574
556
|
object_id: d.object_id,
|
|
@@ -680,7 +662,7 @@ bucket_definitions:
|
|
|
680
662
|
|
|
681
663
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']])));
|
|
682
664
|
|
|
683
|
-
const data = batch[0].data.map((d) => {
|
|
665
|
+
const data = batch[0].batch.data.map((d) => {
|
|
684
666
|
return {
|
|
685
667
|
op: d.op,
|
|
686
668
|
object_id: d.object_id,
|
|
@@ -855,7 +837,7 @@ bucket_definitions:
|
|
|
855
837
|
const checkpoint2 = result2!.flushed_op;
|
|
856
838
|
|
|
857
839
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint2, new Map([['global[]', checkpoint1]])));
|
|
858
|
-
const data = batch[0].data.map((d) => {
|
|
840
|
+
const data = batch[0].batch.data.map((d) => {
|
|
859
841
|
return {
|
|
860
842
|
op: d.op,
|
|
861
843
|
object_id: d.object_id,
|
|
@@ -939,7 +921,7 @@ bucket_definitions:
|
|
|
939
921
|
const checkpoint3 = result3!.flushed_op;
|
|
940
922
|
|
|
941
923
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint3, new Map([['global[]', checkpoint1]])));
|
|
942
|
-
const data = batch[0].data.map((d) => {
|
|
924
|
+
const data = batch[0].batch.data.map((d) => {
|
|
943
925
|
return {
|
|
944
926
|
op: d.op,
|
|
945
927
|
object_id: d.object_id,
|
|
@@ -1031,7 +1013,7 @@ bucket_definitions:
|
|
|
1031
1013
|
const checkpoint3 = result3!.flushed_op;
|
|
1032
1014
|
|
|
1033
1015
|
const batch = await fromAsync(storage.getBucketDataBatch(checkpoint3, new Map([['global[]', checkpoint1]])));
|
|
1034
|
-
const data = batch[0].data.map((d) => {
|
|
1016
|
+
const data = batch[0].batch.data.map((d) => {
|
|
1035
1017
|
return {
|
|
1036
1018
|
op: d.op,
|
|
1037
1019
|
object_id: d.object_id,
|
|
@@ -1133,7 +1115,7 @@ bucket_definitions:
|
|
|
1133
1115
|
});
|
|
1134
1116
|
|
|
1135
1117
|
const batch2 = await fromAsync(
|
|
1136
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1[0].next_after]]), options)
|
|
1118
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1[0].batch.next_after]]), options)
|
|
1137
1119
|
);
|
|
1138
1120
|
expect(getBatchData(batch2)).toEqual([
|
|
1139
1121
|
{ op_id: '3', op: 'PUT', object_id: 'large2', checksum: 1795508474 },
|
|
@@ -1146,7 +1128,7 @@ bucket_definitions:
|
|
|
1146
1128
|
});
|
|
1147
1129
|
|
|
1148
1130
|
const batch3 = await fromAsync(
|
|
1149
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2[0].next_after]]), options)
|
|
1131
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2[0].batch.next_after]]), options)
|
|
1150
1132
|
);
|
|
1151
1133
|
expect(getBatchData(batch3)).toEqual([]);
|
|
1152
1134
|
expect(getBatchMeta(batch3)).toEqual(null);
|
|
@@ -1223,7 +1205,7 @@ bucket_definitions:
|
|
|
1223
1205
|
});
|
|
1224
1206
|
|
|
1225
1207
|
const batch2 = await fromAsync(
|
|
1226
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1[0].next_after]]), options)
|
|
1208
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1[0].batch.next_after]]), options)
|
|
1227
1209
|
);
|
|
1228
1210
|
expect(getBatchData(batch2)).toEqual([{ op_id: '3', op: 'PUT', object_id: 'large2', checksum: 1607205872 }]);
|
|
1229
1211
|
expect(getBatchMeta(batch2)).toEqual({
|
|
@@ -1233,7 +1215,7 @@ bucket_definitions:
|
|
|
1233
1215
|
});
|
|
1234
1216
|
|
|
1235
1217
|
const batch3 = await fromAsync(
|
|
1236
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2[0].next_after]]), options)
|
|
1218
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2[0].batch.next_after]]), options)
|
|
1237
1219
|
);
|
|
1238
1220
|
expect(getBatchData(batch3)).toEqual([{ op_id: '4', op: 'PUT', object_id: 'test3', checksum: 1359888332 }]);
|
|
1239
1221
|
expect(getBatchMeta(batch3)).toEqual({
|
|
@@ -1270,7 +1252,9 @@ bucket_definitions:
|
|
|
1270
1252
|
|
|
1271
1253
|
const checkpoint = result!.flushed_op;
|
|
1272
1254
|
|
|
1273
|
-
const batch1 = await
|
|
1255
|
+
const batch1 = await oneFromAsync(
|
|
1256
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', '0']]), { limit: 4 })
|
|
1257
|
+
);
|
|
1274
1258
|
|
|
1275
1259
|
expect(getBatchData(batch1)).toEqual([
|
|
1276
1260
|
{ op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
|
|
@@ -1285,8 +1269,8 @@ bucket_definitions:
|
|
|
1285
1269
|
next_after: '4'
|
|
1286
1270
|
});
|
|
1287
1271
|
|
|
1288
|
-
const batch2 = await
|
|
1289
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1
|
|
1272
|
+
const batch2 = await oneFromAsync(
|
|
1273
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch1.batch.next_after]]), {
|
|
1290
1274
|
limit: 4
|
|
1291
1275
|
})
|
|
1292
1276
|
);
|
|
@@ -1302,7 +1286,7 @@ bucket_definitions:
|
|
|
1302
1286
|
});
|
|
1303
1287
|
|
|
1304
1288
|
const batch3 = await fromAsync(
|
|
1305
|
-
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2
|
|
1289
|
+
storage.getBucketDataBatch(checkpoint, new Map([['global[]', batch2.batch.next_after]]), {
|
|
1306
1290
|
limit: 4
|
|
1307
1291
|
})
|
|
1308
1292
|
);
|
|
@@ -1311,28 +1295,3 @@ bucket_definitions:
|
|
|
1311
1295
|
expect(getBatchMeta(batch3)).toEqual(null);
|
|
1312
1296
|
});
|
|
1313
1297
|
}
|
|
1314
|
-
|
|
1315
|
-
function getBatchData(batch: SyncBucketData[]) {
|
|
1316
|
-
if (batch.length == 0) {
|
|
1317
|
-
return [];
|
|
1318
|
-
}
|
|
1319
|
-
return batch[0].data.map((d) => {
|
|
1320
|
-
return {
|
|
1321
|
-
op_id: d.op_id,
|
|
1322
|
-
op: d.op,
|
|
1323
|
-
object_id: d.object_id,
|
|
1324
|
-
checksum: d.checksum
|
|
1325
|
-
};
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function getBatchMeta(batch: SyncBucketData[]) {
|
|
1330
|
-
if (batch.length == 0) {
|
|
1331
|
-
return null;
|
|
1332
|
-
}
|
|
1333
|
-
return {
|
|
1334
|
-
has_more: batch[0].has_more,
|
|
1335
|
-
after: batch[0].after,
|
|
1336
|
-
next_after: batch[0].next_after
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
@@ -4,12 +4,15 @@ import { afterEach, describe, expect, test } from 'vitest';
|
|
|
4
4
|
import { WalStream, WalStreamOptions } from '../../src/replication/WalStream.js';
|
|
5
5
|
import { getClientCheckpoint } from '../../src/util/utils.js';
|
|
6
6
|
import { env } from './env.js';
|
|
7
|
-
import { MONGO_STORAGE_FACTORY, StorageFactory, TEST_CONNECTION_OPTIONS, connectPgPool } from './util.js';
|
|
7
|
+
import { MONGO_STORAGE_FACTORY, StorageFactory, TEST_CONNECTION_OPTIONS, clearTestDb, connectPgPool } from './util.js';
|
|
8
8
|
|
|
9
9
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
10
10
|
import { SqliteRow } from '@powersync/service-sync-rules';
|
|
11
11
|
import { MongoBucketStorage } from '../../src/storage/MongoBucketStorage.js';
|
|
12
12
|
import { PgManager } from '../../src/util/PgManager.js';
|
|
13
|
+
import { mapOpEntry } from '@/storage/storage-index.js';
|
|
14
|
+
import { reduceBucket, validateCompactedBucket, validateBucket } from './bucket_validation.js';
|
|
15
|
+
import * as timers from 'node:timers/promises';
|
|
13
16
|
|
|
14
17
|
describe('slow tests - mongodb', function () {
|
|
15
18
|
// These are slow, inconsistent tests.
|
|
@@ -51,130 +54,169 @@ function defineSlowTests(factory: StorageFactory) {
|
|
|
51
54
|
// * Skipping LSNs after a keepalive message
|
|
52
55
|
// * Skipping LSNs when source transactions overlap
|
|
53
56
|
test(
|
|
54
|
-
'repeated replication',
|
|
57
|
+
'repeated replication - basic',
|
|
55
58
|
async () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
await testRepeatedReplication({ compact: false, maxBatchSize: 50, numBatches: 5 });
|
|
60
|
+
},
|
|
61
|
+
{ timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }
|
|
62
|
+
);
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
test(
|
|
65
|
+
'repeated replication - compacted',
|
|
66
|
+
async () => {
|
|
67
|
+
await testRepeatedReplication({ compact: true, maxBatchSize: 100, numBatches: 2 });
|
|
68
|
+
},
|
|
69
|
+
{ timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
async function testRepeatedReplication(testOptions: { compact: boolean; maxBatchSize: number; numBatches: number }) {
|
|
73
|
+
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
74
|
+
const replicationConnection = await connections.replicationConnection();
|
|
75
|
+
const pool = connections.pool;
|
|
76
|
+
await clearTestDb(pool);
|
|
77
|
+
const f = (await factory()) as MongoBucketStorage;
|
|
78
|
+
|
|
79
|
+
const syncRuleContent = `
|
|
62
80
|
bucket_definitions:
|
|
63
81
|
global:
|
|
64
82
|
data:
|
|
65
83
|
- SELECT * FROM "test_data"
|
|
66
84
|
`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
const syncRules = await f.updateSyncRules({ content: syncRuleContent });
|
|
86
|
+
const storage = f.getInstance(syncRules.parsed());
|
|
87
|
+
abortController = new AbortController();
|
|
88
|
+
const options: WalStreamOptions = {
|
|
89
|
+
abort_signal: abortController.signal,
|
|
90
|
+
connections,
|
|
91
|
+
storage: storage,
|
|
92
|
+
factory: f
|
|
93
|
+
};
|
|
94
|
+
walStream = new WalStream(options);
|
|
95
|
+
|
|
96
|
+
await pool.query(
|
|
97
|
+
`CREATE TABLE test_data(id uuid primary key default uuid_generate_v4(), description text, num decimal)`
|
|
98
|
+
);
|
|
99
|
+
await pool.query(`ALTER TABLE test_data REPLICA IDENTITY FULL`);
|
|
100
|
+
|
|
101
|
+
await walStream.initReplication(replicationConnection);
|
|
102
|
+
await storage.autoActivate();
|
|
103
|
+
let abort = false;
|
|
104
|
+
streamPromise = walStream.streamChanges(replicationConnection).finally(() => {
|
|
105
|
+
abort = true;
|
|
106
|
+
});
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
|
|
109
|
+
while (!abort && Date.now() - start < TEST_DURATION_MS) {
|
|
110
|
+
const bg = async () => {
|
|
111
|
+
for (let j = 0; j < testOptions.numBatches && !abort; j++) {
|
|
112
|
+
const n = Math.max(1, Math.floor(Math.random() * testOptions.maxBatchSize));
|
|
113
|
+
let statements: pgwire.Statement[] = [];
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
const description = `test${i}`;
|
|
116
|
+
statements.push({
|
|
117
|
+
statement: `INSERT INTO test_data(description, num) VALUES($1, $2) returning id as test_id`,
|
|
118
|
+
params: [
|
|
119
|
+
{ type: 'varchar', value: description },
|
|
120
|
+
{ type: 'float8', value: Math.random() }
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const results = await pool.query(...statements);
|
|
125
|
+
const ids = results.results.map((sub) => {
|
|
126
|
+
return sub.rows[0][0] as string;
|
|
127
|
+
});
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 30));
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let statements: pgwire.Statement[] = [];
|
|
97
|
-
for (let i = 0; i < n; i++) {
|
|
98
|
-
const description = `test${i}`;
|
|
99
|
-
statements.push({
|
|
100
|
-
statement: `INSERT INTO test_data(description, num) VALUES($1, $2) returning id as test_id`,
|
|
130
|
+
if (Math.random() > 0.5) {
|
|
131
|
+
const updateStatements: pgwire.Statement[] = ids.map((id) => {
|
|
132
|
+
return {
|
|
133
|
+
statement: `UPDATE test_data SET num = $2 WHERE id = $1`,
|
|
101
134
|
params: [
|
|
102
|
-
{ type: '
|
|
135
|
+
{ type: 'uuid', value: id },
|
|
103
136
|
{ type: 'float8', value: Math.random() }
|
|
104
137
|
]
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const results = await pool.query(...statements);
|
|
108
|
-
const ids = results.results.map((sub) => {
|
|
109
|
-
return sub.rows[0][0] as string;
|
|
138
|
+
};
|
|
110
139
|
});
|
|
111
|
-
await new Promise((resolve) => setTimeout(resolve, Math.random() * 30));
|
|
112
140
|
|
|
141
|
+
await pool.query(...updateStatements);
|
|
113
142
|
if (Math.random() > 0.5) {
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
statement: `UPDATE test_data SET num = $2 WHERE id = $1`,
|
|
117
|
-
params: [
|
|
118
|
-
{ type: 'uuid', value: id },
|
|
119
|
-
{ type: 'float8', value: Math.random() }
|
|
120
|
-
]
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
|
|
143
|
+
// Special case - an update that doesn't change data
|
|
124
144
|
await pool.query(...updateStatements);
|
|
125
|
-
if (Math.random() > 0.5) {
|
|
126
|
-
// Special case - an update that doesn't change data
|
|
127
|
-
await pool.query(...updateStatements);
|
|
128
|
-
}
|
|
129
145
|
}
|
|
130
|
-
|
|
131
|
-
const deleteStatements: pgwire.Statement[] = ids.map((id) => {
|
|
132
|
-
return {
|
|
133
|
-
statement: `DELETE FROM test_data WHERE id = $1`,
|
|
134
|
-
params: [{ type: 'uuid', value: id }]
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
await pool.query(...deleteStatements);
|
|
138
|
-
|
|
139
|
-
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10));
|
|
140
146
|
}
|
|
141
|
-
};
|
|
142
147
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
const deleteStatements: pgwire.Statement[] = ids.map((id) => {
|
|
149
|
+
return {
|
|
150
|
+
statement: `DELETE FROM test_data WHERE id = $1`,
|
|
151
|
+
params: [{ type: 'uuid', value: id }]
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
await pool.query(...deleteStatements);
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
for (let op of ops) {
|
|
161
|
-
const key = op.source_key.toHexString();
|
|
162
|
-
if (op.op == 'PUT') {
|
|
163
|
-
active.add(key);
|
|
164
|
-
} else if (op.op == 'REMOVE') {
|
|
165
|
-
active.delete(key);
|
|
160
|
+
let compactController = new AbortController();
|
|
161
|
+
|
|
162
|
+
const bgCompact = async () => {
|
|
163
|
+
// Repeatedly compact, and check that the compact conditions hold
|
|
164
|
+
while (!compactController.signal.aborted) {
|
|
165
|
+
const delay = Math.random() * 50;
|
|
166
|
+
try {
|
|
167
|
+
await timers.setTimeout(delay, undefined, { signal: compactController.signal });
|
|
168
|
+
} catch (e) {
|
|
169
|
+
break;
|
|
166
170
|
}
|
|
171
|
+
|
|
172
|
+
const checkpoint = BigInt((await storage.getCheckpoint()).checkpoint);
|
|
173
|
+
const opsBefore = (await f.db.bucket_data.find().sort({ _id: 1 }).toArray())
|
|
174
|
+
.filter((row) => row._id.o <= checkpoint)
|
|
175
|
+
.map(mapOpEntry);
|
|
176
|
+
await storage.compact({ maxOpId: checkpoint });
|
|
177
|
+
const opsAfter = (await f.db.bucket_data.find().sort({ _id: 1 }).toArray())
|
|
178
|
+
.filter((row) => row._id.o <= checkpoint)
|
|
179
|
+
.map(mapOpEntry);
|
|
180
|
+
|
|
181
|
+
validateCompactedBucket(opsBefore, opsAfter);
|
|
167
182
|
}
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Call the above loop multiple times concurrently
|
|
186
|
+
const promises = [1, 2, 3].map((i) => bg());
|
|
187
|
+
const compactPromise = testOptions.compact ? bgCompact() : null;
|
|
188
|
+
await Promise.all(promises);
|
|
189
|
+
compactController.abort();
|
|
190
|
+
await compactPromise;
|
|
191
|
+
|
|
192
|
+
// Wait for replication to finish
|
|
193
|
+
let checkpoint = await getClientCheckpoint(pool, storage.factory, { timeout: TIMEOUT_MARGIN_MS });
|
|
194
|
+
|
|
195
|
+
// Check that all inserts have been deleted again
|
|
196
|
+
const docs = await f.db.current_data.find().toArray();
|
|
197
|
+
const transformed = docs.map((doc) => {
|
|
198
|
+
return bson.deserialize((doc.data as mongo.Binary).buffer) as SqliteRow;
|
|
199
|
+
});
|
|
200
|
+
expect(transformed).toEqual([]);
|
|
201
|
+
|
|
202
|
+
// Check that each PUT has a REMOVE
|
|
203
|
+
const ops = await f.db.bucket_data.find().sort({ _id: 1 }).toArray();
|
|
204
|
+
|
|
205
|
+
// All a single bucket in this test
|
|
206
|
+
const bucket = ops.map((op) => mapOpEntry(op));
|
|
207
|
+
const reduced = reduceBucket(bucket);
|
|
208
|
+
expect(reduced).toMatchObject([
|
|
209
|
+
{
|
|
210
|
+
op_id: '0',
|
|
211
|
+
op: 'CLEAR'
|
|
170
212
|
}
|
|
171
|
-
|
|
213
|
+
// Should contain no additional data
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
172
216
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
{ timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }
|
|
177
|
-
);
|
|
217
|
+
abortController.abort();
|
|
218
|
+
await streamPromise;
|
|
219
|
+
}
|
|
178
220
|
|
|
179
221
|
// Test repeatedly performing initial replication.
|
|
180
222
|
//
|
|
@@ -184,6 +226,7 @@ bucket_definitions:
|
|
|
184
226
|
'repeated initial replication',
|
|
185
227
|
async () => {
|
|
186
228
|
const pool = await connectPgPool();
|
|
229
|
+
await clearTestDb(pool);
|
|
187
230
|
const f = await factory();
|
|
188
231
|
|
|
189
232
|
const syncRuleContent = `
|
|
@@ -196,7 +239,6 @@ bucket_definitions:
|
|
|
196
239
|
const storage = f.getInstance(syncRules.parsed());
|
|
197
240
|
|
|
198
241
|
// 1. Setup some base data that will be replicated in initial replication
|
|
199
|
-
await pool.query(`DROP TABLE IF EXISTS test_data`);
|
|
200
242
|
await pool.query(`CREATE TABLE test_data(id uuid primary key default uuid_generate_v4(), description text)`);
|
|
201
243
|
|
|
202
244
|
let statements: pgwire.Statement[] = [];
|