@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
|
@@ -560,37 +560,51 @@ export async function getParameterSetsV3(
|
|
|
560
560
|
};
|
|
561
561
|
};
|
|
562
562
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
563
|
+
// Every lookup after the first adds a $unionWith stage, and MongoDB rejects
|
|
564
|
+
// pipelines with more than 1000 stages, so evaluate lookups in batches.
|
|
565
|
+
const MAX_LOOKUPS_PER_PIPELINE = 900;
|
|
566
|
+
const rows: { index: number; bucket_parameters: SqliteJsonRow }[] = [];
|
|
567
|
+
|
|
568
|
+
for (let start = 0; start < lookups.length; start += MAX_LOOKUPS_PER_PIPELINE) {
|
|
569
|
+
const remaining = limit + 1 - rows.length;
|
|
570
|
+
if (remaining <= 0) {
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
568
573
|
|
|
569
|
-
|
|
570
|
-
...
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
574
|
+
const batch = lookups.slice(start, start + MAX_LOOKUPS_PER_PIPELINE);
|
|
575
|
+
const [firstLookup, ...remainingLookups] = batch;
|
|
576
|
+
const firstQuery = buildLookupPipeline(firstLookup, start);
|
|
577
|
+
|
|
578
|
+
const pipeline: mongo.Document[] = [
|
|
579
|
+
...firstQuery.pipeline,
|
|
580
|
+
...remainingLookups.map((lookup, indexInRemaining) => {
|
|
581
|
+
const query = buildLookupPipeline(lookup, start + indexInRemaining + 1);
|
|
582
|
+
return {
|
|
583
|
+
$unionWith: {
|
|
584
|
+
coll: query.collection.collectionName,
|
|
585
|
+
pipeline: query.pipeline
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}),
|
|
589
|
+
{ $unwind: '$bucket_parameters' },
|
|
590
|
+
{ $limit: remaining }
|
|
591
|
+
];
|
|
583
592
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
593
|
+
const batchRows = await firstQuery.collection
|
|
594
|
+
.aggregate<{ index: number; bucket_parameters: SqliteJsonRow }>(pipeline, {
|
|
595
|
+
session,
|
|
596
|
+
readConcern: 'snapshot',
|
|
597
|
+
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
598
|
+
})
|
|
599
|
+
.toArray()
|
|
600
|
+
.catch((e) => {
|
|
601
|
+
throw lib_mongo.mapQueryError(e, 'while evaluating parameter queries');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
for (const row of batchRows) {
|
|
605
|
+
rows.push(row);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
594
608
|
|
|
595
609
|
if (rows.length > limit) {
|
|
596
610
|
throw new ParameterSetLimitExceededError(limit);
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { BucketDefinitionMapping, InternalOpId, storage } from '@powersync/service-core';
|
|
4
|
-
import {
|
|
4
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
5
|
import * as bson from 'bson';
|
|
6
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
7
|
+
import { BatchOperationBuffer } from '../common/BatchOperationBuffer.js';
|
|
7
8
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
8
9
|
import {
|
|
9
10
|
BucketStateUpdate,
|
|
10
11
|
PersistedBatch,
|
|
11
12
|
PersistedBatchOptions,
|
|
13
|
+
SaveBucketDataOptions,
|
|
12
14
|
SaveParameterDataOptions,
|
|
13
15
|
UpsertCurrentDataOptions
|
|
14
16
|
} from '../common/PersistedBatch.js';
|
|
17
|
+
import { PreparedPublication } from '../common/PreparedPublication.js';
|
|
15
18
|
import { SourceRecordLookupState } from '../common/SourceRecordStore.js';
|
|
16
19
|
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
17
20
|
import { serializeBucketData } from './bucket-format.js';
|
|
@@ -25,55 +28,165 @@ import {
|
|
|
25
28
|
SourceTableDocumentV3,
|
|
26
29
|
taggedBucketParameterDocumentToTagged
|
|
27
30
|
} from './models.js';
|
|
28
|
-
import {
|
|
31
|
+
import { ObjectStorage } from './object-storage/ObjectStorage.js';
|
|
32
|
+
import { ObjectStorageLifecycle, PreparedObjectStorageUpload } from './object-storage/ObjectStorageLifecycle.js';
|
|
29
33
|
import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
30
34
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
31
35
|
|
|
36
|
+
export const DEFAULT_INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
37
|
+
|
|
38
|
+
export interface PersistedBatchV3Options extends PersistedBatchOptions {
|
|
39
|
+
objectStorage?: ObjectStorage;
|
|
40
|
+
inlineThresholdBytes?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Aborts in-flight object storage uploads when replication stops.
|
|
43
|
+
*/
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
objectStorageUsageWriterId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
interface SourceRecordWrite {
|
|
33
49
|
sourceTableId: bson.ObjectId;
|
|
34
50
|
operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3>;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
export class PersistedBatchV3
|
|
53
|
+
export class PersistedBatchV3 implements PersistedBatch, PreparedPublication {
|
|
38
54
|
// Upserts and soft deletes supply the complete source-record state, including
|
|
39
55
|
// pending_delete. Keep the final state per key, without removing any history.
|
|
40
|
-
currentData = new Map<string, SourceRecordWrite>();
|
|
41
|
-
sourceTablePendingDeletes = new Map<string, InternalOpId>();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
private readonly currentData = new Map<string, SourceRecordWrite>();
|
|
57
|
+
private readonly sourceTablePendingDeletes = new Map<string, InternalOpId>();
|
|
58
|
+
private readonly objectStorageLifecycle?: ObjectStorageLifecycle;
|
|
59
|
+
private readonly objectStorageUsage?: ObjectStorageUsage;
|
|
60
|
+
|
|
61
|
+
private readonly operations: BatchOperationBuffer;
|
|
62
|
+
private readonly inlineThresholdBytes: number;
|
|
63
|
+
private readonly signal?: AbortSignal;
|
|
64
|
+
private sealed = false;
|
|
65
|
+
private preparation?: Promise<void>;
|
|
66
|
+
private payloadsReady = false;
|
|
46
67
|
|
|
47
68
|
constructor(
|
|
48
|
-
db: VersionedPowerSyncMongoV3,
|
|
49
|
-
group_id: number,
|
|
50
|
-
mapping: BucketDefinitionMapping,
|
|
69
|
+
private readonly db: VersionedPowerSyncMongoV3,
|
|
70
|
+
private readonly group_id: number,
|
|
71
|
+
private readonly mapping: BucketDefinitionMapping,
|
|
51
72
|
writtenSize: number,
|
|
52
|
-
options?:
|
|
73
|
+
options?: PersistedBatchV3Options
|
|
53
74
|
) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
this.operations = new BatchOperationBuffer(
|
|
76
|
+
group_id,
|
|
77
|
+
writtenSize,
|
|
78
|
+
{
|
|
79
|
+
// V3-specific definition validation; v1 does not persist definition ids.
|
|
80
|
+
checkDefinitionId: (definitionId) => {
|
|
81
|
+
if (definitionId == null) {
|
|
82
|
+
throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
|
|
83
|
+
}
|
|
84
|
+
return definitionId;
|
|
85
|
+
},
|
|
86
|
+
getBucketDefinitionId: (source) => mapping.bucketSourceId(source)
|
|
87
|
+
},
|
|
88
|
+
options?.logger
|
|
89
|
+
);
|
|
90
|
+
this.inlineThresholdBytes = options?.inlineThresholdBytes ?? DEFAULT_INLINE_THRESHOLD_BYTES;
|
|
91
|
+
this.signal = options?.signal;
|
|
92
|
+
if (options?.objectStorage) {
|
|
93
|
+
this.objectStorageLifecycle = new ObjectStorageLifecycle(db, group_id, options.objectStorage);
|
|
94
|
+
this.objectStorageUsage = new ObjectStorageUsage(db, group_id, options.objectStorageUsageWriterId);
|
|
58
95
|
}
|
|
59
96
|
}
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
get currentSize(): number {
|
|
99
|
+
return this.operations.currentSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
saveBucketData(options: SaveBucketDataOptions): void {
|
|
103
|
+
this.assertMutable();
|
|
104
|
+
this.operations.saveBucketData(options);
|
|
105
|
+
}
|
|
62
106
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
shouldFlushTransaction(): boolean {
|
|
108
|
+
return this.operations.shouldFlushTransaction(this.currentData.size);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
append(row: PersistedBatchV3): void {
|
|
112
|
+
this.assertMutable();
|
|
113
|
+
this.operations.append(row.operations);
|
|
114
|
+
for (const [key, value] of row.currentData) {
|
|
115
|
+
this.currentData.set(key, value);
|
|
116
|
+
}
|
|
117
|
+
for (const [key, value] of row.sourceTablePendingDeletes) {
|
|
118
|
+
const previous = this.sourceTablePendingDeletes.get(key) ?? 0n;
|
|
119
|
+
this.sourceTablePendingDeletes.set(key, value > previous ? value : previous);
|
|
66
120
|
}
|
|
67
|
-
return definitionId;
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
|
|
123
|
+
/** Return the sealed view of this batch without starting I/O or moving its data. */
|
|
124
|
+
seal(): PreparedPublication {
|
|
125
|
+
this.assertMutable();
|
|
126
|
+
this.sealed = true;
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private assertMutable(): void {
|
|
131
|
+
if (this.sealed) {
|
|
132
|
+
throw new Error('Cannot modify a sealed publication');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Cache readiness, including failure; the v3 batch owns the entire lifecycle. */
|
|
137
|
+
preparePayloads(): Promise<void> {
|
|
138
|
+
if (!this.sealed) {
|
|
139
|
+
throw new ReplicationAssertionError('Cannot prepare an unsealed publication');
|
|
140
|
+
}
|
|
141
|
+
return (this.preparation ??= Promise.resolve()
|
|
142
|
+
.then(() => this.preparePayloadsImpl())
|
|
143
|
+
.then(() => {
|
|
144
|
+
this.payloadsReady = true;
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async publish(
|
|
149
|
+
session: mongo.ClientSession,
|
|
150
|
+
options?: storage.BucketBatchCommitOptions,
|
|
151
|
+
additionalWrites?: MongoWriteBatch
|
|
152
|
+
) {
|
|
153
|
+
// Never start preparation from a transaction attempt. The eager coordinator
|
|
154
|
+
// awaits readiness for the entire bounded prefix before opening a transaction.
|
|
155
|
+
if (!this.payloadsReady) {
|
|
156
|
+
throw new ReplicationAssertionError('Prepare publication payloads before starting the transaction');
|
|
157
|
+
}
|
|
158
|
+
return this.write(session, options, additionalWrites);
|
|
159
|
+
}
|
|
71
160
|
|
|
72
|
-
|
|
73
|
-
|
|
161
|
+
private async write(
|
|
162
|
+
session: mongo.ClientSession,
|
|
163
|
+
options?: storage.BucketBatchCommitOptions,
|
|
164
|
+
additionalWrites?: MongoWriteBatch
|
|
165
|
+
) {
|
|
166
|
+
const startAt = performance.now();
|
|
167
|
+
const writes = additionalWrites ?? this.db.createWriteBatch(session, { ordered: false });
|
|
168
|
+
// Checked marker removal, references and usage must commit together. Recheck
|
|
169
|
+
// expiry on every attempt; neither an expired nor a missing marker is a
|
|
170
|
+
// transient MongoDB error that may replay this generation indefinitely.
|
|
171
|
+
this.objectStorageLifecycle?.publishUploads(this.preparedUploads, writes);
|
|
172
|
+
if (this.operations.bucketDataCount > 0) {
|
|
173
|
+
await this.queueBucketData(writes);
|
|
174
|
+
}
|
|
175
|
+
if (this.operations.bucketParameters.length > 0) {
|
|
176
|
+
this.queueBucketParameters(writes);
|
|
177
|
+
}
|
|
178
|
+
if (this.currentData.size > 0) {
|
|
179
|
+
this.queueCurrentData(writes);
|
|
180
|
+
}
|
|
181
|
+
if (this.operations.bucketStates.size > 0) {
|
|
182
|
+
this.queueBucketStates(writes);
|
|
183
|
+
}
|
|
184
|
+
await writes.execute();
|
|
185
|
+
return this.operations.logFlush(startAt, this.currentData.size, options);
|
|
74
186
|
}
|
|
75
187
|
|
|
76
188
|
saveParameterData(data: SaveParameterDataOptions) {
|
|
189
|
+
this.assertMutable();
|
|
77
190
|
const { sourceTable, sourceKey, evaluated } = data;
|
|
78
191
|
const remaining_lookups = new Map<string, SourceRecordLookupState>();
|
|
79
192
|
|
|
@@ -95,7 +208,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
95
208
|
remaining_lookups.delete(`${sourceDefinitionId}.${binLookup.toString('base64')}`);
|
|
96
209
|
|
|
97
210
|
const op_id = data.op_seq.next();
|
|
98
|
-
this.debugLastOpId = op_id;
|
|
211
|
+
this.operations.debugLastOpId = op_id;
|
|
99
212
|
const values = {
|
|
100
213
|
_id: op_id,
|
|
101
214
|
key: {
|
|
@@ -105,17 +218,17 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
105
218
|
lookup: binLookup,
|
|
106
219
|
bucket_parameters: result.bucketParameters
|
|
107
220
|
};
|
|
108
|
-
this.bucketParameters.push({
|
|
221
|
+
this.operations.bucketParameters.push({
|
|
109
222
|
...values,
|
|
110
223
|
index: sourceDefinitionId
|
|
111
224
|
});
|
|
112
225
|
|
|
113
|
-
this.currentSize += 200;
|
|
226
|
+
this.operations.currentSize += 200;
|
|
114
227
|
}
|
|
115
228
|
|
|
116
229
|
for (let lookup of remaining_lookups.values()) {
|
|
117
230
|
const op_id = data.op_seq.next();
|
|
118
|
-
this.debugLastOpId = op_id;
|
|
231
|
+
this.operations.debugLastOpId = op_id;
|
|
119
232
|
const indexId = lookup.indexId;
|
|
120
233
|
if (indexId == null) {
|
|
121
234
|
throw new ReplicationAssertionError('Expected lookup when incrementalReprocessing is enabled');
|
|
@@ -129,16 +242,17 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
129
242
|
lookup: lookup.lookup,
|
|
130
243
|
bucket_parameters: []
|
|
131
244
|
};
|
|
132
|
-
this.bucketParameters.push({
|
|
245
|
+
this.operations.bucketParameters.push({
|
|
133
246
|
...values,
|
|
134
247
|
index: indexId
|
|
135
248
|
});
|
|
136
249
|
|
|
137
|
-
this.currentSize += 200;
|
|
250
|
+
this.operations.currentSize += 200;
|
|
138
251
|
}
|
|
139
252
|
}
|
|
140
253
|
|
|
141
254
|
hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
|
|
255
|
+
this.assertMutable();
|
|
142
256
|
this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
|
|
143
257
|
sourceTableId,
|
|
144
258
|
operation: {
|
|
@@ -147,7 +261,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
147
261
|
}
|
|
148
262
|
}
|
|
149
263
|
});
|
|
150
|
-
this.currentSize += 50;
|
|
264
|
+
this.operations.currentSize += 50;
|
|
151
265
|
}
|
|
152
266
|
|
|
153
267
|
softDeleteCurrentData(
|
|
@@ -155,6 +269,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
155
269
|
replicaId: storage.ReplicaId,
|
|
156
270
|
checkpointGreaterThan: InternalOpId
|
|
157
271
|
) {
|
|
272
|
+
this.assertMutable();
|
|
158
273
|
this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
|
|
159
274
|
sourceTableId,
|
|
160
275
|
operation: {
|
|
@@ -178,10 +293,11 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
178
293
|
this.sourceTablePendingDeletes.set(sourceTableKey, checkpointGreaterThan);
|
|
179
294
|
}
|
|
180
295
|
|
|
181
|
-
this.currentSize += 50;
|
|
296
|
+
this.operations.currentSize += 50;
|
|
182
297
|
}
|
|
183
298
|
|
|
184
299
|
upsertCurrentData(values: UpsertCurrentDataOptions) {
|
|
300
|
+
this.assertMutable();
|
|
185
301
|
const buckets = values.buckets.map((bucket) => {
|
|
186
302
|
if (bucket.definitionId == null) {
|
|
187
303
|
throw new ReplicationAssertionError('Expected bucket when incrementalReprocessing is enabled');
|
|
@@ -220,103 +336,93 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
220
336
|
}
|
|
221
337
|
}
|
|
222
338
|
});
|
|
223
|
-
this.currentSize += (values.data?.length() ?? 0) + 100;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
protected get currentDataCount() {
|
|
227
|
-
return this.currentData.size;
|
|
339
|
+
this.operations.currentSize += (values.data?.length() ?? 0) + 100;
|
|
228
340
|
}
|
|
229
341
|
|
|
230
|
-
|
|
342
|
+
private preparedBucketWrites?: { definitionId: BucketDefinitionId; documents: BucketDataDocumentV3[] }[];
|
|
343
|
+
private readonly preparedUsageDeltas = new Map<BucketDefinitionId, bigint>();
|
|
344
|
+
private preparedUploads: PreparedObjectStorageUpload[] = [];
|
|
231
345
|
|
|
232
|
-
|
|
346
|
+
private async preparePayloadsImpl(): Promise<void> {
|
|
233
347
|
const operationsByDefinition = new Map<BucketDefinitionId, BucketDataDoc[]>();
|
|
234
|
-
for (const document of this.bucketData) {
|
|
348
|
+
for (const document of this.operations.bucketData) {
|
|
235
349
|
const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
|
|
236
350
|
existing.push(document);
|
|
237
351
|
operationsByDefinition.set(document.bucketKey.definitionId, existing);
|
|
238
352
|
}
|
|
239
353
|
|
|
240
|
-
const
|
|
354
|
+
const uploads: { path: string; payload: Uint8Array }[] = [];
|
|
355
|
+
this.preparedBucketWrites = Array.from(operationsByDefinition, ([definitionId, documents]) => {
|
|
241
356
|
const operationsByBucket = Map.groupBy(documents, (document) => document.bucketKey.bucket);
|
|
242
357
|
const lifecycle = this.objectStorageLifecycle;
|
|
243
|
-
|
|
244
|
-
const createInserts: (() => Promise<BucketDataInsert>)[] = [];
|
|
358
|
+
const preparedDocuments: BucketDataDocumentV3[] = [];
|
|
245
359
|
|
|
246
360
|
for (const [bucket, ops] of operationsByBucket) {
|
|
247
|
-
this.resetBucketPersistedBytes(definitionId, bucket);
|
|
361
|
+
this.operations.resetBucketPersistedBytes(definitionId, bucket);
|
|
248
362
|
for (const chunk of chunkBucketData(ops)) {
|
|
249
363
|
const serialized = serializeBucketData(bucket, chunk);
|
|
250
|
-
this.incrementBucketPersistedChunk(definitionId, bucket, serialized.size);
|
|
364
|
+
this.operations.incrementBucketPersistedChunk(definitionId, bucket, serialized.size);
|
|
251
365
|
if (lifecycle == null || serialized.size <= this.inlineThresholdBytes) {
|
|
252
|
-
|
|
253
|
-
insertOne: {
|
|
254
|
-
document: serialized
|
|
255
|
-
}
|
|
256
|
-
}));
|
|
366
|
+
preparedDocuments.push(serialized);
|
|
257
367
|
continue;
|
|
258
368
|
}
|
|
259
369
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
storage_ref: {
|
|
271
|
-
path,
|
|
272
|
-
file_size: fileSize
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
};
|
|
370
|
+
const minOp = chunk[0].o;
|
|
371
|
+
const maxOp = chunk[chunk.length - 1].o;
|
|
372
|
+
const { ops: bucketOps, ...metadata } = serialized;
|
|
373
|
+
const path = lifecycle.allocatePath(definitionId, bucket, minOp, maxOp);
|
|
374
|
+
// Seal paths and bytes before starting any upload. Publication reads
|
|
375
|
+
// only the resulting metadata and never regenerates an object.
|
|
376
|
+
const payload = bson.serialize({ ops: bucketOps! });
|
|
377
|
+
preparedDocuments.push({
|
|
378
|
+
...metadata,
|
|
379
|
+
storage_ref: { path, file_size: payload.byteLength }
|
|
277
380
|
});
|
|
381
|
+
uploads.push({ path, payload });
|
|
278
382
|
}
|
|
279
383
|
}
|
|
280
384
|
|
|
281
|
-
|
|
385
|
+
const delta = preparedDocuments.reduce((sum, document) => sum + ObjectStorageUsage.bytes(document), 0n);
|
|
386
|
+
if (delta !== 0n) {
|
|
387
|
+
this.preparedUsageDeltas.set(definitionId, delta);
|
|
388
|
+
}
|
|
389
|
+
return { definitionId, documents: preparedDocuments };
|
|
282
390
|
});
|
|
283
391
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
392
|
+
const lifecycle = this.objectStorageLifecycle;
|
|
393
|
+
if (uploads.length === 0 || lifecycle == null) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.signal?.throwIfAborted();
|
|
397
|
+
// Seal every path/byte first, then register the whole group durably. No PUT
|
|
398
|
+
// may start after a partial or ambiguous registration failure.
|
|
399
|
+
this.preparedUploads = await lifecycle.prepareUploads(
|
|
400
|
+
uploads.map((upload) => upload.path),
|
|
401
|
+
new Date()
|
|
402
|
+
);
|
|
403
|
+
this.signal?.throwIfAborted();
|
|
404
|
+
await lifecycle.storeUploads(
|
|
405
|
+
uploads.map(({ payload }, index) => ({ upload: this.preparedUploads[index], payload })),
|
|
406
|
+
{ signal: this.signal }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
291
409
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const bucketWrites = await createAllInserts();
|
|
296
|
-
|
|
297
|
-
const usageDeltas = this.objectStorageUsage == null ? undefined : new Map<BucketDefinitionId, bigint>();
|
|
298
|
-
for (const { definitionId, inserts } of bucketWrites) {
|
|
299
|
-
if (usageDeltas != null) {
|
|
300
|
-
const delta = inserts.reduce(
|
|
301
|
-
(sum, operation) => sum + ObjectStorageUsage.bytes(operation.insertOne.document),
|
|
302
|
-
0n
|
|
303
|
-
);
|
|
304
|
-
if (delta !== 0n) {
|
|
305
|
-
usageDeltas.set(definitionId, delta);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (inserts.length > 0) {
|
|
309
|
-
writes.bulkWriteUnordered(this.db.bucketData(this.group_id, definitionId), inserts);
|
|
310
|
-
}
|
|
410
|
+
private async queueBucketData(writes: MongoWriteBatch) {
|
|
411
|
+
if (this.preparedBucketWrites == null) {
|
|
412
|
+
throw new ReplicationAssertionError('Publication payloads have not been prepared');
|
|
311
413
|
}
|
|
312
|
-
|
|
313
|
-
|
|
414
|
+
for (const { definitionId, documents } of this.preparedBucketWrites) {
|
|
415
|
+
writes.bulkWriteUnordered(
|
|
416
|
+
this.db.bucketData(this.group_id, definitionId),
|
|
417
|
+
documents.map((document) => ({ insertOne: { document } }))
|
|
418
|
+
);
|
|
314
419
|
}
|
|
420
|
+
this.objectStorageUsage?.applyDeltas(this.preparedUsageDeltas, writes);
|
|
315
421
|
}
|
|
316
422
|
|
|
317
|
-
|
|
318
|
-
const operationsByIndex = new Map<string, typeof this.bucketParameters>();
|
|
319
|
-
for (const document of this.bucketParameters) {
|
|
423
|
+
private queueBucketParameters(writes: MongoWriteBatch): void {
|
|
424
|
+
const operationsByIndex = new Map<string, typeof this.operations.bucketParameters>();
|
|
425
|
+
for (const document of this.operations.bucketParameters) {
|
|
320
426
|
const existing = operationsByIndex.get(document.index) ?? [];
|
|
321
427
|
existing.push(document);
|
|
322
428
|
operationsByIndex.set(document.index, existing);
|
|
@@ -334,7 +440,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
334
440
|
}
|
|
335
441
|
}
|
|
336
442
|
|
|
337
|
-
|
|
443
|
+
private queueCurrentData(writes: MongoWriteBatch): void {
|
|
338
444
|
const operationsBySourceTable = new Map<string, SourceRecordWrite[]>();
|
|
339
445
|
for (const operation of this.currentData.values()) {
|
|
340
446
|
const sourceTableId = operation.sourceTableId.toHexString();
|
|
@@ -371,21 +477,16 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
371
477
|
}
|
|
372
478
|
}
|
|
373
479
|
|
|
374
|
-
|
|
480
|
+
private queueBucketStates(writes: MongoWriteBatch): void {
|
|
375
481
|
writes.bulkWriteUnordered(this.db.bucketState(this.group_id), this.getBucketStateUpdates());
|
|
376
482
|
}
|
|
377
483
|
|
|
378
|
-
protected resetCurrentData() {
|
|
379
|
-
this.currentData.clear();
|
|
380
|
-
this.sourceTablePendingDeletes.clear();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
484
|
private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
|
|
384
485
|
return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
|
|
385
486
|
}
|
|
386
487
|
|
|
387
488
|
private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[] {
|
|
388
|
-
return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
|
|
489
|
+
return Array.from(this.operations.bucketStates.values()).map((state: BucketStateUpdate) => {
|
|
389
490
|
if (state.definitionId == null) {
|
|
390
491
|
throw new ReplicationAssertionError('Expected bucket definition id when incrementalReprocessing is enabled');
|
|
391
492
|
}
|