@powersync/service-module-mongodb-storage 0.15.4 → 0.17.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 +69 -0
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -2
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +8 -6
- package/dist/storage/MongoBucketStorage.js +153 -66
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +15 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/CheckpointState.d.ts +20 -0
- package/dist/storage/implementation/CheckpointState.js +31 -0
- package/dist/storage/implementation/CheckpointState.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +48 -35
- package/dist/storage/implementation/MongoBucketBatch.js +118 -379
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
- package/dist/storage/implementation/MongoChecksums.d.ts +29 -17
- package/dist/storage/implementation/MongoChecksums.js +13 -72
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
- package/dist/storage/implementation/MongoCompactor.js +229 -296
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js +67 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +22 -5
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +56 -13
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +61 -32
- package/dist/storage/implementation/MongoSyncBucketStorage.js +85 -523
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncRulesLock.js +19 -13
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
- package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
- package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
- package/dist/storage/implementation/common/PersistedBatch.js +237 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
- package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
- package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +35 -0
- package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
- package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
- package/dist/storage/implementation/db.d.ts +41 -36
- package/dist/storage/implementation/db.js +77 -99
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +79 -66
- package/dist/storage/implementation/models.js +20 -1
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +27 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +407 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +50 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +354 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +25 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
- package/dist/storage/implementation/v1/models.d.ts +45 -0
- package/dist/storage/implementation/v1/models.js +37 -0
- package/dist/storage/implementation/v1/models.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +30 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +463 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +4 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +63 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +508 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +28 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +22 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +74 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +101 -0
- package/dist/storage/implementation/v3/models.js +34 -0
- package/dist/storage/implementation/v3/models.js.map +1 -0
- package/dist/storage/storage-index.d.ts +6 -3
- package/dist/storage/storage-index.js +6 -3
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/util.d.ts +10 -3
- package/dist/utils/util.js +24 -3
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
- package/src/storage/MongoBucketStorage.ts +254 -99
- package/src/storage/implementation/BucketDefinitionMapping.ts +75 -0
- package/src/storage/implementation/CheckpointState.ts +59 -0
- package/src/storage/implementation/MongoBucketBatch.ts +182 -490
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +53 -75
- package/src/storage/implementation/MongoCompactor.ts +374 -404
- package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
- package/src/storage/implementation/MongoPersistedSyncRules.ts +82 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +78 -16
- package/src/storage/implementation/MongoSyncBucketStorage.ts +179 -628
- package/src/storage/implementation/MongoSyncRulesLock.ts +20 -16
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
- package/src/storage/implementation/OperationBatch.ts +1 -1
- package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
- package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
- package/src/storage/implementation/common/PersistedBatch.ts +364 -0
- package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +48 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +110 -131
- package/src/storage/implementation/models.ts +102 -79
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +509 -0
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +543 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +229 -0
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
- package/src/storage/implementation/v1/models.ts +99 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +607 -0
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
- package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +11 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +678 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +317 -0
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +117 -0
- package/src/storage/implementation/v3/models.ts +164 -0
- package/src/storage/storage-index.ts +6 -3
- package/src/utils/util.ts +34 -5
- package/test/src/storage_compacting.test.ts +57 -29
- package/test/src/storage_sync.test.ts +767 -5
- package/test/src/storeCurrentData.test.ts +211 -0
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
- package/dist/storage/implementation/PersistedBatch.js +0 -354
- package/dist/storage/implementation/PersistedBatch.js.map +0 -1
- package/src/storage/implementation/PersistedBatch.ts +0 -432
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import { HydratedSyncConfig, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
|
|
3
3
|
import * as bson from 'bson';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
BaseObserver,
|
|
7
7
|
container,
|
|
8
|
-
logger as defaultLogger,
|
|
9
8
|
ErrorCode,
|
|
10
9
|
errors,
|
|
11
10
|
Logger,
|
|
@@ -14,28 +13,24 @@ import {
|
|
|
14
13
|
} from '@powersync/lib-services-framework';
|
|
15
14
|
import {
|
|
16
15
|
BucketStorageMarkRecordUnavailable,
|
|
17
|
-
CheckpointResult,
|
|
18
16
|
deserializeBson,
|
|
19
17
|
InternalOpId,
|
|
20
18
|
isCompleteRow,
|
|
19
|
+
PerformanceTracer,
|
|
21
20
|
SaveOperationTag,
|
|
22
21
|
storage,
|
|
23
|
-
SyncRuleState,
|
|
24
22
|
utils
|
|
25
23
|
} from '@powersync/service-core';
|
|
26
24
|
import * as timers from 'node:timers/promises';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
25
|
+
import { mongoTableId } from '../../utils/util.js';
|
|
26
|
+
import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
|
|
27
|
+
import { PersistedBatch } from './common/PersistedBatch.js';
|
|
28
|
+
import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
|
|
29
|
+
import type { VersionedPowerSyncMongo } from './db.js';
|
|
30
|
+
import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
|
|
30
31
|
import { MongoIdSequence } from './MongoIdSequence.js';
|
|
31
32
|
import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
|
|
32
|
-
import {
|
|
33
|
-
import { PersistedBatch } from './PersistedBatch.js';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 15MB
|
|
37
|
-
*/
|
|
38
|
-
export const MAX_ROW_SIZE = 15 * 1024 * 1024;
|
|
33
|
+
import { OperationBatch, RecordOperation } from './OperationBatch.js';
|
|
39
34
|
|
|
40
35
|
// Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
|
|
41
36
|
// While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
|
|
@@ -44,49 +39,65 @@ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
|
|
|
44
39
|
// In the future, we can investigate allowing multiple replication streams operating independently.
|
|
45
40
|
const replicationMutex = new utils.Mutex();
|
|
46
41
|
|
|
47
|
-
export const EMPTY_DATA = new bson.Binary(bson.serialize({}));
|
|
48
|
-
|
|
49
42
|
export interface MongoBucketBatchOptions {
|
|
50
43
|
db: VersionedPowerSyncMongo;
|
|
51
|
-
syncRules:
|
|
44
|
+
syncRules: HydratedSyncConfig;
|
|
52
45
|
groupId: number;
|
|
53
46
|
slotName: string;
|
|
47
|
+
syncConfigId?: bson.ObjectId | null;
|
|
54
48
|
lastCheckpointLsn: string | null;
|
|
55
49
|
keepaliveOp: InternalOpId | null;
|
|
56
50
|
resumeFromLsn: string | null;
|
|
57
51
|
storeCurrentData: boolean;
|
|
52
|
+
mapping: BucketDefinitionMapping;
|
|
58
53
|
/**
|
|
59
54
|
* Set to true for initial replication.
|
|
60
55
|
*/
|
|
61
56
|
skipExistingRows: boolean;
|
|
62
57
|
|
|
63
58
|
markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
59
|
+
hooks: storage.StorageHooks | undefined;
|
|
64
60
|
|
|
65
|
-
logger
|
|
61
|
+
logger: Logger;
|
|
62
|
+
tracer?: PerformanceTracer<'storage' | 'evaluate'>;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
export class MongoBucketBatch
|
|
65
|
+
export abstract class MongoBucketBatch
|
|
69
66
|
extends BaseObserver<storage.BucketBatchStorageListener>
|
|
70
67
|
implements storage.BucketStorageBatch
|
|
71
68
|
{
|
|
72
|
-
|
|
69
|
+
protected readonly options: MongoBucketBatchOptions;
|
|
70
|
+
protected logger: Logger;
|
|
73
71
|
|
|
74
72
|
private readonly client: mongo.MongoClient;
|
|
75
73
|
public readonly db: VersionedPowerSyncMongo;
|
|
76
74
|
public readonly session: mongo.ClientSession;
|
|
77
|
-
|
|
75
|
+
protected readonly sync_rules: HydratedSyncConfig;
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
protected readonly group_id: number;
|
|
80
78
|
|
|
81
79
|
private readonly slot_name: string;
|
|
80
|
+
/**
|
|
81
|
+
* Source-level setting for whether raw row data should be stored in current_data.
|
|
82
|
+
*
|
|
83
|
+
* Some sources always send complete rows (MongoDB, MySQL with binlog_row_image=full),
|
|
84
|
+
* in which case this is false for the whole batch. For sources where it depends on the
|
|
85
|
+
* table (Postgres REPLICA IDENTITY), this is true and the decision is refined per-table
|
|
86
|
+
* via SourceTable.storeCurrentData. The effective per-record value is the conjunction of
|
|
87
|
+
* the two.
|
|
88
|
+
*/
|
|
82
89
|
private readonly storeCurrentData: boolean;
|
|
83
|
-
|
|
90
|
+
public readonly skipExistingRows: boolean;
|
|
91
|
+
protected readonly mapping: BucketDefinitionMapping;
|
|
84
92
|
|
|
85
93
|
private batch: OperationBatch | null = null;
|
|
86
94
|
private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
|
|
87
95
|
private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
96
|
+
private hooks: storage.StorageHooks | undefined;
|
|
88
97
|
private clearedError = false;
|
|
89
98
|
|
|
99
|
+
private tracer: PerformanceTracer<'storage' | 'evaluate'>;
|
|
100
|
+
|
|
90
101
|
/**
|
|
91
102
|
* Last LSN received associated with a checkpoint.
|
|
92
103
|
*
|
|
@@ -94,9 +105,9 @@ export class MongoBucketBatch
|
|
|
94
105
|
* 1. A commit LSN.
|
|
95
106
|
* 2. A keepalive message LSN.
|
|
96
107
|
*/
|
|
97
|
-
|
|
108
|
+
protected last_checkpoint_lsn: string | null = null;
|
|
98
109
|
|
|
99
|
-
|
|
110
|
+
protected persisted_op: InternalOpId | null = null;
|
|
100
111
|
|
|
101
112
|
/**
|
|
102
113
|
* Last written op, if any. This may not reflect a consistent checkpoint.
|
|
@@ -115,11 +126,10 @@ export class MongoBucketBatch
|
|
|
115
126
|
*/
|
|
116
127
|
public resumeFromLsn: string | null = null;
|
|
117
128
|
|
|
118
|
-
private needsActivation = true;
|
|
119
|
-
|
|
120
129
|
constructor(options: MongoBucketBatchOptions) {
|
|
121
130
|
super();
|
|
122
|
-
this.logger = options.logger
|
|
131
|
+
this.logger = options.logger;
|
|
132
|
+
this.options = options;
|
|
123
133
|
this.client = options.db.client;
|
|
124
134
|
this.db = options.db;
|
|
125
135
|
this.group_id = options.groupId;
|
|
@@ -129,11 +139,14 @@ export class MongoBucketBatch
|
|
|
129
139
|
this.slot_name = options.slotName;
|
|
130
140
|
this.sync_rules = options.syncRules;
|
|
131
141
|
this.storeCurrentData = options.storeCurrentData;
|
|
142
|
+
this.mapping = options.mapping;
|
|
132
143
|
this.skipExistingRows = options.skipExistingRows;
|
|
133
144
|
this.markRecordUnavailable = options.markRecordUnavailable;
|
|
145
|
+
this.hooks = options.hooks;
|
|
134
146
|
this.batch = new OperationBatch();
|
|
135
147
|
|
|
136
148
|
this.persisted_op = options.keepaliveOp ?? null;
|
|
149
|
+
this.tracer = options.tracer ?? new PerformanceTracer('MongoDB storage');
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
addCustomWriteCheckpoint(checkpoint: storage.BatchedCustomWriteCheckpointOptions): void {
|
|
@@ -147,6 +160,33 @@ export class MongoBucketBatch
|
|
|
147
160
|
return this.last_checkpoint_lsn;
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
abstract resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult>;
|
|
164
|
+
|
|
165
|
+
protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
|
|
166
|
+
|
|
167
|
+
protected abstract get sourceRecordStore(): SourceRecordStore;
|
|
168
|
+
|
|
169
|
+
protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
|
|
170
|
+
|
|
171
|
+
abstract commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult>;
|
|
172
|
+
|
|
173
|
+
abstract keepalive(lsn: string): Promise<storage.CheckpointResult>;
|
|
174
|
+
|
|
175
|
+
abstract setResumeLsn(lsn: string): Promise<void>;
|
|
176
|
+
|
|
177
|
+
abstract getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null>;
|
|
178
|
+
|
|
179
|
+
abstract markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
|
|
180
|
+
|
|
181
|
+
abstract markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
|
|
182
|
+
|
|
183
|
+
abstract markTableSnapshotRequired(table: storage.SourceTable): Promise<void>;
|
|
184
|
+
|
|
185
|
+
abstract markTableSnapshotDone(
|
|
186
|
+
tables: storage.SourceTable[],
|
|
187
|
+
no_checkpoint_before_lsn?: string
|
|
188
|
+
): Promise<storage.SourceTable[]>;
|
|
189
|
+
|
|
150
190
|
async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
|
|
151
191
|
let result: storage.FlushedResult | null = null;
|
|
152
192
|
// One flush may be split over multiple transactions.
|
|
@@ -165,6 +205,10 @@ export class MongoBucketBatch
|
|
|
165
205
|
let last_op: InternalOpId | null = null;
|
|
166
206
|
let resumeBatch: OperationBatch | null = null;
|
|
167
207
|
|
|
208
|
+
using _ = this.tracer.span('storage', 'flush');
|
|
209
|
+
|
|
210
|
+
await this.hooks?.beforeBatchFlush?.(this);
|
|
211
|
+
|
|
168
212
|
await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
|
|
169
213
|
if (batch != null) {
|
|
170
214
|
resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
|
|
@@ -188,6 +232,7 @@ export class MongoBucketBatch
|
|
|
188
232
|
|
|
189
233
|
this.persisted_op = last_op;
|
|
190
234
|
this.last_flushed_op = last_op;
|
|
235
|
+
await this.hooks?.afterBatchFlush?.(this);
|
|
191
236
|
return { flushed_op: last_op };
|
|
192
237
|
}
|
|
193
238
|
|
|
@@ -198,8 +243,13 @@ export class MongoBucketBatch
|
|
|
198
243
|
options?: storage.BucketBatchCommitOptions
|
|
199
244
|
): Promise<OperationBatch | null> {
|
|
200
245
|
let sizes: Map<string, number> | undefined = undefined;
|
|
201
|
-
|
|
202
|
-
|
|
246
|
+
using _ = this.tracer.span('storage', 'replicate_batch');
|
|
247
|
+
// Only look up current_data sizes if the batch stores current_data and at least one
|
|
248
|
+
// table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
|
|
249
|
+
const anyTableStoresCurrentData =
|
|
250
|
+
this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
251
|
+
if (anyTableStoresCurrentData && !this.skipExistingRows) {
|
|
252
|
+
// We skip this step if no tables store current_data, since the sizes will
|
|
203
253
|
// always be small in that case.
|
|
204
254
|
|
|
205
255
|
// With skipExistingRows, we don't load the full documents into memory,
|
|
@@ -212,33 +262,16 @@ export class MongoBucketBatch
|
|
|
212
262
|
// (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
|
|
213
263
|
// the order of processing, which then becomes really tricky to manage.
|
|
214
264
|
// This now takes 2+ queries, but doesn't have any issues with order of operations.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
$match: {
|
|
226
|
-
_id: { $in: sizeLookups }
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
$project: {
|
|
231
|
-
_id: 1,
|
|
232
|
-
size: { $bsonSize: '$$ROOT' }
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
],
|
|
236
|
-
{ session }
|
|
237
|
-
);
|
|
238
|
-
for await (let doc of sizeCursor.stream()) {
|
|
239
|
-
const key = cacheKey(doc._id.t, doc._id.k);
|
|
240
|
-
sizes.set(key, doc.size);
|
|
241
|
-
}
|
|
265
|
+
// Within this branch this.storeCurrentData is true, so the per-table flag is the
|
|
266
|
+
// effective value - only look up sizes for tables that actually store current_data.
|
|
267
|
+
const sizeLookups = batch.batch
|
|
268
|
+
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
269
|
+
.map((r) => ({
|
|
270
|
+
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
271
|
+
replicaId: r.beforeId
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
|
|
242
275
|
}
|
|
243
276
|
|
|
244
277
|
// If set, we need to start a new transaction with this batch.
|
|
@@ -256,64 +289,65 @@ export class MongoBucketBatch
|
|
|
256
289
|
}
|
|
257
290
|
continue;
|
|
258
291
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let persistedBatch: PersistedBatch | null = new PersistedBatch(this.db, this.group_id, transactionSize, {
|
|
276
|
-
logger: this.logger
|
|
277
|
-
});
|
|
278
|
-
|
|
292
|
+
using lookupSpan = this.tracer.span('storage', 'lookup');
|
|
293
|
+
const lookups = b.map((r) => ({
|
|
294
|
+
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
295
|
+
replicaId: r.beforeId
|
|
296
|
+
}));
|
|
297
|
+
let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
|
|
298
|
+
lookupSpan.end();
|
|
299
|
+
|
|
300
|
+
let persistedBatch: PersistedBatch | null = this.createPersistedBatch(transactionSize);
|
|
301
|
+
|
|
302
|
+
// The current code structure makes it tricky to cleanly split this span from the one
|
|
303
|
+
// where fluhsing. So we manually end and re-create this span whenever we flush.
|
|
304
|
+
let evalSpan = this.tracer.span('evaluate');
|
|
279
305
|
for (let op of b) {
|
|
280
306
|
if (resumeBatch) {
|
|
281
307
|
resumeBatch.push(op);
|
|
282
308
|
continue;
|
|
283
309
|
}
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
310
|
+
const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
|
|
311
|
+
if (sourceRecord != null) {
|
|
286
312
|
// If it will be used again later, it will be set again using nextData below
|
|
287
|
-
|
|
313
|
+
sourceRecordLookup.delete(op.internalBeforeKey);
|
|
288
314
|
}
|
|
289
|
-
const nextData = this.saveOperation(persistedBatch!, op,
|
|
315
|
+
const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq);
|
|
290
316
|
if (nextData != null) {
|
|
291
317
|
// Update our current_data and size cache
|
|
292
|
-
|
|
293
|
-
sizes?.set(op.internalAfterKey!, nextData.data
|
|
318
|
+
sourceRecordLookup.set(op.internalAfterKey!, nextData);
|
|
319
|
+
sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
|
|
294
320
|
}
|
|
295
321
|
|
|
296
322
|
if (persistedBatch!.shouldFlushTransaction()) {
|
|
323
|
+
evalSpan.end();
|
|
297
324
|
// Transaction is getting big.
|
|
298
325
|
// Flush, and resume in a new transaction.
|
|
326
|
+
using persistSpan = this.tracer.span('storage', 'persist_flush');
|
|
299
327
|
const { flushedAny } = await persistedBatch!.flush(this.session, options);
|
|
328
|
+
|
|
300
329
|
didFlush ||= flushedAny;
|
|
301
330
|
persistedBatch = null;
|
|
302
331
|
// Computing our current progress is a little tricky here, since
|
|
303
332
|
// we're stopping in the middle of a batch.
|
|
304
333
|
// We create a new batch, and push any remaining operations to it.
|
|
305
334
|
resumeBatch = new OperationBatch();
|
|
335
|
+
persistSpan.end();
|
|
336
|
+
evalSpan = this.tracer.span('evaluate');
|
|
306
337
|
}
|
|
307
338
|
}
|
|
339
|
+
evalSpan.end();
|
|
308
340
|
|
|
309
341
|
if (persistedBatch) {
|
|
310
342
|
transactionSize = persistedBatch.currentSize;
|
|
343
|
+
using _ = this.tracer.span('storage', 'persist_flush');
|
|
311
344
|
const { flushedAny } = await persistedBatch.flush(this.session, options);
|
|
312
345
|
didFlush ||= flushedAny;
|
|
313
346
|
}
|
|
314
347
|
}
|
|
315
348
|
|
|
316
349
|
if (didFlush) {
|
|
350
|
+
using _ = this.tracer.span('storage', 'clear_error');
|
|
317
351
|
await this.clearError();
|
|
318
352
|
}
|
|
319
353
|
|
|
@@ -323,7 +357,7 @@ export class MongoBucketBatch
|
|
|
323
357
|
private saveOperation(
|
|
324
358
|
batch: PersistedBatch,
|
|
325
359
|
operation: RecordOperation,
|
|
326
|
-
|
|
360
|
+
sourceRecord: LoadedSourceRecord | null,
|
|
327
361
|
opSeq: MongoIdSequence
|
|
328
362
|
) {
|
|
329
363
|
const record = operation.record;
|
|
@@ -331,17 +365,20 @@ export class MongoBucketBatch
|
|
|
331
365
|
const afterId = operation.afterId;
|
|
332
366
|
let after = record.after;
|
|
333
367
|
const sourceTable = record.sourceTable;
|
|
368
|
+
// Effective per-record flag: store current_data only if both the batch (source-level,
|
|
369
|
+
// e.g. Postgres) and the table (e.g. non-FULL replica identity) require it.
|
|
370
|
+
const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
|
|
334
371
|
|
|
335
|
-
let existing_buckets:
|
|
336
|
-
let new_buckets:
|
|
337
|
-
let existing_lookups:
|
|
338
|
-
let new_lookups:
|
|
372
|
+
let existing_buckets: LoadedSourceRecord['buckets'] = [];
|
|
373
|
+
let new_buckets: LoadedSourceRecord['buckets'] = [];
|
|
374
|
+
let existing_lookups: LoadedSourceRecord['lookups'] = [];
|
|
375
|
+
let new_lookups: LoadedSourceRecord['lookups'] = [];
|
|
339
376
|
|
|
340
|
-
const
|
|
377
|
+
const sourceTableId = mongoTableId(record.sourceTable.id);
|
|
341
378
|
|
|
342
379
|
if (this.skipExistingRows) {
|
|
343
380
|
if (record.tag == SaveOperationTag.INSERT) {
|
|
344
|
-
if (
|
|
381
|
+
if (sourceRecord != null) {
|
|
345
382
|
// Initial replication, and we already have the record.
|
|
346
383
|
// This may be a different version of the record, but streaming replication
|
|
347
384
|
// will take care of that.
|
|
@@ -354,12 +391,12 @@ export class MongoBucketBatch
|
|
|
354
391
|
}
|
|
355
392
|
|
|
356
393
|
if (record.tag == SaveOperationTag.UPDATE) {
|
|
357
|
-
const result =
|
|
394
|
+
const result = sourceRecord;
|
|
358
395
|
if (result == null) {
|
|
359
396
|
// Not an error if we re-apply a transaction
|
|
360
397
|
existing_buckets = [];
|
|
361
398
|
existing_lookups = [];
|
|
362
|
-
if (!isCompleteRow(
|
|
399
|
+
if (!isCompleteRow(storeCurrentData, after!)) {
|
|
363
400
|
if (this.markRecordUnavailable != null) {
|
|
364
401
|
// This will trigger a "resnapshot" of the record.
|
|
365
402
|
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
@@ -375,18 +412,20 @@ export class MongoBucketBatch
|
|
|
375
412
|
} else {
|
|
376
413
|
existing_buckets = result.buckets;
|
|
377
414
|
existing_lookups = result.lookups;
|
|
378
|
-
if (
|
|
379
|
-
const data = deserializeBson(
|
|
415
|
+
if (storeCurrentData && result.data != null) {
|
|
416
|
+
const data = deserializeBson(result.data.buffer) as SqliteRow;
|
|
380
417
|
after = storage.mergeToast<SqliteValue>(after!, data);
|
|
381
418
|
}
|
|
382
419
|
}
|
|
383
420
|
} else if (record.tag == SaveOperationTag.DELETE) {
|
|
384
|
-
const result =
|
|
421
|
+
const result = sourceRecord;
|
|
385
422
|
if (result == null) {
|
|
386
423
|
// Not an error if we re-apply a transaction
|
|
387
424
|
existing_buckets = [];
|
|
388
425
|
existing_lookups = [];
|
|
389
|
-
// Log to help with debugging if there was a consistency issue
|
|
426
|
+
// Log to help with debugging if there was a consistency issue.
|
|
427
|
+
// Gate on the batch-level flag: FULL tables (per-record flag false) still get a
|
|
428
|
+
// current_data entry, so a missing record on DELETE is meaningful for them too.
|
|
390
429
|
if (this.storeCurrentData && this.markRecordUnavailable == null) {
|
|
391
430
|
this.logger.warn(
|
|
392
431
|
`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
@@ -398,9 +437,9 @@ export class MongoBucketBatch
|
|
|
398
437
|
}
|
|
399
438
|
}
|
|
400
439
|
|
|
401
|
-
let afterData: bson.Binary |
|
|
402
|
-
if (afterId != null && !
|
|
403
|
-
afterData =
|
|
440
|
+
let afterData: bson.Binary | null = null;
|
|
441
|
+
if (afterId != null && !storeCurrentData) {
|
|
442
|
+
afterData = null;
|
|
404
443
|
} else if (afterId != null) {
|
|
405
444
|
try {
|
|
406
445
|
// This will fail immediately if the record is > 16MB.
|
|
@@ -466,13 +505,15 @@ export class MongoBucketBatch
|
|
|
466
505
|
// However, it will be valid by the end of the transaction.
|
|
467
506
|
//
|
|
468
507
|
// In this case, we don't save the op, but we do save the current data.
|
|
469
|
-
if (afterId && after && utils.isCompleteRow(
|
|
508
|
+
if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
|
|
470
509
|
// Insert or update
|
|
471
510
|
if (sourceTable.syncData) {
|
|
472
|
-
const { results
|
|
511
|
+
const { results, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
|
|
473
512
|
record: after,
|
|
474
|
-
sourceTable
|
|
513
|
+
sourceTable: sourceTable.ref,
|
|
514
|
+
bucketDataSources: sourceTable.bucketDataSources
|
|
475
515
|
});
|
|
516
|
+
const evaluated = results;
|
|
476
517
|
|
|
477
518
|
for (let error of syncErrors) {
|
|
478
519
|
container.reporter.captureMessage(
|
|
@@ -498,20 +539,15 @@ export class MongoBucketBatch
|
|
|
498
539
|
table: sourceTable,
|
|
499
540
|
before_buckets: existing_buckets
|
|
500
541
|
});
|
|
501
|
-
new_buckets =
|
|
502
|
-
return {
|
|
503
|
-
bucket: e.bucket,
|
|
504
|
-
table: e.table,
|
|
505
|
-
id: e.id
|
|
506
|
-
};
|
|
507
|
-
});
|
|
542
|
+
new_buckets = this.sourceRecordStore.mapEvaluatedBuckets(evaluated);
|
|
508
543
|
}
|
|
509
544
|
|
|
510
545
|
if (sourceTable.syncParameters) {
|
|
511
546
|
// Parameters
|
|
512
547
|
const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(
|
|
513
|
-
sourceTable,
|
|
514
|
-
after
|
|
548
|
+
sourceTable.ref,
|
|
549
|
+
after,
|
|
550
|
+
{ parameterLookupSources: sourceTable.parameterLookupSources }
|
|
515
551
|
);
|
|
516
552
|
|
|
517
553
|
for (let error of paramErrors) {
|
|
@@ -537,28 +573,29 @@ export class MongoBucketBatch
|
|
|
537
573
|
evaluated: paramEvaluated,
|
|
538
574
|
existing_lookups
|
|
539
575
|
});
|
|
540
|
-
new_lookups =
|
|
541
|
-
return storage.serializeLookup(p.lookup);
|
|
542
|
-
});
|
|
576
|
+
new_lookups = this.sourceRecordStore.mapParameterLookups(paramEvaluated);
|
|
543
577
|
}
|
|
544
578
|
}
|
|
545
579
|
|
|
546
|
-
let result:
|
|
580
|
+
let result: LoadedSourceRecord | null = null;
|
|
547
581
|
|
|
548
582
|
// 5. TOAST: Update current data and bucket list.
|
|
549
583
|
if (afterId) {
|
|
550
584
|
// Insert or update
|
|
551
|
-
|
|
552
|
-
|
|
585
|
+
batch.upsertCurrentData({
|
|
586
|
+
sourceTableId,
|
|
587
|
+
replicaId: afterId,
|
|
553
588
|
data: afterData,
|
|
554
589
|
buckets: new_buckets,
|
|
555
590
|
lookups: new_lookups
|
|
556
591
|
});
|
|
557
592
|
result = {
|
|
558
|
-
|
|
559
|
-
|
|
593
|
+
sourceTableId,
|
|
594
|
+
replicaId: afterId,
|
|
595
|
+
data: afterData,
|
|
560
596
|
buckets: new_buckets,
|
|
561
|
-
lookups: new_lookups
|
|
597
|
+
lookups: new_lookups,
|
|
598
|
+
cacheKey: operation.internalAfterKey!
|
|
562
599
|
};
|
|
563
600
|
}
|
|
564
601
|
|
|
@@ -567,13 +604,15 @@ export class MongoBucketBatch
|
|
|
567
604
|
// Note that this is a soft delete.
|
|
568
605
|
// We don't specifically need a new or unique op_id here, but it must be greater than the
|
|
569
606
|
// last checkpoint, so we use next().
|
|
570
|
-
batch.softDeleteCurrentData(
|
|
607
|
+
batch.softDeleteCurrentData(sourceTableId, beforeId, opSeq.next());
|
|
571
608
|
}
|
|
572
609
|
return result;
|
|
573
610
|
}
|
|
574
611
|
|
|
575
|
-
|
|
612
|
+
protected async withTransaction(cb: () => Promise<void>) {
|
|
613
|
+
using lockSpan = this.tracer.span('storage', 'internal_lock');
|
|
576
614
|
await replicationMutex.exclusiveLock(async () => {
|
|
615
|
+
lockSpan.end();
|
|
577
616
|
await this.session.withTransaction(
|
|
578
617
|
async () => {
|
|
579
618
|
try {
|
|
@@ -584,7 +623,9 @@ export class MongoBucketBatch
|
|
|
584
623
|
} else {
|
|
585
624
|
this.logger.warn('Transaction error', e as Error);
|
|
586
625
|
}
|
|
587
|
-
|
|
626
|
+
const delay = Math.random() * 50;
|
|
627
|
+
using _ = this.tracer.span('storage', 'retry_delay');
|
|
628
|
+
await timers.setTimeout(delay);
|
|
588
629
|
throw e;
|
|
589
630
|
}
|
|
590
631
|
},
|
|
@@ -678,271 +719,9 @@ export class MongoBucketBatch
|
|
|
678
719
|
await this[Symbol.asyncDispose]();
|
|
679
720
|
}
|
|
680
721
|
|
|
681
|
-
private lastWaitingLogThottled = 0;
|
|
682
|
-
|
|
683
|
-
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<CheckpointResult> {
|
|
684
|
-
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
685
|
-
|
|
686
|
-
await this.flush(options);
|
|
687
|
-
|
|
688
|
-
const now = new Date();
|
|
689
|
-
|
|
690
|
-
// Mark relevant write checkpoints as "processed".
|
|
691
|
-
// This makes it easier to identify write checkpoints that are "valid" in order.
|
|
692
|
-
await this.db.write_checkpoints.updateMany(
|
|
693
|
-
{
|
|
694
|
-
processed_at_lsn: null,
|
|
695
|
-
'lsns.1': { $lte: lsn }
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
$set: {
|
|
699
|
-
processed_at_lsn: lsn
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
session: this.session
|
|
704
|
-
}
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
const can_checkpoint = {
|
|
708
|
-
$and: [
|
|
709
|
-
{ $eq: ['$snapshot_done', true] },
|
|
710
|
-
{
|
|
711
|
-
$or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
$or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
|
|
715
|
-
}
|
|
716
|
-
]
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
const new_keepalive_op = {
|
|
720
|
-
$cond: [
|
|
721
|
-
can_checkpoint,
|
|
722
|
-
{ $literal: null },
|
|
723
|
-
{
|
|
724
|
-
$toString: {
|
|
725
|
-
$max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
]
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
const new_last_checkpoint = {
|
|
732
|
-
$cond: [
|
|
733
|
-
can_checkpoint,
|
|
734
|
-
{
|
|
735
|
-
$max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
|
|
736
|
-
},
|
|
737
|
-
'$last_checkpoint'
|
|
738
|
-
]
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
// For this query, we need to handle multiple cases, depending on the state:
|
|
742
|
-
// 1. Normal commit - advance last_checkpoint to this.persisted_op.
|
|
743
|
-
// 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
|
|
744
|
-
// 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
|
|
745
|
-
// We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
|
|
746
|
-
let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate(
|
|
747
|
-
{ _id: this.group_id },
|
|
748
|
-
[
|
|
749
|
-
{
|
|
750
|
-
$set: {
|
|
751
|
-
_can_checkpoint: can_checkpoint,
|
|
752
|
-
_not_empty: createEmptyCheckpoints
|
|
753
|
-
? true
|
|
754
|
-
: {
|
|
755
|
-
$or: [
|
|
756
|
-
{ $literal: createEmptyCheckpoints },
|
|
757
|
-
{ $ne: ['$keepalive_op', new_keepalive_op] },
|
|
758
|
-
{ $ne: ['$last_checkpoint', new_last_checkpoint] }
|
|
759
|
-
]
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
$set: {
|
|
765
|
-
last_checkpoint_lsn: {
|
|
766
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
|
|
767
|
-
},
|
|
768
|
-
last_checkpoint_ts: {
|
|
769
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
|
|
770
|
-
},
|
|
771
|
-
last_keepalive_ts: { $literal: now },
|
|
772
|
-
last_fatal_error: { $literal: null },
|
|
773
|
-
last_fatal_error_ts: { $literal: null },
|
|
774
|
-
keepalive_op: new_keepalive_op,
|
|
775
|
-
last_checkpoint: new_last_checkpoint,
|
|
776
|
-
// Unset snapshot_lsn on checkpoint
|
|
777
|
-
snapshot_lsn: {
|
|
778
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
},
|
|
782
|
-
{
|
|
783
|
-
$unset: ['_can_checkpoint', '_not_empty']
|
|
784
|
-
}
|
|
785
|
-
],
|
|
786
|
-
{
|
|
787
|
-
session: this.session,
|
|
788
|
-
// We return the before document, so that we can check the previous state to determine if a checkpoint was actually created or if we were blocked by snapshot/no_checkpoint_before.
|
|
789
|
-
returnDocument: 'before',
|
|
790
|
-
projection: {
|
|
791
|
-
snapshot_done: 1,
|
|
792
|
-
last_checkpoint_lsn: 1,
|
|
793
|
-
no_checkpoint_before: 1,
|
|
794
|
-
keepalive_op: 1,
|
|
795
|
-
last_checkpoint: 1
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
);
|
|
799
|
-
|
|
800
|
-
if (preUpdateDocument == null) {
|
|
801
|
-
throw new ReplicationAssertionError(
|
|
802
|
-
'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// This re-implements the same logic as in the pipeline, to determine what was actually updated.
|
|
807
|
-
// Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
|
|
808
|
-
const canCheckpoint =
|
|
809
|
-
preUpdateDocument.snapshot_done === true &&
|
|
810
|
-
(preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
|
|
811
|
-
(preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
|
|
812
|
-
|
|
813
|
-
const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
|
|
814
|
-
const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
|
|
815
|
-
const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
|
|
816
|
-
const newLastCheckpoint = canCheckpoint
|
|
817
|
-
? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) =>
|
|
818
|
-
a > b ? a : b
|
|
819
|
-
)
|
|
820
|
-
: preUpdateDocument.last_checkpoint;
|
|
821
|
-
const notEmpty =
|
|
822
|
-
createEmptyCheckpoints ||
|
|
823
|
-
preUpdateDocument.keepalive_op !== newKeepaliveOp ||
|
|
824
|
-
preUpdateDocument.last_checkpoint !== newLastCheckpoint;
|
|
825
|
-
const checkpointCreated = canCheckpoint && notEmpty;
|
|
826
|
-
|
|
827
|
-
const checkpointBlocked = !canCheckpoint;
|
|
828
|
-
|
|
829
|
-
if (checkpointBlocked) {
|
|
830
|
-
// Failed on snapshot_done or no_checkpoint_before.
|
|
831
|
-
if (Date.now() - this.lastWaitingLogThottled > 5_000) {
|
|
832
|
-
this.logger.info(
|
|
833
|
-
`Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify(
|
|
834
|
-
{
|
|
835
|
-
snapshot_done: preUpdateDocument.snapshot_done,
|
|
836
|
-
last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
|
|
837
|
-
no_checkpoint_before: preUpdateDocument.no_checkpoint_before
|
|
838
|
-
}
|
|
839
|
-
)}`
|
|
840
|
-
);
|
|
841
|
-
this.lastWaitingLogThottled = Date.now();
|
|
842
|
-
}
|
|
843
|
-
} else {
|
|
844
|
-
if (checkpointCreated) {
|
|
845
|
-
this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
|
|
846
|
-
}
|
|
847
|
-
await this.autoActivate(lsn);
|
|
848
|
-
await this.db.notifyCheckpoint();
|
|
849
|
-
this.persisted_op = null;
|
|
850
|
-
this.last_checkpoint_lsn = lsn;
|
|
851
|
-
if (this.db.storageConfig.softDeleteCurrentData && newLastCheckpoint != null) {
|
|
852
|
-
await this.cleanupCurrentData(newLastCheckpoint);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return { checkpointBlocked, checkpointCreated };
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
private async cleanupCurrentData(lastCheckpoint: bigint) {
|
|
859
|
-
const result = await this.db.v3_current_data.deleteMany({
|
|
860
|
-
'_id.g': this.group_id,
|
|
861
|
-
pending_delete: { $exists: true, $lte: lastCheckpoint }
|
|
862
|
-
});
|
|
863
|
-
if (result.deletedCount > 0) {
|
|
864
|
-
this.logger.info(
|
|
865
|
-
`Cleaned up ${result.deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Switch from processing -> active if relevant.
|
|
872
|
-
*
|
|
873
|
-
* Called on new commits.
|
|
874
|
-
*/
|
|
875
|
-
private async autoActivate(lsn: string) {
|
|
876
|
-
if (!this.needsActivation) {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Activate the batch, so it can start processing.
|
|
881
|
-
// This is done automatically when the first save() is called.
|
|
882
|
-
|
|
883
|
-
const session = this.session;
|
|
884
|
-
let activated = false;
|
|
885
|
-
await session.withTransaction(async () => {
|
|
886
|
-
const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
|
|
887
|
-
if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
|
|
888
|
-
await this.db.sync_rules.updateOne(
|
|
889
|
-
{
|
|
890
|
-
_id: this.group_id
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
$set: {
|
|
894
|
-
state: storage.SyncRuleState.ACTIVE
|
|
895
|
-
}
|
|
896
|
-
},
|
|
897
|
-
{ session }
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
await this.db.sync_rules.updateMany(
|
|
901
|
-
{
|
|
902
|
-
_id: { $ne: this.group_id },
|
|
903
|
-
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
904
|
-
},
|
|
905
|
-
{
|
|
906
|
-
$set: {
|
|
907
|
-
state: storage.SyncRuleState.STOP
|
|
908
|
-
}
|
|
909
|
-
},
|
|
910
|
-
{ session }
|
|
911
|
-
);
|
|
912
|
-
activated = true;
|
|
913
|
-
} else if (doc?.state != SyncRuleState.PROCESSING) {
|
|
914
|
-
this.needsActivation = false;
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
if (activated) {
|
|
918
|
-
this.logger.info(`Activated new sync rules at ${lsn}`);
|
|
919
|
-
await this.db.notifyCheckpoint();
|
|
920
|
-
this.needsActivation = false;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async keepalive(lsn: string): Promise<CheckpointResult> {
|
|
925
|
-
return await this.commit(lsn, { createEmptyCheckpoints: true });
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
async setResumeLsn(lsn: string): Promise<void> {
|
|
929
|
-
const update: Partial<SyncRuleDocument> = {
|
|
930
|
-
snapshot_lsn: lsn
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
await this.db.sync_rules.updateOne(
|
|
934
|
-
{
|
|
935
|
-
_id: this.group_id
|
|
936
|
-
},
|
|
937
|
-
{
|
|
938
|
-
$set: update
|
|
939
|
-
},
|
|
940
|
-
{ session: this.session }
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
722
|
async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
|
|
945
723
|
const { after, before, sourceTable, tag } = record;
|
|
724
|
+
const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
|
|
946
725
|
for (const event of this.getTableEvents(sourceTable)) {
|
|
947
726
|
this.iterateListeners((cb) =>
|
|
948
727
|
cb.replicationEvent?.({
|
|
@@ -950,8 +729,8 @@ export class MongoBucketBatch
|
|
|
950
729
|
table: sourceTable,
|
|
951
730
|
data: {
|
|
952
731
|
op: tag,
|
|
953
|
-
after: after && utils.isCompleteRow(
|
|
954
|
-
before: before && utils.isCompleteRow(
|
|
732
|
+
after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
|
|
733
|
+
before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
|
|
955
734
|
},
|
|
956
735
|
event
|
|
957
736
|
})
|
|
@@ -988,9 +767,11 @@ export class MongoBucketBatch
|
|
|
988
767
|
|
|
989
768
|
await this.withTransaction(async () => {
|
|
990
769
|
for (let table of sourceTables) {
|
|
991
|
-
await this.db.
|
|
770
|
+
await this.db.commonSourceTables(this.group_id).deleteOne({ _id: mongoTableId(table.id) });
|
|
992
771
|
}
|
|
993
772
|
});
|
|
773
|
+
|
|
774
|
+
await this.cleanupDroppedSourceTables(sourceTables);
|
|
994
775
|
return result;
|
|
995
776
|
}
|
|
996
777
|
|
|
@@ -1022,24 +803,10 @@ export class MongoBucketBatch
|
|
|
1022
803
|
let lastBatchCount = BATCH_LIMIT;
|
|
1023
804
|
while (lastBatchCount == BATCH_LIMIT) {
|
|
1024
805
|
await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
pending_delete: { $exists: false }
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
const cursor = this.db.common_current_data.find(current_data_filter, {
|
|
1033
|
-
projection: {
|
|
1034
|
-
_id: 1,
|
|
1035
|
-
buckets: 1,
|
|
1036
|
-
lookups: 1
|
|
1037
|
-
},
|
|
1038
|
-
limit: BATCH_LIMIT,
|
|
1039
|
-
session: session
|
|
1040
|
-
});
|
|
1041
|
-
const batch = await cursor.toArray();
|
|
1042
|
-
const persistedBatch = new PersistedBatch(this.db, this.group_id, 0, { logger: this.logger });
|
|
806
|
+
using evalSpan = this.tracer.span('evaluate');
|
|
807
|
+
const sourceTableId = mongoTableId(sourceTable.id);
|
|
808
|
+
const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
|
|
809
|
+
const persistedBatch = this.createPersistedBatch(0);
|
|
1043
810
|
|
|
1044
811
|
for (let value of batch) {
|
|
1045
812
|
persistedBatch.saveBucketData({
|
|
@@ -1047,19 +814,22 @@ export class MongoBucketBatch
|
|
|
1047
814
|
before_buckets: value.buckets,
|
|
1048
815
|
evaluated: [],
|
|
1049
816
|
table: sourceTable,
|
|
1050
|
-
sourceKey: value.
|
|
817
|
+
sourceKey: value.replicaId
|
|
1051
818
|
});
|
|
1052
819
|
persistedBatch.saveParameterData({
|
|
1053
820
|
op_seq: opSeq,
|
|
1054
821
|
existing_lookups: value.lookups,
|
|
1055
822
|
evaluated: [],
|
|
1056
823
|
sourceTable: sourceTable,
|
|
1057
|
-
sourceKey: value.
|
|
824
|
+
sourceKey: value.replicaId
|
|
1058
825
|
});
|
|
1059
826
|
|
|
1060
827
|
// Since this is not from streaming replication, we can do a hard delete
|
|
1061
|
-
persistedBatch.hardDeleteCurrentData(value.
|
|
828
|
+
persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
|
|
1062
829
|
}
|
|
830
|
+
evalSpan.end();
|
|
831
|
+
|
|
832
|
+
using _ = this.tracer.span('storage', 'persist_flush');
|
|
1063
833
|
await persistedBatch.flush(session);
|
|
1064
834
|
lastBatchCount = batch.length;
|
|
1065
835
|
|
|
@@ -1083,7 +853,7 @@ export class MongoBucketBatch
|
|
|
1083
853
|
copy.snapshotStatus = snapshotStatus;
|
|
1084
854
|
|
|
1085
855
|
await this.withTransaction(async () => {
|
|
1086
|
-
await this.db.
|
|
856
|
+
await this.db.commonSourceTables(this.group_id).updateOne(
|
|
1087
857
|
{ _id: mongoTableId(table.id) },
|
|
1088
858
|
{
|
|
1089
859
|
$set: {
|
|
@@ -1101,80 +871,6 @@ export class MongoBucketBatch
|
|
|
1101
871
|
return copy;
|
|
1102
872
|
}
|
|
1103
873
|
|
|
1104
|
-
async markAllSnapshotDone(no_checkpoint_before_lsn: string) {
|
|
1105
|
-
await this.db.sync_rules.updateOne(
|
|
1106
|
-
{
|
|
1107
|
-
_id: this.group_id
|
|
1108
|
-
},
|
|
1109
|
-
{
|
|
1110
|
-
$set: {
|
|
1111
|
-
snapshot_done: true,
|
|
1112
|
-
last_keepalive_ts: new Date()
|
|
1113
|
-
},
|
|
1114
|
-
$max: {
|
|
1115
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
1116
|
-
}
|
|
1117
|
-
},
|
|
1118
|
-
{ session: this.session }
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
|
|
1123
|
-
await this.db.sync_rules.updateOne(
|
|
1124
|
-
{
|
|
1125
|
-
_id: this.group_id
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
$set: {
|
|
1129
|
-
snapshot_done: false
|
|
1130
|
-
}
|
|
1131
|
-
},
|
|
1132
|
-
{ session: this.session }
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
async markTableSnapshotDone(tables: storage.SourceTable[], no_checkpoint_before_lsn?: string) {
|
|
1137
|
-
const session = this.session;
|
|
1138
|
-
const ids = tables.map((table) => mongoTableId(table.id));
|
|
1139
|
-
|
|
1140
|
-
await this.withTransaction(async () => {
|
|
1141
|
-
await this.db.source_tables.updateMany(
|
|
1142
|
-
{ _id: { $in: ids } },
|
|
1143
|
-
{
|
|
1144
|
-
$set: {
|
|
1145
|
-
snapshot_done: true
|
|
1146
|
-
},
|
|
1147
|
-
$unset: {
|
|
1148
|
-
snapshot_status: 1
|
|
1149
|
-
}
|
|
1150
|
-
},
|
|
1151
|
-
{ session }
|
|
1152
|
-
);
|
|
1153
|
-
|
|
1154
|
-
if (no_checkpoint_before_lsn != null) {
|
|
1155
|
-
await this.db.sync_rules.updateOne(
|
|
1156
|
-
{
|
|
1157
|
-
_id: this.group_id
|
|
1158
|
-
},
|
|
1159
|
-
{
|
|
1160
|
-
$set: {
|
|
1161
|
-
last_keepalive_ts: new Date()
|
|
1162
|
-
},
|
|
1163
|
-
$max: {
|
|
1164
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
1165
|
-
}
|
|
1166
|
-
},
|
|
1167
|
-
{ session: this.session }
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
});
|
|
1171
|
-
return tables.map((table) => {
|
|
1172
|
-
const copy = table.clone();
|
|
1173
|
-
copy.snapshotComplete = true;
|
|
1174
|
-
return copy;
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
874
|
protected async clearError(): Promise<void> {
|
|
1179
875
|
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
1180
876
|
if (this.clearedError) {
|
|
@@ -1200,11 +896,7 @@ export class MongoBucketBatch
|
|
|
1200
896
|
*/
|
|
1201
897
|
protected getTableEvents(table: storage.SourceTable): SqlEventDescriptor[] {
|
|
1202
898
|
return this.sync_rules.eventDescriptors.filter((evt) =>
|
|
1203
|
-
[...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table))
|
|
899
|
+
[...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
|
|
1204
900
|
);
|
|
1205
901
|
}
|
|
1206
902
|
}
|
|
1207
|
-
|
|
1208
|
-
export function currentBucketKey(b: CurrentBucket) {
|
|
1209
|
-
return `${b.bucket}/${b.table}/${b.id}`;
|
|
1210
|
-
}
|