@powersync/service-module-postgres-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214
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 +179 -6
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.d.ts +3 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js +42 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js.map +1 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.d.ts +3 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js +22 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js.map +1 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.d.ts +3 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.js +12 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.js.map +1 -0
- package/dist/{@types/storage → storage}/PostgresBucketStorageFactory.d.ts +14 -12
- package/dist/storage/PostgresBucketStorageFactory.js +73 -62
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/{@types/storage → storage}/PostgresCompactor.d.ts +3 -1
- package/dist/storage/PostgresCompactor.js +21 -1
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresReportStorage.js.map +1 -1
- package/dist/storage/PostgresStorageProvider.js +3 -1
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/{@types/storage → storage}/PostgresSyncRulesStorage.d.ts +39 -13
- package/dist/storage/PostgresSyncRulesStorage.js +266 -181
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/{@types/storage → storage}/batch/PostgresBucketBatch.d.ts +10 -26
- package/dist/storage/batch/PostgresBucketBatch.js +290 -286
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/{@types/storage → storage}/checkpoints/PostgresWriteCheckpointAPI.d.ts +2 -2
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +163 -26
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
- package/dist/{@types/storage → storage}/current-data-store.d.ts +8 -2
- package/dist/storage/current-data-store.js +66 -11
- package/dist/storage/current-data-store.js.map +1 -1
- package/dist/{@types/storage → storage}/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.d.ts +17 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js +94 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -0
- package/dist/{@types/types → types}/models/ActiveCheckpoint.d.ts +2 -2
- package/dist/types/models/ActiveCheckpoint.js +3 -2
- package/dist/types/models/ActiveCheckpoint.js.map +1 -1
- package/dist/{@types/types → types}/models/ActiveCheckpointNotification.d.ts +2 -2
- package/dist/types/models/BucketData.d.ts +22 -0
- package/dist/types/models/BucketData.js +7 -6
- package/dist/types/models/BucketData.js.map +1 -1
- package/dist/{@types/types → types}/models/CurrentData.d.ts +1 -1
- package/dist/types/models/CurrentData.js +2 -1
- package/dist/types/models/CurrentData.js.map +1 -1
- package/dist/{@types/types → types}/models/SdkReporting.d.ts +3 -3
- package/dist/types/models/SdkReporting.js +3 -5
- package/dist/types/models/SdkReporting.js.map +1 -1
- package/dist/{@types/types → types}/models/SourceTable.d.ts +13 -8
- package/dist/types/models/SourceTable.js +11 -6
- package/dist/types/models/SourceTable.js.map +1 -1
- package/dist/{@types/types → types}/models/SyncRules.d.ts +11 -10
- package/dist/types/models/SyncRules.js +12 -11
- package/dist/types/models/SyncRules.js.map +1 -1
- package/dist/{@types/types → types}/models/WriteCheckpoint.d.ts +2 -0
- package/dist/types/models/WriteCheckpoint.js +6 -2
- package/dist/types/models/WriteCheckpoint.js.map +1 -1
- package/dist/{@types/types → types}/types.d.ts +2 -0
- package/dist/utils/checkpoints.d.ts +9 -0
- package/dist/utils/checkpoints.js +26 -0
- package/dist/utils/checkpoints.js.map +1 -0
- package/dist/utils/test-utils.js +2 -2
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +10 -22
- package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
- package/src/migrations/scripts/1784900000000-source-metadata.ts +31 -0
- package/src/migrations/scripts/1788000000000-sync-config-version-label.ts +18 -0
- package/src/storage/PostgresBucketStorageFactory.ts +82 -65
- package/src/storage/PostgresCompactor.ts +25 -1
- package/src/storage/PostgresReportStorage.ts +1 -2
- package/src/storage/PostgresStorageProvider.ts +3 -1
- package/src/storage/PostgresSyncRulesStorage.ts +295 -196
- package/src/storage/batch/PostgresBucketBatch.ts +321 -310
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +176 -26
- package/src/storage/current-data-store.ts +66 -11
- package/src/storage/storage-index.ts +1 -0
- package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +105 -0
- package/src/types/models/ActiveCheckpoint.ts +3 -2
- package/src/types/models/BucketData.ts +7 -6
- package/src/types/models/CurrentData.ts +2 -1
- package/src/types/models/SdkReporting.ts +4 -4
- package/src/types/models/SourceTable.ts +12 -6
- package/src/types/models/SyncRules.ts +12 -11
- package/src/types/models/WriteCheckpoint.ts +6 -2
- package/src/utils/checkpoints.ts +31 -0
- package/src/utils/test-utils.ts +2 -2
- package/test/src/__snapshots__/storage_sync.test.ts.snap +44 -1621
- package/test/src/checkpoint_notifications.test.ts +528 -0
- package/test/src/migrations.test.ts +1 -1
- package/test/src/storage.test.ts +235 -11
- package/test/src/storage_compacting.test.ts +17 -9
- package/test/src/storage_sync.test.ts +55 -8
- package/test/src/util.ts +1 -1
- package/test/tsconfig.json +1 -3
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +0 -9
- package/dist/@types/types/models/BucketData.d.ts +0 -22
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +0 -49
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +0 -1
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +0 -54
- /package/dist/{@types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{@types/migrations → migrations}/PostgresMigrationAgent.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/PostgresMigrationStore.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/migration-utils.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1684951997326-init.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1749024804042-snapshot-progress.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1756282360128-connection-reporting.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771232439485-storage-version.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771424826685-current-data-pending-deletes.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771491856000-sync-plan.d.ts +0 -0
- /package/dist/{@types/module → module}/PostgresStorageModule.d.ts +0 -0
- /package/dist/{@types/storage → storage}/PostgresReportStorage.d.ts +0 -0
- /package/dist/{@types/storage → storage}/PostgresStorageProvider.d.ts +0 -0
- /package/dist/{@types/storage → storage}/batch/OperationBatch.d.ts +0 -0
- /package/dist/{@types/storage → storage}/batch/PostgresPersistedBatch.d.ts +0 -0
- /package/dist/{@types/storage → storage}/table-id.d.ts +0 -0
- /package/dist/{@types/types → types}/codecs.d.ts +0 -0
- /package/dist/{@types/types → types}/models/BucketParameters.d.ts +0 -0
- /package/dist/{@types/types → types}/models/Instance.d.ts +0 -0
- /package/dist/{@types/types → types}/models/Migration.d.ts +0 -0
- /package/dist/{@types/types → types}/models/json.d.ts +0 -0
- /package/dist/{@types/types → types}/models/models-index.d.ts +0 -0
- /package/dist/{@types/utils → utils}/application-name.d.ts +0 -0
- /package/dist/{@types/utils → utils}/bson.d.ts +0 -0
- /package/dist/{@types/utils → utils}/bucket-data.d.ts +0 -0
- /package/dist/{@types/utils → utils}/db.d.ts +0 -0
- /package/dist/{@types/utils → utils}/test-utils.d.ts +0 -0
- /package/dist/{@types/utils → utils}/ts-codec.d.ts +0 -0
- /package/dist/{@types/utils → utils}/utils-index.d.ts +0 -0
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
BucketChecksum,
|
|
5
5
|
CHECKPOINT_INVALIDATE_ALL,
|
|
6
6
|
CheckpointChanges,
|
|
7
|
+
CompactInitialReplicationOptions,
|
|
8
|
+
CompactInitialReplicationResults,
|
|
7
9
|
GetCheckpointChangesOptions,
|
|
8
10
|
InternalOpId,
|
|
9
11
|
internalToExternalOpId,
|
|
@@ -11,11 +13,10 @@ import {
|
|
|
11
13
|
maxLsn,
|
|
12
14
|
ParameterSetLimitExceededError,
|
|
13
15
|
PartialChecksum,
|
|
14
|
-
PopulateChecksumCacheOptions,
|
|
15
|
-
PopulateChecksumCacheResults,
|
|
16
16
|
ReplicationCheckpoint,
|
|
17
17
|
storage,
|
|
18
18
|
StorageVersionConfig,
|
|
19
|
+
SyncRuleState,
|
|
19
20
|
utils,
|
|
20
21
|
WatchWriteCheckpointOptions
|
|
21
22
|
} from '@powersync/service-core';
|
|
@@ -28,9 +29,11 @@ import { replicaIdToSubkey } from '../utils/bson.js';
|
|
|
28
29
|
import { mapOpEntry } from '../utils/bucket-data.js';
|
|
29
30
|
|
|
30
31
|
import * as framework from '@powersync/lib-services-framework';
|
|
31
|
-
import {
|
|
32
|
+
import type { Statement } from '@powersync/service-jpgwire';
|
|
32
33
|
import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
|
|
33
34
|
import * as t from 'ts-codec';
|
|
35
|
+
import { ActiveCheckpointDecoded } from '../types/models/ActiveCheckpoint.js';
|
|
36
|
+
import * as checkpointUtils from '../utils/checkpoints.js';
|
|
34
37
|
import { pick } from '../utils/ts-codec.js';
|
|
35
38
|
import { PostgresBucketBatch } from './batch/PostgresBucketBatch.js';
|
|
36
39
|
import { PostgresWriteCheckpointAPI } from './checkpoints/PostgresWriteCheckpointAPI.js';
|
|
@@ -41,20 +44,27 @@ import { PostgresCompactor } from './PostgresCompactor.js';
|
|
|
41
44
|
export type PostgresSyncRulesStorageOptions = {
|
|
42
45
|
factory: PostgresBucketStorageFactory;
|
|
43
46
|
db: lib_postgres.DatabaseClient;
|
|
44
|
-
|
|
47
|
+
replicationStream: storage.PersistedReplicationStream;
|
|
45
48
|
write_checkpoint_mode?: storage.WriteCheckpointMode;
|
|
46
49
|
batchLimits: RequiredOperationBatchLimits;
|
|
50
|
+
checksumCacheTtlMs?: number;
|
|
47
51
|
};
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Number of rows deleted per batch when clearing storage for a replication stream.
|
|
55
|
+
*/
|
|
56
|
+
export const CLEAR_BATCH_LIMIT = 50_000;
|
|
57
|
+
|
|
49
58
|
export class PostgresSyncRulesStorage
|
|
50
59
|
extends framework.BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
51
60
|
implements storage.SyncRulesBucketStorage
|
|
52
61
|
{
|
|
53
62
|
[framework.DO_NOT_LOG] = true;
|
|
54
63
|
|
|
55
|
-
public readonly
|
|
56
|
-
public readonly
|
|
57
|
-
public readonly
|
|
64
|
+
public readonly replicationStreamId: number;
|
|
65
|
+
public readonly replicationStream: storage.PersistedReplicationStream;
|
|
66
|
+
public readonly sync_rules: storage.PersistedSyncConfigContent;
|
|
67
|
+
public readonly replicationStreamName: string;
|
|
58
68
|
public readonly factory: PostgresBucketStorageFactory;
|
|
59
69
|
public readonly storageConfig: StorageVersionConfig;
|
|
60
70
|
public readonly logger: framework.Logger;
|
|
@@ -65,22 +75,27 @@ export class PostgresSyncRulesStorage
|
|
|
65
75
|
protected writeCheckpointAPI: PostgresWriteCheckpointAPI;
|
|
66
76
|
private readonly currentDataStore: PostgresCurrentDataStore;
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Canonical parsed sync config sets, keyed by defaultSchema. Entries are never evicted,
|
|
80
|
+
* so each parse options value maps to exactly one parsed set for the lifetime of this
|
|
81
|
+
* storage instance.
|
|
82
|
+
*
|
|
83
|
+
* TODO we might be able to share this in an abstract class
|
|
84
|
+
*/
|
|
85
|
+
private readonly parsedSyncConfigSets = new Map<string, storage.ParsedSyncConfigSet>();
|
|
72
86
|
private _checksumCache: storage.ChecksumCache | undefined;
|
|
73
87
|
|
|
74
88
|
constructor(protected options: PostgresSyncRulesStorageOptions) {
|
|
75
89
|
super();
|
|
76
|
-
this.
|
|
90
|
+
this.replicationStream = options.replicationStream;
|
|
91
|
+
this.replicationStreamId = options.replicationStream.replicationStreamId;
|
|
77
92
|
this.db = options.db;
|
|
78
|
-
this.sync_rules = options.
|
|
79
|
-
this.
|
|
93
|
+
this.sync_rules = options.replicationStream.syncConfigContent[0];
|
|
94
|
+
this.replicationStreamName = options.replicationStream.replicationStreamName;
|
|
80
95
|
this.factory = options.factory;
|
|
81
|
-
this.storageConfig = options.
|
|
96
|
+
this.storageConfig = options.replicationStream.getStorageConfig();
|
|
82
97
|
this.currentDataStore = new PostgresCurrentDataStore(this.storageConfig);
|
|
83
|
-
this.logger =
|
|
98
|
+
this.logger = this.sync_rules.logger;
|
|
84
99
|
|
|
85
100
|
this.writeCheckpointAPI = new PostgresWriteCheckpointAPI({
|
|
86
101
|
db: this.db,
|
|
@@ -95,6 +110,7 @@ export class PostgresSyncRulesStorage
|
|
|
95
110
|
*/
|
|
96
111
|
private get checksumCache(): storage.ChecksumCache {
|
|
97
112
|
this._checksumCache ??= new storage.ChecksumCache({
|
|
113
|
+
ttlMs: this.options.checksumCacheTtlMs,
|
|
98
114
|
fetchChecksums: (batch) => {
|
|
99
115
|
return this.getChecksumsInternal(batch);
|
|
100
116
|
}
|
|
@@ -106,18 +122,17 @@ export class PostgresSyncRulesStorage
|
|
|
106
122
|
return this.writeCheckpointAPI.writeCheckpointMode;
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* Parse sync config if the options are different or if there is no cached value.
|
|
115
|
-
*/
|
|
116
|
-
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
117
|
-
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncConfig(), options };
|
|
125
|
+
getParsedSyncConfigSet(options: storage.ParseSyncConfigOptions): storage.ParsedSyncConfigSet {
|
|
126
|
+
let parsed = this.parsedSyncConfigSets.get(options.defaultSchema);
|
|
127
|
+
if (parsed == null) {
|
|
128
|
+
parsed = this.replicationStream.parsed(options);
|
|
129
|
+
this.parsedSyncConfigSets.set(options.defaultSchema, parsed);
|
|
118
130
|
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
134
|
+
getParsedSyncRules(options: storage.ParseSyncConfigOptions): sync_rules.HydratedSyncConfig {
|
|
135
|
+
return this.getParsedSyncConfigSet(options).hydratedSyncConfig;
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
async reportError(e: any): Promise<void> {
|
|
@@ -127,11 +142,23 @@ export class PostgresSyncRulesStorage
|
|
|
127
142
|
SET
|
|
128
143
|
last_fatal_error = ${{ type: 'varchar', value: message }}
|
|
129
144
|
WHERE
|
|
130
|
-
id = ${{ type: 'int4', value: this.
|
|
145
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }};
|
|
131
146
|
`.execute();
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
supportsIncrementalParameterCompaction(): boolean {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
async compact(options?: storage.CompactOptions): Promise<void> {
|
|
154
|
+
if (options?.incrementalOnly) {
|
|
155
|
+
// Not supported yet
|
|
156
|
+
this.logger.info('Incremental compacting is not supported on Postgres storage yet.');
|
|
157
|
+
return;
|
|
158
|
+
} else if (this.replicationStream.state != SyncRuleState.ACTIVE) {
|
|
159
|
+
this.logger.info(`Skipping compacting of replication stream in ${this.replicationStream.state} state.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
135
162
|
let maxOpId = options?.maxOpId;
|
|
136
163
|
if (maxOpId == null) {
|
|
137
164
|
const checkpoint = await this.getCheckpoint();
|
|
@@ -139,14 +166,16 @@ export class PostgresSyncRulesStorage
|
|
|
139
166
|
maxOpId = checkpoint.checkpoint;
|
|
140
167
|
}
|
|
141
168
|
|
|
142
|
-
return new PostgresCompactor(this.db, this.
|
|
169
|
+
return new PostgresCompactor(this.db, this.replicationStreamId, {
|
|
143
170
|
...options,
|
|
144
171
|
maxOpId,
|
|
145
172
|
logger: this.logger
|
|
146
173
|
}).compact();
|
|
147
174
|
}
|
|
148
175
|
|
|
149
|
-
async
|
|
176
|
+
async compactInitialReplication(
|
|
177
|
+
_options: CompactInitialReplicationOptions
|
|
178
|
+
): Promise<CompactInitialReplicationResults> {
|
|
150
179
|
// no-op - checksum cache is not implemented for Postgres yet
|
|
151
180
|
return { buckets: 0 };
|
|
152
181
|
}
|
|
@@ -154,16 +183,18 @@ export class PostgresSyncRulesStorage
|
|
|
154
183
|
lastWriteCheckpoint(filters: storage.SyncStorageLastWriteCheckpointFilters): Promise<bigint | null> {
|
|
155
184
|
return this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
156
185
|
...filters,
|
|
157
|
-
sync_rules_id: this.
|
|
186
|
+
sync_rules_id: this.replicationStreamId
|
|
158
187
|
});
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
setWriteCheckpointMode(
|
|
162
|
-
return this.writeCheckpointAPI.setWriteCheckpointMode(
|
|
190
|
+
setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
|
|
191
|
+
return this.writeCheckpointAPI.setWriteCheckpointMode(config);
|
|
163
192
|
}
|
|
164
193
|
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
createManagedWriteCheckpoints(
|
|
195
|
+
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
196
|
+
): Promise<storage.CreateManagedWriteCheckpointsResult> {
|
|
197
|
+
return this.writeCheckpointAPI.createManagedWriteCheckpoints(checkpoints);
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
async getCheckpoint(): Promise<storage.ReplicationCheckpoint> {
|
|
@@ -174,7 +205,7 @@ export class PostgresSyncRulesStorage
|
|
|
174
205
|
FROM
|
|
175
206
|
sync_rules
|
|
176
207
|
WHERE
|
|
177
|
-
id = ${{ type: 'int4', value: this.
|
|
208
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
178
209
|
`
|
|
179
210
|
.decoded(pick(models.SyncRules, ['last_checkpoint', 'last_checkpoint_lsn']))
|
|
180
211
|
.first();
|
|
@@ -196,7 +227,7 @@ export class PostgresSyncRulesStorage
|
|
|
196
227
|
FROM
|
|
197
228
|
sync_rules
|
|
198
229
|
WHERE
|
|
199
|
-
id = ${{ type: 'int4', value: this.
|
|
230
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
200
231
|
`
|
|
201
232
|
.decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op', 'snapshot_lsn']))
|
|
202
233
|
.first();
|
|
@@ -206,9 +237,9 @@ export class PostgresSyncRulesStorage
|
|
|
206
237
|
const writer = new PostgresBucketBatch({
|
|
207
238
|
logger: options.logger ?? this.logger,
|
|
208
239
|
db: this.db,
|
|
209
|
-
sync_rules: this.
|
|
210
|
-
|
|
211
|
-
|
|
240
|
+
sync_rules: this.getParsedSyncRules(options),
|
|
241
|
+
replicationStreamId: this.replicationStreamId,
|
|
242
|
+
replicationStreamName: this.replicationStreamName,
|
|
212
243
|
last_checkpoint_lsn: checkpoint_lsn,
|
|
213
244
|
keep_alive_op: syncRules?.keepalive_op,
|
|
214
245
|
resumeFromLsn: maxLsn(syncRules?.snapshot_lsn, checkpoint_lsn),
|
|
@@ -254,7 +285,7 @@ export class PostgresSyncRulesStorage
|
|
|
254
285
|
value: lookups.map((l) => storage.serializeLookupBuffer(l).toString('hex'))
|
|
255
286
|
}}) WITH ORDINALITY AS requested (value, index)
|
|
256
287
|
WHERE
|
|
257
|
-
group_id = ${{ type: 'int4', value: this.
|
|
288
|
+
group_id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
258
289
|
AND lookup = decode((requested.value ->> 0)::text, 'hex') -- Decode the hex string to bytea
|
|
259
290
|
AND id <= ${{ type: 'int8', value: checkpoint.checkpoint }}
|
|
260
291
|
ORDER BY
|
|
@@ -304,7 +335,7 @@ export class PostgresSyncRulesStorage
|
|
|
304
335
|
}
|
|
305
336
|
|
|
306
337
|
async *getBucketDataBatch(
|
|
307
|
-
checkpoint:
|
|
338
|
+
checkpoint: storage.ReplicationCheckpoint,
|
|
308
339
|
dataBuckets: storage.BucketDataRequest[],
|
|
309
340
|
options?: storage.BucketDataBatchOptions
|
|
310
341
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
@@ -319,8 +350,8 @@ export class PostgresSyncRulesStorage
|
|
|
319
350
|
// Each batch query batch are streamed in separate sets of rows, which may or may
|
|
320
351
|
// not match up with chunks.
|
|
321
352
|
|
|
322
|
-
const end = checkpoint ?? BIGINT_MAX;
|
|
323
|
-
const
|
|
353
|
+
const end = checkpoint.checkpoint ?? BIGINT_MAX;
|
|
354
|
+
const sortedBuckets = [...dataBuckets].sort((a, b) => a.bucket.localeCompare(b.bucket));
|
|
324
355
|
const startOpByBucket = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
325
356
|
|
|
326
357
|
const batchRowLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
@@ -331,65 +362,66 @@ export class PostgresSyncRulesStorage
|
|
|
331
362
|
let targetOp: InternalOpId | null = null;
|
|
332
363
|
let batchRowCount = 0;
|
|
333
364
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
* FROM
|
|
343
|
-
* jsonb_array_elements($1::jsonb) AS FILTER
|
|
344
|
-
* )
|
|
345
|
-
* SELECT
|
|
346
|
-
* b.*,
|
|
347
|
-
* octet_length(b.data) AS data_size
|
|
348
|
-
* FROM
|
|
349
|
-
* bucket_data b
|
|
350
|
-
* JOIN filter_data f ON b.bucket_name = f.bucket_name
|
|
351
|
-
* AND b.op_id > f.start_op_id
|
|
352
|
-
* AND b.op_id <= $2
|
|
353
|
-
* WHERE
|
|
354
|
-
* b.group_id = $3
|
|
355
|
-
* ORDER BY
|
|
356
|
-
* b.bucket_name ASC,
|
|
357
|
-
* b.op_id ASC
|
|
358
|
-
* LIMIT
|
|
359
|
-
* $4;
|
|
360
|
-
* ```
|
|
361
|
-
* Which might be better for large volumes of buckets, but in testing the JSON method
|
|
362
|
-
* was significantly slower than the method below. Syncing 2.5 million rows in a single
|
|
363
|
-
* bucket takes 2 minutes and 11 seconds with the method below. With the JSON method
|
|
364
|
-
* 1 million rows were only synced before a 5 minute timeout.
|
|
365
|
-
*/
|
|
366
|
-
for await (const rows of this.db.streamRows({
|
|
365
|
+
const requestedBuckets = sortedBuckets
|
|
366
|
+
.map((_, index) => `($${index * 2 + 4}, $${index * 2 + 5}, ${index})`)
|
|
367
|
+
.join(', ');
|
|
368
|
+
|
|
369
|
+
// Use one round trip while keeping a parameterized index range scan per bucket. Ordering the non-flattened request
|
|
370
|
+
// subquery gives Postgres a presorted bucket key, allowing an incremental sort and the outer limit to stop the
|
|
371
|
+
// nested loop early. The inner limit cannot exclude rows needed by the outer batch because both limits are equal.
|
|
372
|
+
const query: Statement = {
|
|
367
373
|
statement: `
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
374
|
+
SELECT
|
|
375
|
+
bucket_data.*
|
|
376
|
+
FROM
|
|
377
|
+
(
|
|
378
|
+
SELECT
|
|
379
|
+
*
|
|
380
|
+
FROM
|
|
381
|
+
(
|
|
382
|
+
VALUES ${requestedBuckets}
|
|
383
|
+
) AS bucket_requests(bucket_name, start_op_id, bucket_order)
|
|
384
|
+
-- Present requests in bucket order so the final sort can be incremental.
|
|
385
|
+
ORDER BY
|
|
386
|
+
bucket_order ASC
|
|
387
|
+
-- Prevent the planner from flattening away the ordered request path.
|
|
388
|
+
OFFSET
|
|
389
|
+
0
|
|
390
|
+
) AS requested
|
|
391
|
+
CROSS JOIN LATERAL (
|
|
392
|
+
SELECT
|
|
393
|
+
*
|
|
394
|
+
FROM
|
|
395
|
+
bucket_data
|
|
396
|
+
WHERE
|
|
397
|
+
group_id = $1
|
|
398
|
+
AND bucket_name = requested.bucket_name
|
|
399
|
+
AND op_id > requested.start_op_id
|
|
400
|
+
AND op_id <= $2
|
|
401
|
+
-- Keep each bucket's index range scan ordered and bounded.
|
|
402
|
+
ORDER BY
|
|
403
|
+
op_id ASC
|
|
404
|
+
LIMIT
|
|
405
|
+
$3
|
|
406
|
+
) AS bucket_data
|
|
407
|
+
-- Guarantee contiguous bucket chunks while allowing the presorted bucket key to stop scans early.
|
|
408
|
+
ORDER BY
|
|
409
|
+
requested.bucket_order ASC,
|
|
410
|
+
bucket_data.op_id ASC
|
|
411
|
+
LIMIT
|
|
412
|
+
$3;`,
|
|
383
413
|
params: [
|
|
384
|
-
{ type: 'int4', value: this.
|
|
414
|
+
{ type: 'int4', value: this.replicationStreamId },
|
|
385
415
|
{ type: 'int8', value: end },
|
|
386
416
|
{ type: 'int4', value: batchRowLimit },
|
|
387
|
-
...
|
|
388
|
-
{ type: 'varchar' as const, value:
|
|
389
|
-
{ type: 'int8' as const, value:
|
|
417
|
+
...sortedBuckets.flatMap((request) => [
|
|
418
|
+
{ type: 'varchar' as const, value: request.bucket },
|
|
419
|
+
{ type: 'int8' as const, value: request.start }
|
|
390
420
|
])
|
|
391
421
|
]
|
|
392
|
-
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
for await (const rows of this.db.streamRows(query)) {
|
|
393
425
|
const decodedRows = rows.map((r) => models.BucketData.decode(r as any));
|
|
394
426
|
|
|
395
427
|
for (const row of decodedRows) {
|
|
@@ -477,10 +509,11 @@ export class PostgresSyncRulesStorage
|
|
|
477
509
|
}
|
|
478
510
|
|
|
479
511
|
async getChecksums(
|
|
480
|
-
checkpoint:
|
|
481
|
-
buckets: storage.BucketChecksumRequest[]
|
|
512
|
+
checkpoint: storage.ReplicationCheckpoint,
|
|
513
|
+
buckets: storage.BucketChecksumRequest[],
|
|
514
|
+
_options?: storage.BucketChecksumOptions
|
|
482
515
|
): Promise<utils.ChecksumMap> {
|
|
483
|
-
return this.checksumCache.getChecksumMap(checkpoint, buckets);
|
|
516
|
+
return this.checksumCache.getChecksumMap(checkpoint.checkpoint, buckets);
|
|
484
517
|
}
|
|
485
518
|
|
|
486
519
|
clearChecksumCache() {
|
|
@@ -497,26 +530,23 @@ export class PostgresSyncRulesStorage
|
|
|
497
530
|
state = ${{ type: 'varchar', value: storage.SyncRuleState.TERMINATED }},
|
|
498
531
|
snapshot_done = ${{ type: 'bool', value: false }}
|
|
499
532
|
WHERE
|
|
500
|
-
id = ${{ type: 'int4', value: this.
|
|
533
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
501
534
|
`.execute();
|
|
502
535
|
}
|
|
503
536
|
|
|
504
|
-
async getStatus(): Promise<storage.
|
|
537
|
+
async getStatus(): Promise<storage.ReplicationStreamStatus> {
|
|
505
538
|
const syncRulesRow = await this.db.sql`
|
|
506
539
|
SELECT
|
|
507
540
|
snapshot_done,
|
|
508
541
|
snapshot_lsn,
|
|
509
542
|
last_checkpoint_lsn,
|
|
510
|
-
state
|
|
511
|
-
keepalive_op
|
|
543
|
+
state
|
|
512
544
|
FROM
|
|
513
545
|
sync_rules
|
|
514
546
|
WHERE
|
|
515
|
-
id = ${{ type: 'int4', value: this.
|
|
547
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
516
548
|
`
|
|
517
|
-
.decoded(
|
|
518
|
-
pick(models.SyncRules, ['snapshot_done', 'last_checkpoint_lsn', 'state', 'snapshot_lsn', 'keepalive_op'])
|
|
519
|
-
)
|
|
549
|
+
.decoded(pick(models.SyncRules, ['snapshot_done', 'last_checkpoint_lsn', 'snapshot_lsn']))
|
|
520
550
|
.first();
|
|
521
551
|
|
|
522
552
|
if (syncRulesRow == null) {
|
|
@@ -524,16 +554,17 @@ export class PostgresSyncRulesStorage
|
|
|
524
554
|
}
|
|
525
555
|
|
|
526
556
|
return {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
checkpoint_lsn: syncRulesRow.last_checkpoint_lsn ?? null,
|
|
530
|
-
snapshot_lsn: syncRulesRow.snapshot_lsn ?? null,
|
|
531
|
-
keepalive_op: syncRulesRow.keepalive_op ?? null
|
|
557
|
+
snapshotDone: syncRulesRow.snapshot_done && syncRulesRow.last_checkpoint_lsn != null,
|
|
558
|
+
resumeLsn: maxLsn(syncRulesRow.snapshot_lsn, syncRulesRow.last_checkpoint_lsn)
|
|
532
559
|
};
|
|
533
560
|
}
|
|
534
561
|
|
|
535
562
|
async clear(options?: storage.ClearStorageOptions): Promise<void> {
|
|
536
|
-
|
|
563
|
+
const signal = options?.signal;
|
|
564
|
+
if (signal?.aborted) {
|
|
565
|
+
throw new framework.ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
566
|
+
}
|
|
567
|
+
|
|
537
568
|
await this.db.sql`
|
|
538
569
|
UPDATE sync_rules
|
|
539
570
|
SET
|
|
@@ -542,28 +573,71 @@ export class PostgresSyncRulesStorage
|
|
|
542
573
|
last_checkpoint = NULL,
|
|
543
574
|
no_checkpoint_before = NULL
|
|
544
575
|
WHERE
|
|
545
|
-
id = ${{ type: 'int4', value: this.
|
|
546
|
-
`.execute();
|
|
547
|
-
|
|
548
|
-
await this.db.sql`
|
|
549
|
-
DELETE FROM bucket_data
|
|
550
|
-
WHERE
|
|
551
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
576
|
+
id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
552
577
|
`.execute();
|
|
553
578
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
579
|
+
// Delete in batches - a single DELETE covering the entire group can run for
|
|
580
|
+
// hours on large deployments, never completing once it exceeds statement or
|
|
581
|
+
// socket timeouts. Each batch is its own autocommit statement, so progress
|
|
582
|
+
// is durable and a retry continues where the previous attempt stopped.
|
|
583
|
+
await this.clearBatched('bucket_data', signal, () => this.deleteGroupBatch('bucket_data'));
|
|
584
|
+
await this.clearBatched('bucket_parameters', signal, () => this.deleteGroupBatch('bucket_parameters'));
|
|
585
|
+
await this.clearBatched('current_data', signal, () =>
|
|
586
|
+
this.currentDataStore.deleteGroupRowsBatch(this.db, {
|
|
587
|
+
groupId: this.replicationStreamId,
|
|
588
|
+
limit: CLEAR_BATCH_LIMIT
|
|
589
|
+
})
|
|
590
|
+
);
|
|
591
|
+
await this.clearBatched('source_tables', signal, () => this.deleteGroupBatch('source_tables'));
|
|
592
|
+
}
|
|
559
593
|
|
|
560
|
-
|
|
594
|
+
private async clearBatched(
|
|
595
|
+
label: string,
|
|
596
|
+
signal: AbortSignal | undefined,
|
|
597
|
+
deleteBatch: () => Promise<bigint>
|
|
598
|
+
): Promise<void> {
|
|
599
|
+
while (true) {
|
|
600
|
+
if (signal?.aborted) {
|
|
601
|
+
throw new framework.ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
602
|
+
}
|
|
603
|
+
const count = await deleteBatch();
|
|
604
|
+
if (count < CLEAR_BATCH_LIMIT) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
this.logger.info(`Cleared batch of ${count} ${label} rows, continuing...`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
561
610
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Delete up to {@link CLEAR_BATCH_LIMIT} rows for this group from the given table,
|
|
613
|
+
* returning the number of candidate rows found by the scan.
|
|
614
|
+
*
|
|
615
|
+
* The count is taken from the scan (`batch`) rather than the delete: with a
|
|
616
|
+
* concurrent process deleting the same rows, the delete may remove fewer rows
|
|
617
|
+
* than the scan found, and counting deleted rows could stop the loop while
|
|
618
|
+
* rows remain.
|
|
619
|
+
*/
|
|
620
|
+
private async deleteGroupBatch(table: 'bucket_data' | 'bucket_parameters' | 'source_tables'): Promise<bigint> {
|
|
621
|
+
const [row] = await this.db.queryRows<{ count: bigint }>({
|
|
622
|
+
statement: `
|
|
623
|
+
WITH batch AS (
|
|
624
|
+
SELECT ctid FROM ${table}
|
|
625
|
+
WHERE group_id = $1
|
|
626
|
+
LIMIT $2
|
|
627
|
+
),
|
|
628
|
+
deleted AS (
|
|
629
|
+
DELETE FROM ${table}
|
|
630
|
+
WHERE ctid IN (SELECT ctid FROM batch)
|
|
631
|
+
RETURNING 1
|
|
632
|
+
)
|
|
633
|
+
SELECT COUNT(*) AS count FROM batch
|
|
634
|
+
`,
|
|
635
|
+
params: [
|
|
636
|
+
{ type: 'int4', value: this.replicationStreamId },
|
|
637
|
+
{ type: 'int4', value: CLEAR_BATCH_LIMIT }
|
|
638
|
+
]
|
|
639
|
+
});
|
|
640
|
+
return row?.count ?? 0n;
|
|
567
641
|
}
|
|
568
642
|
|
|
569
643
|
private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
|
|
@@ -603,7 +677,7 @@ export class PostgresSyncRulesStorage
|
|
|
603
677
|
AND b.op_id > f.start_op_id
|
|
604
678
|
AND b.op_id <= f.end_op_id
|
|
605
679
|
WHERE
|
|
606
|
-
b.group_id = ${{ type: 'int4', value: this.
|
|
680
|
+
b.group_id = ${{ type: 'int4', value: this.replicationStreamId }}
|
|
607
681
|
GROUP BY
|
|
608
682
|
b.bucket_name;
|
|
609
683
|
`.rows<{ bucket: string; checksum_total: bigint; total: bigint; has_clear_op: number }>();
|
|
@@ -632,25 +706,8 @@ export class PostgresSyncRulesStorage
|
|
|
632
706
|
}
|
|
633
707
|
|
|
634
708
|
async getActiveCheckpoint(): Promise<storage.ReplicationCheckpoint> {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
id,
|
|
638
|
-
last_checkpoint,
|
|
639
|
-
last_checkpoint_lsn
|
|
640
|
-
FROM
|
|
641
|
-
sync_rules
|
|
642
|
-
WHERE
|
|
643
|
-
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
644
|
-
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
645
|
-
ORDER BY
|
|
646
|
-
id DESC
|
|
647
|
-
LIMIT
|
|
648
|
-
1
|
|
649
|
-
`
|
|
650
|
-
.decoded(models.ActiveCheckpoint)
|
|
651
|
-
.first();
|
|
652
|
-
|
|
653
|
-
return this.makeActiveCheckpoint(activeCheckpoint);
|
|
709
|
+
const activeCheckpointDocument = await checkpointUtils.getActiveCheckpointDocument({ db: this.db });
|
|
710
|
+
return this.makeActiveCheckpoint(activeCheckpointDocument);
|
|
654
711
|
}
|
|
655
712
|
|
|
656
713
|
async *watchCheckpointChanges(options: WatchWriteCheckpointOptions): AsyncIterable<storage.StorageCheckpointUpdate> {
|
|
@@ -696,61 +753,92 @@ export class PostgresSyncRulesStorage
|
|
|
696
753
|
}
|
|
697
754
|
|
|
698
755
|
protected async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<storage.ReplicationCheckpoint> {
|
|
699
|
-
const
|
|
700
|
-
SELECT
|
|
701
|
-
id,
|
|
702
|
-
last_checkpoint,
|
|
703
|
-
last_checkpoint_lsn
|
|
704
|
-
FROM
|
|
705
|
-
sync_rules
|
|
706
|
-
WHERE
|
|
707
|
-
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
708
|
-
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
709
|
-
LIMIT
|
|
710
|
-
1
|
|
711
|
-
`
|
|
712
|
-
.decoded(models.ActiveCheckpoint)
|
|
713
|
-
.first();
|
|
714
|
-
|
|
715
|
-
if (doc == null) {
|
|
716
|
-
// Abort the connections - clients will have to retry later.
|
|
717
|
-
throw new framework.ServiceError(framework.ErrorCode.PSYNC_S2302, 'No active replication stream available');
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const sink = new LastValueSink<string>(undefined);
|
|
756
|
+
const sink = new LastValueSink<string | null>(null);
|
|
721
757
|
|
|
758
|
+
// Listen for changes before reading the initial doc
|
|
722
759
|
const disposeListener = this.db.registerListener({
|
|
723
|
-
|
|
760
|
+
notificationEvent: (event) => {
|
|
761
|
+
switch (event.type) {
|
|
762
|
+
case 'notification':
|
|
763
|
+
sink.write(event.notification.payload);
|
|
764
|
+
break;
|
|
765
|
+
case 'channels-registered':
|
|
766
|
+
// Add a null value to the stream, indicating the loop should query the value.
|
|
767
|
+
sink.write(null);
|
|
768
|
+
break;
|
|
769
|
+
case 'connection-error':
|
|
770
|
+
// End the watcher after reconnect attempts are exhausted. Consumers will
|
|
771
|
+
// retry the watcher, which registers a new listener and pokes the slot.
|
|
772
|
+
sink.error(event.error);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
724
776
|
});
|
|
725
777
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
778
|
+
try {
|
|
779
|
+
const initialCheckpointDocument = requireActiveCheckpointDocument(
|
|
780
|
+
await checkpointUtils.getActiveCheckpointDocument({ db: this.db })
|
|
781
|
+
);
|
|
782
|
+
const initialCheckpoint = this.makeActiveCheckpoint(initialCheckpointDocument);
|
|
783
|
+
let lastOp = initialCheckpoint;
|
|
730
784
|
|
|
731
|
-
|
|
785
|
+
yield initialCheckpoint;
|
|
732
786
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
787
|
+
for await (const payload of sink.withSignal(signal)) {
|
|
788
|
+
if (signal.aborted) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
738
791
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
792
|
+
let baseActiveCheckpoint: ActiveCheckpointDecoded | null = null;
|
|
793
|
+
if (payload == null) {
|
|
794
|
+
// Reconnected (or manually triggered) - re-query the current checkpoint.
|
|
795
|
+
// Unlike the initial read, a missing document here (e.g. sync rules being
|
|
796
|
+
// replaced) must not abort the stream: keep waiting for the next notification.
|
|
797
|
+
baseActiveCheckpoint = await checkpointUtils.getActiveCheckpointDocument({ db: this.db });
|
|
798
|
+
if (baseActiveCheckpoint == null) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
let notification: models.ActiveCheckpointNotificationDecoded;
|
|
803
|
+
try {
|
|
804
|
+
notification = models.ActiveCheckpointNotification.decode(payload);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
// A malformed payload must not abort the shared stream for every
|
|
807
|
+
// subscriber. Skip it and wait for the next notification.
|
|
808
|
+
this.logger.warn('Failed to decode active checkpoint notification, ignoring', error);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (notification.active_checkpoint == null) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
baseActiveCheckpoint = notification.active_checkpoint;
|
|
815
|
+
}
|
|
747
816
|
|
|
748
|
-
|
|
817
|
+
if (baseActiveCheckpoint.id != initialCheckpointDocument.id) {
|
|
818
|
+
// Active replication stream changed - abort and restart the stream
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
749
821
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
822
|
+
const activeCheckpoint = this.makeActiveCheckpoint(baseActiveCheckpoint);
|
|
823
|
+
|
|
824
|
+
const checkpointAdvanced = activeCheckpoint.checkpoint > lastOp.checkpoint;
|
|
825
|
+
const checkpointRegressed = activeCheckpoint.checkpoint < lastOp.checkpoint;
|
|
826
|
+
const lsnAdvanced =
|
|
827
|
+
lastOp.lsn == null
|
|
828
|
+
? activeCheckpoint.lsn != null
|
|
829
|
+
: activeCheckpoint.lsn != null && activeCheckpoint.lsn > lastOp.lsn;
|
|
830
|
+
const lsnRegressed = lastOp.lsn != null && (activeCheckpoint.lsn == null || activeCheckpoint.lsn < lastOp.lsn);
|
|
831
|
+
|
|
832
|
+
// Notifications buffered before a query or reconnect may be stale. Neither
|
|
833
|
+
// coordinate may regress, but only one needs to advance: an empty checkpoint
|
|
834
|
+
// updates the LSN while keeping the operation checkpoint unchanged.
|
|
835
|
+
if (!checkpointRegressed && !lsnRegressed && (checkpointAdvanced || lsnAdvanced)) {
|
|
836
|
+
lastOp = activeCheckpoint;
|
|
837
|
+
yield activeCheckpoint;
|
|
838
|
+
}
|
|
753
839
|
}
|
|
840
|
+
} finally {
|
|
841
|
+
disposeListener();
|
|
754
842
|
}
|
|
755
843
|
}
|
|
756
844
|
|
|
@@ -783,3 +871,14 @@ const parameterSetsRow = t.object({
|
|
|
783
871
|
index: bigint,
|
|
784
872
|
bucket_parameters: t.string
|
|
785
873
|
});
|
|
874
|
+
|
|
875
|
+
function requireActiveCheckpointDocument(doc: models.ActiveCheckpointDecoded | null): models.ActiveCheckpointDecoded {
|
|
876
|
+
if (doc == null) {
|
|
877
|
+
// Used for the initial checkpoint read only: with no active replication stream
|
|
878
|
+
// at stream start, fail fast so clients disconnect and retry later. Mid-stream
|
|
879
|
+
// reconnects tolerate a transiently missing document instead of aborting.
|
|
880
|
+
throw new framework.ServiceError(framework.ErrorCode.PSYNC_S2302, 'No active replication stream available');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return doc;
|
|
884
|
+
}
|