@powersync/service-module-mongodb 0.0.0-dev-20241111122558 → 0.0.0-dev-20241128134723
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 +26 -17
- package/dist/api/MongoRouteAPIAdapter.d.ts +1 -0
- package/dist/api/MongoRouteAPIAdapter.js +54 -21
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +23 -2
- package/dist/replication/ChangeStream.js +178 -42
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +7 -4
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/MongoErrorRateLimiter.js +0 -6
- package/dist/replication/MongoErrorRateLimiter.js.map +1 -1
- package/dist/replication/MongoRelation.js +5 -2
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/replication-utils.d.ts +1 -0
- package/dist/replication/replication-utils.js +1 -0
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +38 -2
- package/dist/types/types.js.map +1 -1
- package/package.json +6 -9
- package/src/api/MongoRouteAPIAdapter.ts +53 -21
- package/src/replication/ChangeStream.ts +277 -121
- package/src/replication/ChangeStreamReplicationJob.ts +6 -4
- package/src/replication/MongoErrorRateLimiter.ts +1 -8
- package/src/replication/MongoRelation.ts +5 -2
- package/src/replication/replication-utils.ts +2 -1
- package/src/types/types.ts +43 -3
- package/test/src/change_stream.test.ts +442 -231
- package/test/src/change_stream_utils.ts +54 -27
- package/test/src/mongo_test.test.ts +180 -46
- package/test/src/slow_tests.test.ts +109 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,30 +6,7 @@ import { MongoManager } from '@module/replication/MongoManager.js';
|
|
|
6
6
|
import { ChangeStream, ChangeStreamOptions } from '@module/replication/ChangeStream.js';
|
|
7
7
|
import * as mongo from 'mongodb';
|
|
8
8
|
import { createCheckpoint } from '@module/replication/MongoRelation.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Tests operating on the mongo change stream need to configure the stream and manage asynchronous
|
|
12
|
-
* replication, which gets a little tricky.
|
|
13
|
-
*
|
|
14
|
-
* This wraps a test in a function that configures all the context, and tears it down afterwards.
|
|
15
|
-
*/
|
|
16
|
-
export function changeStreamTest(
|
|
17
|
-
factory: () => Promise<BucketStorageFactory>,
|
|
18
|
-
test: (context: ChangeStreamTestContext) => Promise<void>
|
|
19
|
-
): () => Promise<void> {
|
|
20
|
-
return async () => {
|
|
21
|
-
const f = await factory();
|
|
22
|
-
const connectionManager = new MongoManager(TEST_CONNECTION_OPTIONS);
|
|
23
|
-
|
|
24
|
-
await clearTestDb(connectionManager.db);
|
|
25
|
-
const context = new ChangeStreamTestContext(f, connectionManager);
|
|
26
|
-
try {
|
|
27
|
-
await test(context);
|
|
28
|
-
} finally {
|
|
29
|
-
await context.dispose();
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
}
|
|
9
|
+
import { NormalizedMongoConnectionConfig } from '@module/types/types.js';
|
|
33
10
|
|
|
34
11
|
export class ChangeStreamTestContext {
|
|
35
12
|
private _walStream?: ChangeStream;
|
|
@@ -37,6 +14,20 @@ export class ChangeStreamTestContext {
|
|
|
37
14
|
private streamPromise?: Promise<void>;
|
|
38
15
|
public storage?: SyncRulesBucketStorage;
|
|
39
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Tests operating on the mongo change stream need to configure the stream and manage asynchronous
|
|
19
|
+
* replication, which gets a little tricky.
|
|
20
|
+
*
|
|
21
|
+
* This configures all the context, and tears it down afterwards.
|
|
22
|
+
*/
|
|
23
|
+
static async open(factory: () => Promise<BucketStorageFactory>, options?: Partial<NormalizedMongoConnectionConfig>) {
|
|
24
|
+
const f = await factory();
|
|
25
|
+
const connectionManager = new MongoManager({ ...TEST_CONNECTION_OPTIONS, ...options });
|
|
26
|
+
|
|
27
|
+
await clearTestDb(connectionManager.db);
|
|
28
|
+
return new ChangeStreamTestContext(f, connectionManager);
|
|
29
|
+
}
|
|
30
|
+
|
|
40
31
|
constructor(
|
|
41
32
|
public factory: BucketStorageFactory,
|
|
42
33
|
public connectionManager: MongoManager
|
|
@@ -48,6 +39,10 @@ export class ChangeStreamTestContext {
|
|
|
48
39
|
await this.connectionManager.destroy();
|
|
49
40
|
}
|
|
50
41
|
|
|
42
|
+
async [Symbol.asyncDispose]() {
|
|
43
|
+
await this.dispose();
|
|
44
|
+
}
|
|
45
|
+
|
|
51
46
|
get client() {
|
|
52
47
|
return this.connectionManager.client;
|
|
53
48
|
}
|
|
@@ -96,7 +91,7 @@ export class ChangeStreamTestContext {
|
|
|
96
91
|
getClientCheckpoint(this.client, this.db, this.factory, { timeout: options?.timeout ?? 15_000 }),
|
|
97
92
|
this.streamPromise
|
|
98
93
|
]);
|
|
99
|
-
if (typeof checkpoint == undefined) {
|
|
94
|
+
if (typeof checkpoint == 'undefined') {
|
|
100
95
|
// This indicates an issue with the test setup - streamingPromise completed instead
|
|
101
96
|
// of getClientCheckpoint()
|
|
102
97
|
throw new Error('Test failure - streamingPromise completed');
|
|
@@ -110,14 +105,32 @@ export class ChangeStreamTestContext {
|
|
|
110
105
|
return fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
|
|
111
106
|
}
|
|
112
107
|
|
|
113
|
-
async getBucketData(
|
|
108
|
+
async getBucketData(
|
|
109
|
+
bucket: string,
|
|
110
|
+
start?: string,
|
|
111
|
+
options?: { timeout?: number; limit?: number; chunkLimitBytes?: number }
|
|
112
|
+
) {
|
|
114
113
|
start ??= '0';
|
|
115
114
|
let checkpoint = await this.getCheckpoint(options);
|
|
116
115
|
const map = new Map<string, string>([[bucket, start]]);
|
|
117
|
-
const batch = this.storage!.getBucketDataBatch(checkpoint, map
|
|
116
|
+
const batch = this.storage!.getBucketDataBatch(checkpoint, map, {
|
|
117
|
+
limit: options?.limit,
|
|
118
|
+
chunkLimitBytes: options?.chunkLimitBytes
|
|
119
|
+
});
|
|
118
120
|
const batches = await fromAsync(batch);
|
|
119
121
|
return batches[0]?.batch.data ?? [];
|
|
120
122
|
}
|
|
123
|
+
|
|
124
|
+
async getChecksums(buckets: string[], options?: { timeout?: number }) {
|
|
125
|
+
let checkpoint = await this.getCheckpoint(options);
|
|
126
|
+
return this.storage!.getChecksums(checkpoint, buckets);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getChecksum(bucket: string, options?: { timeout?: number }) {
|
|
130
|
+
let checkpoint = await this.getCheckpoint(options);
|
|
131
|
+
const map = await this.storage!.getChecksums(checkpoint, [bucket]);
|
|
132
|
+
return map.get(bucket);
|
|
133
|
+
}
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
export async function getClientCheckpoint(
|
|
@@ -149,3 +162,17 @@ export async function getClientCheckpoint(
|
|
|
149
162
|
|
|
150
163
|
throw new Error(`Timeout while waiting for checkpoint ${lsn}. Last checkpoint: ${lastCp?.lsn}`);
|
|
151
164
|
}
|
|
165
|
+
|
|
166
|
+
export async function setSnapshotHistorySeconds(client: mongo.MongoClient, seconds: number) {
|
|
167
|
+
const { minSnapshotHistoryWindowInSeconds: currentValue } = await client
|
|
168
|
+
.db('admin')
|
|
169
|
+
.command({ getParameter: 1, minSnapshotHistoryWindowInSeconds: 1 });
|
|
170
|
+
|
|
171
|
+
await client.db('admin').command({ setParameter: 1, minSnapshotHistoryWindowInSeconds: seconds });
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
async [Symbol.asyncDispose]() {
|
|
175
|
+
await client.db('admin').command({ setParameter: 1, minSnapshotHistoryWindowInSeconds: currentValue });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { MongoRouteAPIAdapter } from '@module/api/MongoRouteAPIAdapter.js';
|
|
2
2
|
import { ChangeStream } from '@module/replication/ChangeStream.js';
|
|
3
3
|
import { constructAfterRecord } from '@module/replication/MongoRelation.js';
|
|
4
|
-
import { SqliteRow } from '@powersync/service-sync-rules';
|
|
4
|
+
import { SqliteRow, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
5
5
|
import * as mongo from 'mongodb';
|
|
6
6
|
import { describe, expect, test } from 'vitest';
|
|
7
7
|
import { clearTestDb, connectMongoData, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
8
|
+
import { PostImagesOption } from '@module/types/types.js';
|
|
8
9
|
|
|
9
10
|
describe('mongo data types', () => {
|
|
10
11
|
async function setupTable(db: mongo.Db) {
|
|
@@ -245,58 +246,191 @@ describe('mongo data types', () => {
|
|
|
245
246
|
});
|
|
246
247
|
|
|
247
248
|
test('connection schema', async () => {
|
|
248
|
-
|
|
249
|
+
await using adapter = new MongoRouteAPIAdapter({
|
|
249
250
|
type: 'mongodb',
|
|
250
251
|
...TEST_CONNECTION_OPTIONS
|
|
251
252
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await clearTestDb(db);
|
|
253
|
+
const db = adapter.db;
|
|
254
|
+
await clearTestDb(db);
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
const collection = db.collection('test_data');
|
|
257
|
+
await setupTable(db);
|
|
258
|
+
await insert(collection);
|
|
259
|
+
|
|
260
|
+
const schema = await adapter.getConnectionSchema();
|
|
261
|
+
const dbSchema = schema.filter((s) => s.name == TEST_CONNECTION_OPTIONS.database)[0];
|
|
262
|
+
expect(dbSchema).not.toBeNull();
|
|
263
|
+
expect(dbSchema.tables).toMatchObject([
|
|
264
|
+
{
|
|
265
|
+
name: 'test_data',
|
|
266
|
+
columns: [
|
|
267
|
+
{ name: '_id', sqlite_type: 4, internal_type: 'Integer' },
|
|
268
|
+
{ name: 'bool', sqlite_type: 4, internal_type: 'Boolean' },
|
|
269
|
+
{ name: 'bytea', sqlite_type: 1, internal_type: 'Binary' },
|
|
270
|
+
{ name: 'date', sqlite_type: 2, internal_type: 'Date' },
|
|
271
|
+
{ name: 'decimal', sqlite_type: 2, internal_type: 'Decimal' },
|
|
272
|
+
{ name: 'float', sqlite_type: 8, internal_type: 'Double' },
|
|
273
|
+
{ name: 'int2', sqlite_type: 4, internal_type: 'Integer' },
|
|
274
|
+
{ name: 'int4', sqlite_type: 4, internal_type: 'Integer' },
|
|
275
|
+
{ name: 'int8', sqlite_type: 4, internal_type: 'Long' },
|
|
276
|
+
// We can fix these later
|
|
277
|
+
{ name: 'js', sqlite_type: 2, internal_type: 'Object' },
|
|
278
|
+
{ name: 'js2', sqlite_type: 2, internal_type: 'Object' },
|
|
279
|
+
{ name: 'maxKey', sqlite_type: 0, internal_type: 'MaxKey' },
|
|
280
|
+
{ name: 'minKey', sqlite_type: 0, internal_type: 'MinKey' },
|
|
281
|
+
{ name: 'nested', sqlite_type: 2, internal_type: 'Object' },
|
|
282
|
+
{ name: 'null', sqlite_type: 0, internal_type: 'Null' },
|
|
283
|
+
{ name: 'objectId', sqlite_type: 2, internal_type: 'ObjectId' },
|
|
284
|
+
// We can fix these later
|
|
285
|
+
{ name: 'pointer', sqlite_type: 2, internal_type: 'Object' },
|
|
286
|
+
{ name: 'pointer2', sqlite_type: 2, internal_type: 'Object' },
|
|
287
|
+
{ name: 'regexp', sqlite_type: 2, internal_type: 'RegExp' },
|
|
288
|
+
// Can fix this later
|
|
289
|
+
{ name: 'symbol', sqlite_type: 2, internal_type: 'String' },
|
|
290
|
+
{ name: 'text', sqlite_type: 2, internal_type: 'String' },
|
|
291
|
+
{ name: 'timestamp', sqlite_type: 4, internal_type: 'Timestamp' },
|
|
292
|
+
{ name: 'undefined', sqlite_type: 0, internal_type: 'Null' },
|
|
293
|
+
{ name: 'uuid', sqlite_type: 2, internal_type: 'UUID' }
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('validate postImages', async () => {
|
|
300
|
+
await using adapter = new MongoRouteAPIAdapter({
|
|
301
|
+
type: 'mongodb',
|
|
302
|
+
...TEST_CONNECTION_OPTIONS,
|
|
303
|
+
postImages: PostImagesOption.READ_ONLY
|
|
304
|
+
});
|
|
305
|
+
const db = adapter.db;
|
|
306
|
+
await clearTestDb(db);
|
|
307
|
+
|
|
308
|
+
const collection = db.collection('test_data');
|
|
309
|
+
await setupTable(db);
|
|
310
|
+
await insert(collection);
|
|
311
|
+
|
|
312
|
+
const rules = SqlSyncRules.fromYaml(
|
|
313
|
+
`
|
|
314
|
+
bucket_definitions:
|
|
315
|
+
global:
|
|
316
|
+
data:
|
|
317
|
+
- select _id as id, * from test_data
|
|
259
318
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
319
|
+
`,
|
|
320
|
+
{
|
|
321
|
+
...adapter.getParseSyncRulesOptions(),
|
|
322
|
+
// No schema-based validation at this point
|
|
323
|
+
schema: undefined
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
const source_table_patterns = rules.getSourceTables();
|
|
327
|
+
const results = await adapter.getDebugTablesInfo(source_table_patterns, rules);
|
|
328
|
+
|
|
329
|
+
const result = results[0];
|
|
330
|
+
expect(result).not.toBeNull();
|
|
331
|
+
expect(result.table).toMatchObject({
|
|
332
|
+
schema: 'powersync_test_data',
|
|
333
|
+
name: 'test_data',
|
|
334
|
+
replication_id: ['_id'],
|
|
335
|
+
data_queries: true,
|
|
336
|
+
parameter_queries: false,
|
|
337
|
+
errors: [
|
|
264
338
|
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{ name: '_id', sqlite_type: 4, internal_type: 'Integer' },
|
|
268
|
-
{ name: 'bool', sqlite_type: 4, internal_type: 'Boolean' },
|
|
269
|
-
{ name: 'bytea', sqlite_type: 1, internal_type: 'Binary' },
|
|
270
|
-
{ name: 'date', sqlite_type: 2, internal_type: 'Date' },
|
|
271
|
-
{ name: 'decimal', sqlite_type: 2, internal_type: 'Decimal' },
|
|
272
|
-
{ name: 'float', sqlite_type: 8, internal_type: 'Double' },
|
|
273
|
-
{ name: 'int2', sqlite_type: 4, internal_type: 'Integer' },
|
|
274
|
-
{ name: 'int4', sqlite_type: 4, internal_type: 'Integer' },
|
|
275
|
-
{ name: 'int8', sqlite_type: 4, internal_type: 'Long' },
|
|
276
|
-
// We can fix these later
|
|
277
|
-
{ name: 'js', sqlite_type: 2, internal_type: 'Object' },
|
|
278
|
-
{ name: 'js2', sqlite_type: 2, internal_type: 'Object' },
|
|
279
|
-
{ name: 'maxKey', sqlite_type: 0, internal_type: 'MaxKey' },
|
|
280
|
-
{ name: 'minKey', sqlite_type: 0, internal_type: 'MinKey' },
|
|
281
|
-
{ name: 'nested', sqlite_type: 2, internal_type: 'Object' },
|
|
282
|
-
{ name: 'null', sqlite_type: 0, internal_type: 'Null' },
|
|
283
|
-
{ name: 'objectId', sqlite_type: 2, internal_type: 'ObjectId' },
|
|
284
|
-
// We can fix these later
|
|
285
|
-
{ name: 'pointer', sqlite_type: 2, internal_type: 'Object' },
|
|
286
|
-
{ name: 'pointer2', sqlite_type: 2, internal_type: 'Object' },
|
|
287
|
-
{ name: 'regexp', sqlite_type: 2, internal_type: 'RegExp' },
|
|
288
|
-
// Can fix this later
|
|
289
|
-
{ name: 'symbol', sqlite_type: 2, internal_type: 'String' },
|
|
290
|
-
{ name: 'text', sqlite_type: 2, internal_type: 'String' },
|
|
291
|
-
{ name: 'timestamp', sqlite_type: 4, internal_type: 'Timestamp' },
|
|
292
|
-
{ name: 'undefined', sqlite_type: 0, internal_type: 'Null' },
|
|
293
|
-
{ name: 'uuid', sqlite_type: 2, internal_type: 'UUID' }
|
|
294
|
-
]
|
|
339
|
+
level: 'fatal',
|
|
340
|
+
message: 'changeStreamPreAndPostImages not enabled on powersync_test_data.test_data'
|
|
295
341
|
}
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
342
|
+
]
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('validate postImages - auto-configure', async () => {
|
|
347
|
+
await using adapter = new MongoRouteAPIAdapter({
|
|
348
|
+
type: 'mongodb',
|
|
349
|
+
...TEST_CONNECTION_OPTIONS,
|
|
350
|
+
postImages: PostImagesOption.AUTO_CONFIGURE
|
|
351
|
+
});
|
|
352
|
+
const db = adapter.db;
|
|
353
|
+
await clearTestDb(db);
|
|
354
|
+
|
|
355
|
+
const collection = db.collection('test_data');
|
|
356
|
+
await setupTable(db);
|
|
357
|
+
await insert(collection);
|
|
358
|
+
|
|
359
|
+
const rules = SqlSyncRules.fromYaml(
|
|
360
|
+
`
|
|
361
|
+
bucket_definitions:
|
|
362
|
+
global:
|
|
363
|
+
data:
|
|
364
|
+
- select _id as id, * from test_data
|
|
365
|
+
|
|
366
|
+
`,
|
|
367
|
+
{
|
|
368
|
+
...adapter.getParseSyncRulesOptions(),
|
|
369
|
+
// No schema-based validation at this point
|
|
370
|
+
schema: undefined
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
const source_table_patterns = rules.getSourceTables();
|
|
374
|
+
const results = await adapter.getDebugTablesInfo(source_table_patterns, rules);
|
|
375
|
+
|
|
376
|
+
const result = results[0];
|
|
377
|
+
expect(result).not.toBeNull();
|
|
378
|
+
expect(result.table).toMatchObject({
|
|
379
|
+
schema: 'powersync_test_data',
|
|
380
|
+
name: 'test_data',
|
|
381
|
+
replication_id: ['_id'],
|
|
382
|
+
data_queries: true,
|
|
383
|
+
parameter_queries: false,
|
|
384
|
+
errors: [
|
|
385
|
+
{
|
|
386
|
+
level: 'warning',
|
|
387
|
+
message:
|
|
388
|
+
'changeStreamPreAndPostImages not enabled on powersync_test_data.test_data, will be enabled automatically'
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('validate postImages - off', async () => {
|
|
395
|
+
await using adapter = new MongoRouteAPIAdapter({
|
|
396
|
+
type: 'mongodb',
|
|
397
|
+
...TEST_CONNECTION_OPTIONS,
|
|
398
|
+
postImages: PostImagesOption.OFF
|
|
399
|
+
});
|
|
400
|
+
const db = adapter.db;
|
|
401
|
+
await clearTestDb(db);
|
|
402
|
+
|
|
403
|
+
const collection = db.collection('test_data');
|
|
404
|
+
await setupTable(db);
|
|
405
|
+
await insert(collection);
|
|
406
|
+
|
|
407
|
+
const rules = SqlSyncRules.fromYaml(
|
|
408
|
+
`
|
|
409
|
+
bucket_definitions:
|
|
410
|
+
global:
|
|
411
|
+
data:
|
|
412
|
+
- select _id as id, * from test_data
|
|
413
|
+
|
|
414
|
+
`,
|
|
415
|
+
{
|
|
416
|
+
...adapter.getParseSyncRulesOptions(),
|
|
417
|
+
// No schema-based validation at this point
|
|
418
|
+
schema: undefined
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
const source_table_patterns = rules.getSourceTables();
|
|
422
|
+
const results = await adapter.getDebugTablesInfo(source_table_patterns, rules);
|
|
423
|
+
|
|
424
|
+
const result = results[0];
|
|
425
|
+
expect(result).not.toBeNull();
|
|
426
|
+
expect(result.table).toMatchObject({
|
|
427
|
+
schema: 'powersync_test_data',
|
|
428
|
+
name: 'test_data',
|
|
429
|
+
replication_id: ['_id'],
|
|
430
|
+
data_queries: true,
|
|
431
|
+
parameter_queries: false,
|
|
432
|
+
errors: []
|
|
433
|
+
});
|
|
300
434
|
});
|
|
301
435
|
});
|
|
302
436
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { MONGO_STORAGE_FACTORY } from '@core-tests/util.js';
|
|
2
|
+
import { BucketStorageFactory } from '@powersync/service-core';
|
|
3
|
+
import * as mongo from 'mongodb';
|
|
4
|
+
import { setTimeout } from 'node:timers/promises';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
import { ChangeStreamTestContext, setSnapshotHistorySeconds } from './change_stream_utils.js';
|
|
7
|
+
import { env } from './env.js';
|
|
8
|
+
|
|
9
|
+
type StorageFactory = () => Promise<BucketStorageFactory>;
|
|
10
|
+
|
|
11
|
+
const BASIC_SYNC_RULES = `
|
|
12
|
+
bucket_definitions:
|
|
13
|
+
global:
|
|
14
|
+
data:
|
|
15
|
+
- SELECT _id as id, description FROM "test_data"
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
describe('change stream slow tests - mongodb', { timeout: 60_000 }, function () {
|
|
19
|
+
if (env.CI || env.SLOW_TESTS) {
|
|
20
|
+
defineSlowTests(MONGO_STORAGE_FACTORY);
|
|
21
|
+
} else {
|
|
22
|
+
// Need something in this file.
|
|
23
|
+
test('no-op', () => {});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function defineSlowTests(factory: StorageFactory) {
|
|
28
|
+
test('replicating snapshot with lots of data', async () => {
|
|
29
|
+
await using context = await ChangeStreamTestContext.open(factory);
|
|
30
|
+
// Test with low minSnapshotHistoryWindowInSeconds, to trigger:
|
|
31
|
+
// > Read timestamp .. is older than the oldest available timestamp.
|
|
32
|
+
// This happened when we had {snapshot: true} in the initial
|
|
33
|
+
// snapshot session.
|
|
34
|
+
await using _ = await setSnapshotHistorySeconds(context.client, 1);
|
|
35
|
+
const { db } = context;
|
|
36
|
+
await context.updateSyncRules(`
|
|
37
|
+
bucket_definitions:
|
|
38
|
+
global:
|
|
39
|
+
data:
|
|
40
|
+
- SELECT _id as id, description, num FROM "test_data1"
|
|
41
|
+
- SELECT _id as id, description, num FROM "test_data2"
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
const collection1 = db.collection('test_data1');
|
|
45
|
+
const collection2 = db.collection('test_data2');
|
|
46
|
+
|
|
47
|
+
let operations: mongo.AnyBulkWriteOperation[] = [];
|
|
48
|
+
for (let i = 0; i < 10_000; i++) {
|
|
49
|
+
operations.push({ insertOne: { document: { description: `pre${i}`, num: i } } });
|
|
50
|
+
}
|
|
51
|
+
await collection1.bulkWrite(operations);
|
|
52
|
+
await collection2.bulkWrite(operations);
|
|
53
|
+
|
|
54
|
+
await context.replicateSnapshot();
|
|
55
|
+
context.startStreaming();
|
|
56
|
+
const checksum = await context.getChecksum('global[]');
|
|
57
|
+
expect(checksum).toMatchObject({
|
|
58
|
+
count: 20_000
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('writes concurrently with snapshot', async () => {
|
|
63
|
+
// If there is an issue with snapshotTime (the start LSN for the
|
|
64
|
+
// changestream), we may miss updates, which this test would
|
|
65
|
+
// hopefully catch.
|
|
66
|
+
|
|
67
|
+
await using context = await ChangeStreamTestContext.open(factory);
|
|
68
|
+
const { db } = context;
|
|
69
|
+
await context.updateSyncRules(`
|
|
70
|
+
bucket_definitions:
|
|
71
|
+
global:
|
|
72
|
+
data:
|
|
73
|
+
- SELECT _id as id, description, num FROM "test_data"
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
const collection = db.collection('test_data');
|
|
77
|
+
|
|
78
|
+
let operations: mongo.AnyBulkWriteOperation[] = [];
|
|
79
|
+
for (let i = 0; i < 5_000; i++) {
|
|
80
|
+
operations.push({ insertOne: { document: { description: `pre${i}`, num: i } } });
|
|
81
|
+
}
|
|
82
|
+
await collection.bulkWrite(operations);
|
|
83
|
+
|
|
84
|
+
const snapshotPromise = context.replicateSnapshot();
|
|
85
|
+
|
|
86
|
+
for (let i = 49; i >= 0; i--) {
|
|
87
|
+
await collection.updateMany(
|
|
88
|
+
{ num: { $gte: i * 100, $lt: i * 100 + 100 } },
|
|
89
|
+
{ $set: { description: 'updated' + i } }
|
|
90
|
+
);
|
|
91
|
+
await setTimeout(20);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await snapshotPromise;
|
|
95
|
+
context.startStreaming();
|
|
96
|
+
|
|
97
|
+
const data = await context.getBucketData('global[]', undefined, { limit: 50_000, chunkLimitBytes: 60_000_000 });
|
|
98
|
+
|
|
99
|
+
const preDocuments = data.filter((d) => JSON.parse(d.data! as string).description.startsWith('pre')).length;
|
|
100
|
+
const updatedDocuments = data.filter((d) => JSON.parse(d.data! as string).description.startsWith('updated')).length;
|
|
101
|
+
|
|
102
|
+
// If the test works properly, preDocuments should be around 2000-3000.
|
|
103
|
+
// The total should be around 9000-9900.
|
|
104
|
+
// However, it is very sensitive to timing, so we allow a wide range.
|
|
105
|
+
// updatedDocuments must be strictly >= 5000, otherwise something broke.
|
|
106
|
+
expect(updatedDocuments).toBeGreaterThanOrEqual(5_000);
|
|
107
|
+
expect(preDocuments).toBeLessThanOrEqual(5_000);
|
|
108
|
+
});
|
|
109
|
+
}
|