@powersync/service-module-mongodb-storage 0.23.2 → 0.24.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 +45 -0
- package/LICENSE +201 -67
- package/dist/storage/MongoBucketStorage.d.ts +1 -0
- package/dist/storage/MongoBucketStorage.js +45 -83
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +60 -39
- package/dist/storage/implementation/MongoBucketBatch.js +123 -296
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +6 -1
- package/dist/storage/implementation/MongoOpIdAllocator.js +12 -5
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +7 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +28 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoReplicationOptions.d.ts +27 -0
- package/dist/storage/implementation/MongoReplicationOptions.js +26 -0
- package/dist/storage/implementation/MongoReplicationOptions.js.map +1 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js +2 -3
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +15 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +36 -14
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/common/BatchOperationBuffer.d.ts +79 -0
- package/dist/storage/implementation/common/BatchOperationBuffer.js +286 -0
- package/dist/storage/implementation/common/BatchOperationBuffer.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +10 -103
- package/dist/storage/implementation/common/PersistedBatch.js +1 -291
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/PreparedPublication.d.ts +43 -0
- package/dist/storage/implementation/common/PreparedPublication.js +2 -0
- package/dist/storage/implementation/common/PreparedPublication.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +0 -2
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +17 -13
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +259 -18
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +21 -15
- package/dist/storage/implementation/v1/PersistedBatchV1.js +58 -24
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +0 -2
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +0 -3
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +25 -14
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +350 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +10 -4
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +39 -28
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +46 -24
- package/dist/storage/implementation/v3/PersistedBatchV3.js +159 -83
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +12 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +41 -13
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +4 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +4 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +27 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +61 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/package.json +9 -10
- package/src/storage/MongoBucketStorage.ts +55 -122
- package/src/storage/implementation/MongoBucketBatch.ts +185 -392
- package/src/storage/implementation/MongoOpIdAllocator.ts +12 -5
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +40 -1
- package/src/storage/implementation/MongoReplicationOptions.ts +28 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +2 -3
- package/src/storage/implementation/MongoSyncRulesLock.ts +39 -17
- package/src/storage/implementation/common/BatchOperationBuffer.ts +353 -0
- package/src/storage/implementation/common/PersistedBatch.ts +12 -390
- package/src/storage/implementation/common/PreparedPublication.ts +49 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +0 -2
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +339 -27
- package/src/storage/implementation/v1/PersistedBatchV1.ts +74 -32
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +0 -5
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +430 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +13 -4
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +43 -29
- package/src/storage/implementation/v3/PersistedBatchV3.ts +209 -108
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +45 -15
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +9 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +79 -5
- package/test/src/checkpoint_cleanup.test.ts +75 -0
- package/test/src/concurrent_writers.test.ts +127 -53
- package/test/src/helpers/PublicationCommandMonitor.ts +119 -0
- package/test/src/helpers/concurrentWriter.mjs +2 -2
- package/test/src/invalid_sync_config.test.ts +9 -0
- package/test/src/object_storage_lifecycle.test.ts +72 -2
- package/test/src/prepared_publication.test.ts +388 -0
- package/test/src/replication_preparation.test.ts +829 -0
- package/test/src/replication_publication_baseline.test.ts +203 -0
- package/test/src/replication_publication_batching.test.ts +212 -0
- package/test/src/replication_uploads.test.ts +393 -0
- package/test/src/source_table_config.test.ts +4 -4
- package/test/src/storage_object_storage_inline_threshold.test.ts +1 -1
- package/test/src/storage_s3_compaction_lifecycle.test.ts +93 -1
- package/test/src/storage_sync.test.ts +101 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
|
+
import { PerformanceTracer, Span, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
3
|
+
import { compactActive, getTestStorage, test_utils } from '@powersync/service-core-tests';
|
|
4
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
5
|
+
import { MongoSyncBucketStorageV3 } from '../../src/storage/implementation/v3/MongoSyncBucketStorageV3.js';
|
|
6
|
+
import { PersistedBatchV3 } from '../../src/storage/implementation/v3/PersistedBatchV3.js';
|
|
7
|
+
import { env } from './env.js';
|
|
8
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
9
|
+
import { PublicationCommandMonitor } from './helpers/PublicationCommandMonitor.js';
|
|
10
|
+
|
|
11
|
+
const SYNC_CONFIG = `
|
|
12
|
+
config:
|
|
13
|
+
edition: 3
|
|
14
|
+
streams:
|
|
15
|
+
items:
|
|
16
|
+
query: SELECT id, description FROM items
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
describe.each([3, 4])('replication publication baseline v%s', (storageVersion) => {
|
|
20
|
+
test.each([
|
|
21
|
+
{ enabled: false, offloaded: false, compaction: false },
|
|
22
|
+
{ enabled: false, offloaded: false, compaction: true },
|
|
23
|
+
{ enabled: true, offloaded: false, compaction: false },
|
|
24
|
+
{ enabled: true, offloaded: true, compaction: false },
|
|
25
|
+
{ enabled: true, offloaded: false, compaction: true },
|
|
26
|
+
{ enabled: true, offloaded: true, compaction: true }
|
|
27
|
+
])('records wire commands and uploads: %j', async ({ enabled, offloaded, compaction }) => {
|
|
28
|
+
const objectStorage = new MemoryObjectStorage();
|
|
29
|
+
const factoryGen = mongoTestStorageFactoryGenerator({
|
|
30
|
+
url: env.MONGO_TEST_URL,
|
|
31
|
+
isCI: env.CI,
|
|
32
|
+
monitorCommands: true,
|
|
33
|
+
objectStorage: enabled ? objectStorage : undefined,
|
|
34
|
+
inlineThresholdBytes: offloaded ? 0 : 16 * 1024
|
|
35
|
+
});
|
|
36
|
+
await using factory = await factoryGen.factory();
|
|
37
|
+
const stream = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_CONFIG, { storageVersion }));
|
|
38
|
+
const bucketStorage = (await getTestStorage(factory, stream)) as MongoSyncBucketStorageV3;
|
|
39
|
+
const tracer = new PerformanceTracer<'storage' | 'evaluate'>('replication publication baseline');
|
|
40
|
+
await using writer = await bucketStorage.createWriter({ ...test_utils.BATCH_OPTIONS, tracer });
|
|
41
|
+
const table = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
42
|
+
await writer.markAllSnapshotDone('1/1');
|
|
43
|
+
|
|
44
|
+
async function save(description: string) {
|
|
45
|
+
await writer.save({
|
|
46
|
+
sourceTable: table,
|
|
47
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
48
|
+
after: { id: 'A', description: description.repeat(1000) },
|
|
49
|
+
afterReplicaId: test_utils.rid('A')
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Activate the config, reserve IDs and clear errors before measurement.
|
|
54
|
+
// Two versions ensure real compaction can replace a chunk and bucket state.
|
|
55
|
+
await save('a');
|
|
56
|
+
await writer.commit('1/2');
|
|
57
|
+
await save('b');
|
|
58
|
+
await writer.commit('1/3');
|
|
59
|
+
const horizon = (await bucketStorage.getCheckpoint()).checkpoint;
|
|
60
|
+
const definitionId = stream.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
61
|
+
const bucketStates = bucketStorage.db.bucketState(bucketStorage.replicationStreamId);
|
|
62
|
+
const stateBefore = await bucketStates.findOne({ '_id.d': definitionId });
|
|
63
|
+
expect(stateBefore?.bucket_stats.count).toBe(2);
|
|
64
|
+
|
|
65
|
+
using monitor = new PublicationCommandMonitor(factory.db.client);
|
|
66
|
+
const spans: Span[] = [];
|
|
67
|
+
const span = tracer.span.bind(tracer);
|
|
68
|
+
using _traceSpy = vi.spyOn(tracer, 'span').mockImplementation((...args) => {
|
|
69
|
+
const result = span(...args);
|
|
70
|
+
spans.push(result);
|
|
71
|
+
return result;
|
|
72
|
+
});
|
|
73
|
+
let compacted = false;
|
|
74
|
+
let compacting = false;
|
|
75
|
+
async function compactOnce() {
|
|
76
|
+
if (!compaction || compacted) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
compacted = true;
|
|
80
|
+
compacting = true;
|
|
81
|
+
try {
|
|
82
|
+
await compactActive(factory, {
|
|
83
|
+
maxOpId: horizon,
|
|
84
|
+
compactBuckets: [stateBefore!._id.b],
|
|
85
|
+
minBucketChanges: 1,
|
|
86
|
+
minChangeRatio: 0
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
compacting = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const uploads: { path: string; bytes: number; startedAt: number; endedAt: number; transactions: string[] }[] = [];
|
|
94
|
+
const put = objectStorage.put.bind(objectStorage);
|
|
95
|
+
using _putSpy = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
|
|
96
|
+
if (compacting) {
|
|
97
|
+
return put(...args);
|
|
98
|
+
}
|
|
99
|
+
const sample = {
|
|
100
|
+
path: args[0],
|
|
101
|
+
bytes: args[1].byteLength,
|
|
102
|
+
startedAt: performance.now(),
|
|
103
|
+
endedAt: 0,
|
|
104
|
+
transactions: monitor.activeReplicationAttempts.map((attempt) => attempt.key)
|
|
105
|
+
};
|
|
106
|
+
uploads.push(sample);
|
|
107
|
+
// Hold the replication PUT open while real compaction commits. No elapsed
|
|
108
|
+
// time assertion or scheduler race is needed to force the stale snapshot.
|
|
109
|
+
await compactOnce();
|
|
110
|
+
await put(...args);
|
|
111
|
+
sample.endedAt = performance.now();
|
|
112
|
+
});
|
|
113
|
+
const flush = PersistedBatchV3.prototype.publish;
|
|
114
|
+
using _flushSpy = vi.spyOn(PersistedBatchV3.prototype, 'publish').mockImplementation(async function (
|
|
115
|
+
this: PersistedBatchV3,
|
|
116
|
+
...args
|
|
117
|
+
) {
|
|
118
|
+
// Inline batches have no PUT to pause. Compact after lookup/evaluation,
|
|
119
|
+
// immediately before the writer persists its bucket-state delta instead.
|
|
120
|
+
if (!offloaded) {
|
|
121
|
+
await compactOnce();
|
|
122
|
+
}
|
|
123
|
+
return flush.apply(this, args);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await save('c');
|
|
127
|
+
await writer.commit('1/4');
|
|
128
|
+
const summary = monitor.summary();
|
|
129
|
+
const attempts = monitor.replicationAttempts;
|
|
130
|
+
// Compaction during an offloaded PUT now finishes before replication opens
|
|
131
|
+
// its transaction. Inline work still injects compaction after the fence.
|
|
132
|
+
const conflict = compaction && !offloaded;
|
|
133
|
+
expect(compacted).toBe(compaction);
|
|
134
|
+
expect(attempts).toHaveLength(conflict ? 2 : 1);
|
|
135
|
+
expect(attempts.map((attempt) => attempt.outcome)).toEqual(conflict ? ['aborted', 'committed'] : ['committed']);
|
|
136
|
+
expect(monitor.activeReplicationAttempts).toHaveLength(0);
|
|
137
|
+
const bodies = spans.filter((span) => span.name === 'storage:transaction_body');
|
|
138
|
+
expect(bodies).toHaveLength(attempts.length);
|
|
139
|
+
expect(bodies.every((span) => span.endAt >= span.startAt)).toBe(true);
|
|
140
|
+
const transactions = spans.filter((span) => span.name === 'storage:transaction');
|
|
141
|
+
expect(transactions).toHaveLength(1);
|
|
142
|
+
for (const body of bodies) {
|
|
143
|
+
expect(transactions[0].startAt).toBeLessThanOrEqual(body.startAt);
|
|
144
|
+
expect(transactions[0].endAt).toBeGreaterThanOrEqual(body.endAt);
|
|
145
|
+
}
|
|
146
|
+
// Ending a parent span also ends its children. Check containment of work
|
|
147
|
+
// spans to catch a transaction span disposed before its promise settles.
|
|
148
|
+
for (const persist of spans.filter((span) => span.name === 'storage:persist_flush')) {
|
|
149
|
+
expect(bodies.some((body) => body.startAt <= persist.startAt && body.endAt >= persist.endAt)).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
expect(tracer.stack).toHaveLength(0);
|
|
152
|
+
expect(uploads).toHaveLength(offloaded ? 1 : 0);
|
|
153
|
+
// Real compaction can run throughout a replication PUT without extending or
|
|
154
|
+
// invalidating a replication transaction: the fence has not started yet.
|
|
155
|
+
for (const upload of uploads) {
|
|
156
|
+
expect(upload.transactions).toHaveLength(0);
|
|
157
|
+
expect(attempts[0].startedAt).toBeGreaterThanOrEqual(upload.endedAt);
|
|
158
|
+
}
|
|
159
|
+
expect(new Set(uploads.map((upload) => upload.path)).size).toBe(uploads.length);
|
|
160
|
+
expect(summary.replicationAttempts.at(-1)?.commands).toMatchObject({ findAndModify: 1, commitTransaction: 1 });
|
|
161
|
+
if (summary.replicationAttempts.at(-1)?.commands.bulkWrite != null) {
|
|
162
|
+
// The source-size query and grouped source-record read (R = 2) now run
|
|
163
|
+
// outside the transaction, once per preparation. A conflict omits the
|
|
164
|
+
// commit but adds an abort command; replay sends no source reads. The
|
|
165
|
+
// final data group now executes the independent head/checkpoint tail too.
|
|
166
|
+
expect(summary.replicationAttempts.map((attempt) => attempt.commands)).toEqual([
|
|
167
|
+
...(conflict ? [{ findAndModify: 1, bulkWrite: 1, abortTransaction: 1 }] : []),
|
|
168
|
+
{ findAndModify: 1, bulkWrite: 1, commitTransaction: 1 }
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (process.env.POWERSYNC_PUBLICATION_BASELINE) {
|
|
173
|
+
console.info(
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
storageVersion,
|
|
176
|
+
objectStorageEnabled: enabled,
|
|
177
|
+
offloaded,
|
|
178
|
+
compaction,
|
|
179
|
+
...summary,
|
|
180
|
+
uploads: uploads.map((upload) => ({ bytes: upload.bytes, durationMs: upload.endedAt - upload.startedAt })),
|
|
181
|
+
transactionBodyMs: bodies.map((span) => (span.endAt - span.startAt) / 1000)
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Keep verification commands outside the measured summary.
|
|
187
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
188
|
+
expect(checkpoint.lsn).toBe('1/4');
|
|
189
|
+
expect(checkpoint.checkpoint).toBe(horizon + 1n);
|
|
190
|
+
const documents = await bucketStorage.db
|
|
191
|
+
.bucketData(bucketStorage.replicationStreamId, definitionId)
|
|
192
|
+
.find({})
|
|
193
|
+
.toArray();
|
|
194
|
+
const referencedPaths = documents.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
195
|
+
for (const path of referencedPaths) {
|
|
196
|
+
expect(objectStorage.store.has(path)).toBe(true);
|
|
197
|
+
}
|
|
198
|
+
if (offloaded && compaction) {
|
|
199
|
+
expect(referencedPaths).toContain(uploads[0].path);
|
|
200
|
+
expect(objectStorage.store.has(uploads[0].path)).toBe(true);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
3
|
+
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
4
|
+
import { getTestStorage, test_utils } from '@powersync/service-core-tests';
|
|
5
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
6
|
+
import { MongoWriteBatch } from '../../src/storage/implementation/MongoWriteBatch.js';
|
|
7
|
+
import { MongoBucketBatchV3 } from '../../src/storage/implementation/v3/MongoBucketBatchV3.js';
|
|
8
|
+
import { MongoSyncBucketStorageV3 } from '../../src/storage/implementation/v3/MongoSyncBucketStorageV3.js';
|
|
9
|
+
import { PersistedBatchV3 } from '../../src/storage/implementation/v3/PersistedBatchV3.js';
|
|
10
|
+
import { ObjectStorageLifecycle } from '../../src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js';
|
|
11
|
+
import { env } from './env.js';
|
|
12
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
13
|
+
import { PublicationCommandMonitor } from './helpers/PublicationCommandMonitor.js';
|
|
14
|
+
|
|
15
|
+
const CONFIG = `
|
|
16
|
+
config:
|
|
17
|
+
edition: 3
|
|
18
|
+
streams:
|
|
19
|
+
items:
|
|
20
|
+
query: SELECT * FROM items
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
async function save(writer: storage.BucketStorageBatch, table: storage.SourceTable, id: string) {
|
|
24
|
+
await writer.save({
|
|
25
|
+
sourceTable: table,
|
|
26
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
27
|
+
after: { id, description: id },
|
|
28
|
+
afterReplicaId: test_utils.rid(id)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function open(version: number, mode = 'disabled') {
|
|
33
|
+
const objectStorage = new MemoryObjectStorage();
|
|
34
|
+
const factoryGen = mongoTestStorageFactoryGenerator({
|
|
35
|
+
url: env.MONGO_TEST_URL,
|
|
36
|
+
isCI: env.CI,
|
|
37
|
+
monitorCommands: true,
|
|
38
|
+
objectStorage: mode === 'disabled' ? undefined : objectStorage,
|
|
39
|
+
inlineThresholdBytes: mode === 'inline' ? 16 * 1024 : 0
|
|
40
|
+
});
|
|
41
|
+
const factory = await factoryGen.factory();
|
|
42
|
+
const stream = await factory.updateSyncRules(updateSyncRulesFromYaml(CONFIG, { storageVersion: version }));
|
|
43
|
+
const bucketStorage = (await getTestStorage(factory, stream)) as MongoSyncBucketStorageV3;
|
|
44
|
+
const writer = (await bucketStorage.createWriter(test_utils.BATCH_OPTIONS)) as MongoBucketBatchV3;
|
|
45
|
+
const table = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
46
|
+
await writer.markAllSnapshotDone('1/1');
|
|
47
|
+
// Reserve IDs and activate the config before measuring steady-state publication.
|
|
48
|
+
await save(writer, table, 'A');
|
|
49
|
+
await writer.commit('1/1');
|
|
50
|
+
await save(writer, table, 'B');
|
|
51
|
+
await writer.commit('1/2');
|
|
52
|
+
return { factory, factoryGen, stream, bucketStorage, writer, table, objectStorage };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function countStreamReads(client: mongo.MongoClient) {
|
|
56
|
+
let count = 0;
|
|
57
|
+
const observe = (event: mongo.CommandStartedEvent) => {
|
|
58
|
+
if (event.commandName === 'find' && event.command.find === 'sync_rules' && event.command.autocommit !== false) {
|
|
59
|
+
count++;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
client.on('commandStarted', observe);
|
|
63
|
+
return {
|
|
64
|
+
get count() {
|
|
65
|
+
return count;
|
|
66
|
+
},
|
|
67
|
+
[Symbol.dispose]: () => client.off('commandStarted', observe)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function supportsClientBulkWrites(db: mongo.Db): Promise<boolean> {
|
|
72
|
+
// These integration fixtures run a replica set with one server version. Keep
|
|
73
|
+
// wire expectations conditional on that server's capabilities; forcing the
|
|
74
|
+
// client API on MongoDB 6/7 bypasses the production collection fallback.
|
|
75
|
+
const hello = await db.admin().command({ hello: 1 });
|
|
76
|
+
return hello.maxWireVersion >= 25;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe.each([3, 4])('publication batching v%s', (version) => {
|
|
80
|
+
test.each([
|
|
81
|
+
{ mode: 'disabled', fallback: false },
|
|
82
|
+
{ mode: 'inline', fallback: false },
|
|
83
|
+
{ mode: 'offloaded', fallback: false },
|
|
84
|
+
{ mode: 'disabled', fallback: true },
|
|
85
|
+
{ mode: 'inline', fallback: true },
|
|
86
|
+
{ mode: 'offloaded', fallback: true }
|
|
87
|
+
])('measures steady-state wire commands: %j', async ({ mode, fallback }) => {
|
|
88
|
+
const context = await open(version, mode);
|
|
89
|
+
await using factory = context.factory;
|
|
90
|
+
await using writer = context.writer;
|
|
91
|
+
const clientBulks = !fallback && (await supportsClientBulkWrites(factory.db.db));
|
|
92
|
+
const createWriteBatch = factory.db.createWriteBatch.bind(factory.db);
|
|
93
|
+
using _bulk = vi
|
|
94
|
+
.spyOn(factory.db, 'createWriteBatch')
|
|
95
|
+
.mockImplementation((session, options) =>
|
|
96
|
+
fallback ? new MongoWriteBatch(factory.db.client, false, session, options) : createWriteBatch(session, options)
|
|
97
|
+
);
|
|
98
|
+
using monitor = new PublicationCommandMonitor(factory.db.client);
|
|
99
|
+
using reads = countStreamReads(factory.db.client);
|
|
100
|
+
using registration = vi.spyOn(ObjectStorageLifecycle.prototype, 'prepareUploads');
|
|
101
|
+
await save(writer, context.table, 'C');
|
|
102
|
+
await writer.commit('1/3');
|
|
103
|
+
expect(reads.count).toBe(0);
|
|
104
|
+
expect(registration).toHaveBeenCalledTimes(mode === 'offloaded' ? 1 : 0);
|
|
105
|
+
expect(monitor.summary().replicationAttempts.map((attempt) => attempt.commands)).toEqual([
|
|
106
|
+
!clientBulks
|
|
107
|
+
? {
|
|
108
|
+
findAndModify: 1,
|
|
109
|
+
// Offloaded storage also updates the writer's usage document.
|
|
110
|
+
update: mode === 'offloaded' ? 5 : 4,
|
|
111
|
+
insert: 1,
|
|
112
|
+
...(mode === 'offloaded' ? { delete: 1 } : {}),
|
|
113
|
+
commitTransaction: 1
|
|
114
|
+
}
|
|
115
|
+
: { findAndModify: 1, bulkWrite: 1, commitTransaction: 1 }
|
|
116
|
+
]);
|
|
117
|
+
const checkpoint = await context.bucketStorage.getCheckpoint();
|
|
118
|
+
expect(checkpoint).toMatchObject({ checkpoint: 3n, lsn: '1/3' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('shares confirmed heads between writers without retaining writer-local progress', async () => {
|
|
122
|
+
const context = await open(version);
|
|
123
|
+
await using factory = context.factory;
|
|
124
|
+
await using first = context.writer;
|
|
125
|
+
await using second = (await context.bucketStorage.createWriter(test_utils.BATCH_OPTIONS)) as MongoBucketBatchV3;
|
|
126
|
+
using reads = countStreamReads(factory.db.client);
|
|
127
|
+
await save(first, context.table, 'C');
|
|
128
|
+
await first.flush();
|
|
129
|
+
await save(second, context.table, 'D');
|
|
130
|
+
await second.commit('1/3');
|
|
131
|
+
await first.commit('1/4');
|
|
132
|
+
expect(reads.count).toBe(0);
|
|
133
|
+
expect(first.last_flushed_op).toBe(3n);
|
|
134
|
+
expect(second.last_flushed_op).toBe(4n);
|
|
135
|
+
expect(await context.bucketStorage.getCheckpoint()).toMatchObject({ checkpoint: 4n, lsn: '1/4' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('aborted publication leaves progress unchanged and stops the lease', async () => {
|
|
139
|
+
const context = await open(version);
|
|
140
|
+
await using factory = context.factory;
|
|
141
|
+
await using writer = context.writer;
|
|
142
|
+
using reads = countStreamReads(factory.db.client);
|
|
143
|
+
const publish = PersistedBatchV3.prototype.publish;
|
|
144
|
+
using _failure = vi.spyOn(PersistedBatchV3.prototype, 'publish').mockImplementationOnce(async function (
|
|
145
|
+
this: PersistedBatchV3,
|
|
146
|
+
...args
|
|
147
|
+
) {
|
|
148
|
+
await publish.apply(this, args);
|
|
149
|
+
throw new Error('abort combined data and checkpoint');
|
|
150
|
+
});
|
|
151
|
+
await save(writer, context.table, 'C');
|
|
152
|
+
await expect(writer.commit('1/3')).rejects.toThrow('abort combined data and checkpoint');
|
|
153
|
+
expect(reads.count).toBe(0);
|
|
154
|
+
await expect(writer.commit('1/3')).rejects.toThrow('restart replication from persisted progress');
|
|
155
|
+
expect(await context.bucketStorage.getCheckpoint()).toMatchObject({ checkpoint: 2n, lsn: '1/2' });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('validates live configs from the fence', async () => {
|
|
159
|
+
const context = await open(version);
|
|
160
|
+
await using factory = context.factory;
|
|
161
|
+
await using writer = context.writer;
|
|
162
|
+
await factory.db.sync_rules.updateOne(
|
|
163
|
+
{ _id: context.stream.replicationStreamId },
|
|
164
|
+
{ $set: { 'sync_configs.$[].state': storage.SyncRuleState.STOP } },
|
|
165
|
+
{ writeConcern: { w: 'majority' } }
|
|
166
|
+
);
|
|
167
|
+
using reads = countStreamReads(factory.db.client);
|
|
168
|
+
await save(writer, context.table, 'C');
|
|
169
|
+
await expect(writer.flush()).rejects.toThrow('reload');
|
|
170
|
+
expect(reads.count).toBe(0);
|
|
171
|
+
expect(writer.last_flushed_op).toBe(2n);
|
|
172
|
+
await expect(writer.commit('1/3')).rejects.toThrow('restart replication from persisted progress');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('replacement lease publishes from durable progress', async () => {
|
|
176
|
+
const context = await open(version);
|
|
177
|
+
await using factory = context.factory;
|
|
178
|
+
await using previous = context.writer;
|
|
179
|
+
await test_utils.releaseTestStorageLease(factory, context.stream.replicationStreamId);
|
|
180
|
+
const stream = (await factory.getReplicatingReplicationStreams())[0];
|
|
181
|
+
const restarted = await getTestStorage(factory, stream);
|
|
182
|
+
await using next = await restarted.createWriter(test_utils.BATCH_OPTIONS);
|
|
183
|
+
const table = await test_utils.resolveTestTable(next, 'items', ['id'], context.factoryGen, 1);
|
|
184
|
+
using reads = countStreamReads(factory.db.client);
|
|
185
|
+
await save(next, table, 'C');
|
|
186
|
+
await next.commit('1/3');
|
|
187
|
+
expect(reads.count).toBe(0);
|
|
188
|
+
await expect(previous.commit('1/4')).rejects.toThrow('Replication lock released');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('measures checked-bulk result cursors for 1000 unique rows', async () => {
|
|
192
|
+
const context = await open(version, 'offloaded');
|
|
193
|
+
await using factory = context.factory;
|
|
194
|
+
await using writer = context.writer;
|
|
195
|
+
const clientBulks = await supportsClientBulkWrites(factory.db.db);
|
|
196
|
+
using monitor = new PublicationCommandMonitor(factory.db.client);
|
|
197
|
+
using reads = countStreamReads(factory.db.client);
|
|
198
|
+
using put = vi.spyOn(context.objectStorage, 'put');
|
|
199
|
+
for (let i = 0; i < 1000; i++) {
|
|
200
|
+
await save(writer, context.table, `row${i}`);
|
|
201
|
+
}
|
|
202
|
+
await writer.commit('1/3');
|
|
203
|
+
expect(reads.count).toBe(0);
|
|
204
|
+
expect(put).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect(monitor.summary().replicationAttempts.map((attempt) => attempt.commands)).toEqual([
|
|
206
|
+
clientBulks
|
|
207
|
+
? { findAndModify: 1, bulkWrite: 1, commitTransaction: 1 }
|
|
208
|
+
: { findAndModify: 1, update: 5, delete: 1, insert: 1, commitTransaction: 1 }
|
|
209
|
+
]);
|
|
210
|
+
expect((await context.bucketStorage.getCheckpoint()).checkpoint).toBe(1002n);
|
|
211
|
+
});
|
|
212
|
+
});
|