@powersync/service-module-postgres-storage 0.16.1 → 0.16.2
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 +19 -0
- package/dist/storage/PostgresBucketStorageFactory.d.ts +1 -0
- package/dist/storage/PostgresBucketStorageFactory.js +2 -1
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.d.ts +1 -1
- package/dist/storage/PostgresCompactor.js +1 -1
- package/dist/storage/PostgresStorageProvider.js +2 -1
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.d.ts +5 -4
- package/dist/storage/PostgresSyncRulesStorage.js +6 -5
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.d.ts +4 -1
- package/dist/storage/batch/PostgresBucketBatch.js +22 -12
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +27 -10
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/PostgresBucketStorageFactory.ts +3 -1
- package/src/storage/PostgresCompactor.ts +1 -1
- package/src/storage/PostgresStorageProvider.ts +2 -1
- package/src/storage/PostgresSyncRulesStorage.ts +10 -7
- package/src/storage/batch/PostgresBucketBatch.ts +27 -14
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +32 -10
- package/test/src/storage.test.ts +6 -4
- package/test/src/storage_compacting.test.ts +9 -3
- package/test/src/storage_sync.test.ts +6 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -17,19 +17,36 @@ export class PostgresWriteCheckpointAPI {
|
|
|
17
17
|
async batchCreateCustomWriteCheckpoints(checkpoints, op_id) {
|
|
18
18
|
return batchCreateCustomWriteCheckpoints(this.db, checkpoints);
|
|
19
19
|
}
|
|
20
|
-
async
|
|
20
|
+
async createManagedWriteCheckpoints(checkpoints) {
|
|
21
21
|
if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
|
|
22
22
|
throw new framework.errors.ValidationError(`Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`);
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const uniqueCheckpoints = [...new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, checkpoint])).values()];
|
|
25
|
+
if (uniqueCheckpoints.length == 0) {
|
|
26
|
+
return new Map();
|
|
27
|
+
}
|
|
28
|
+
const mappedCheckpoints = uniqueCheckpoints.map((checkpoint) => ({
|
|
29
|
+
user_id: checkpoint.user_id,
|
|
30
|
+
lsns: checkpoint.heads
|
|
31
|
+
}));
|
|
32
|
+
const rows = await this.db.sql `
|
|
33
|
+
WITH
|
|
34
|
+
json_data AS (
|
|
35
|
+
SELECT
|
|
36
|
+
CHECKPOINT ->> 'user_id' AS user_id,
|
|
37
|
+
CHECKPOINT -> 'lsns' AS lsns
|
|
38
|
+
FROM
|
|
39
|
+
jsonb_array_elements(${{ type: 'jsonb', value: mappedCheckpoints }}) AS
|
|
40
|
+
CHECKPOINT
|
|
41
|
+
)
|
|
25
42
|
INSERT INTO
|
|
26
43
|
write_checkpoints (user_id, lsns, write_checkpoint)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
SELECT
|
|
45
|
+
user_id,
|
|
46
|
+
lsns,
|
|
47
|
+
1
|
|
48
|
+
FROM
|
|
49
|
+
json_data
|
|
33
50
|
ON CONFLICT (user_id) DO UPDATE
|
|
34
51
|
SET
|
|
35
52
|
write_checkpoint = write_checkpoints.write_checkpoint + 1,
|
|
@@ -38,8 +55,8 @@ export class PostgresWriteCheckpointAPI {
|
|
|
38
55
|
*;
|
|
39
56
|
`
|
|
40
57
|
.decoded(models.WriteCheckpoint)
|
|
41
|
-
.
|
|
42
|
-
return row.write_checkpoint;
|
|
58
|
+
.rows();
|
|
59
|
+
return new Map(rows.map((row) => [row.user_id, row.write_checkpoint]));
|
|
43
60
|
}
|
|
44
61
|
async lastWriteCheckpoint(filters) {
|
|
45
62
|
switch (this.writeCheckpointMode) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostgresWriteCheckpointAPI.js","sourceRoot":"","sources":["../../../src/storage/checkpoints/PostgresWriteCheckpointAPI.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAgB,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAO9C,MAAM,OAAO,0BAA0B;IAC5B,EAAE,CAA8B;IACjC,KAAK,CAA8B;IAE3C,YAAY,OAAqC;QAC/C,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI,mBAAmB;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,sBAAsB,CAAC,IAAiC;QACtD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,iCAAiC,CACrC,WAAmD,EACnD,KAAmB;QAEnB,OAAO,iCAAiC,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"PostgresWriteCheckpointAPI.js","sourceRoot":"","sources":["../../../src/storage/checkpoints/PostgresWriteCheckpointAPI.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAgB,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAO9C,MAAM,OAAO,0BAA0B;IAC5B,EAAE,CAA8B;IACjC,KAAK,CAA8B;IAE3C,YAAY,OAAqC;QAC/C,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI,mBAAmB;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,sBAAsB,CAAC,IAAiC;QACtD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,iCAAiC,CACrC,WAAmD,EACnD,KAAmB;QAEnB,OAAO,iCAAiC,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,6BAA6B,CACjC,WAAoD;QAEpD,IAAI,IAAI,CAAC,mBAAmB,KAAK,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC;YACrE,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC,eAAe,CACxC,qGAAqG,IAAI,CAAC,mBAAmB,GAAG,CACjI,CAAC;QACJ,CAAC;QAED,MAAM,iBAAiB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACnH,IAAI,iBAAiB,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC/D,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,IAAI,EAAE,UAAU,CAAC,KAAK;SACvB,CAAC,CAAC,CAAC;QAEJ,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAA;;;;;;;mCAOC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE;;;;;;;;;;;;;;;;;KAiBzE;aACE,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC;aAC/B,IAAI,EAAE,CAAC;QAEV,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAA2C;QACnE,QAAQ,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACjC,KAAK,OAAO,CAAC,mBAAmB,CAAC,MAAM;gBACrC,IAAI,KAAK,IAAI,eAAe,IAAI,OAAO,EAAE,CAAC;oBACxC,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC,eAAe,CACxC,yEAAyE,CAC1E,CAAC;gBACJ,CAAC;gBACD,OAAO,IAAI,CAAC,yBAAyB,CAAC,OAA+C,CAAC,CAAC;YACzF,KAAK,OAAO,CAAC,mBAAmB,CAAC,OAAO;gBACtC,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;oBAChC,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC,eAAe,CACxC,qEAAqE,CACtE,CAAC;gBACJ,CAAC;gBACD,OAAO,IAAI,CAAC,0BAA0B,CAAC,OAAgD,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;IAES,KAAK,CAAC,yBAAyB,CAAC,OAA6C;QACrF,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAA;;;;;;oBAMb,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE;8BACzB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE;KAC/D;aACE,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;aACrC,KAAK,EAAE,CAAC;QACX,OAAO,GAAG,EAAE,gBAAgB,IAAI,IAAI,CAAC;IACvC,CAAC;IAES,KAAK,CAAC,0BAA0B,CAAC,OAA8C;QACvF,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;QACnC,4EAA4E;QAC5E,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YAChB,oDAAoD;YACpD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAA;;;;;;oBAMb,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE;8BACzB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;KACxD;aACE,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC;aAC/B,KAAK,EAAE,CAAC;QACX,OAAO,GAAG,EAAE,gBAAgB,IAAI,IAAI,CAAC;IACvC,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAA+B,EAC/B,WAAmD;IAEnD,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO;IACT,CAAC;IAED,iDAAiD;IACjD,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAC/C,OAAO;YACL,OAAO,EAAE,EAAE,CAAC,OAAO;YACnB,sDAAsD;YACtD,2EAA2E;YAC3E,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC;YACjC,aAAa,EAAE,EAAE,CAAC,aAAa;SAChC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,EAAE,CAAC,GAAG,CAAA;;;;iCAImB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE;;;;;;;;;;;;;;;;;;GAkBzE,CAAC,OAAO,EAAE,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/service-module-postgres-storage",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.2",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
"lru-cache": "^10.2.2",
|
|
17
17
|
"ts-codec": "^1.3.0",
|
|
18
18
|
"uuid": "^14.0.0",
|
|
19
|
-
"@powersync/lib-service-postgres": "0.5.
|
|
20
|
-
"@powersync/
|
|
21
|
-
"@powersync/service-
|
|
22
|
-
"@powersync/
|
|
19
|
+
"@powersync/lib-service-postgres": "0.5.1",
|
|
20
|
+
"@powersync/service-core": "1.23.2",
|
|
21
|
+
"@powersync/service-types": "0.16.1",
|
|
22
|
+
"@powersync/lib-services-framework": "0.9.7",
|
|
23
23
|
"@powersync/service-jpgwire": "0.21.20",
|
|
24
24
|
"@powersync/service-jsonbig": "0.17.13",
|
|
25
|
-
"@powersync/service-sync-rules": "0.38.
|
|
25
|
+
"@powersync/service-sync-rules": "0.38.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "~6.0.3",
|
|
29
|
-
"@powersync/service-core-tests": "0.17.
|
|
29
|
+
"@powersync/service-core-tests": "0.17.1"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -b",
|
|
@@ -15,6 +15,7 @@ import { PostgresPersistedReplicationStream } from './sync-rules/PostgresPersist
|
|
|
15
15
|
export type PostgresBucketStorageOptions = {
|
|
16
16
|
config: NormalizedPostgresStorageConfig;
|
|
17
17
|
replicationStreamNamePrefix: string;
|
|
18
|
+
checksumCacheTtlMs?: number;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
@@ -56,7 +57,8 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
56
57
|
factory: this,
|
|
57
58
|
db: this.db,
|
|
58
59
|
replicationStream,
|
|
59
|
-
batchLimits: this.options.config.batch_limits
|
|
60
|
+
batchLimits: this.options.config.batch_limits,
|
|
61
|
+
checksumCacheTtlMs: this.options.checksumCacheTtlMs
|
|
60
62
|
});
|
|
61
63
|
if (!options?.skipLifecycleHooks) {
|
|
62
64
|
this.iterateListeners((cb) => cb.syncStorageCreated?.(syncRuleStorage));
|
|
@@ -70,7 +70,7 @@ export class PostgresCompactor {
|
|
|
70
70
|
/**
|
|
71
71
|
* Compact buckets by converting operations into MOVE and/or CLEAR operations.
|
|
72
72
|
*
|
|
73
|
-
* See /docs/compacting-operations.md for details.
|
|
73
|
+
* See /docs/storage/compacting-operations.md for details.
|
|
74
74
|
*/
|
|
75
75
|
async compact() {
|
|
76
76
|
if (this.buckets) {
|
|
@@ -27,7 +27,8 @@ export class PostgresStorageProvider implements storage.StorageProvider {
|
|
|
27
27
|
const normalizedConfig = normalizePostgresStorageConfig(decodedConfig);
|
|
28
28
|
const storageFactory = new PostgresBucketStorageFactory({
|
|
29
29
|
config: normalizedConfig,
|
|
30
|
-
replicationStreamNamePrefix: options.resolvedConfig.slot_name_prefix
|
|
30
|
+
replicationStreamNamePrefix: options.resolvedConfig.slot_name_prefix,
|
|
31
|
+
checksumCacheTtlMs: options.resolvedConfig.api_parameters.bucket_count_cache_ttl_minutes * 60_000
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
const reportStorageFactory = new PostgresReportStorage({
|
|
@@ -44,6 +44,7 @@ export type PostgresSyncRulesStorageOptions = {
|
|
|
44
44
|
replicationStream: storage.PersistedReplicationStream;
|
|
45
45
|
write_checkpoint_mode?: storage.WriteCheckpointMode;
|
|
46
46
|
batchLimits: RequiredOperationBatchLimits;
|
|
47
|
+
checksumCacheTtlMs?: number;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export class PostgresSyncRulesStorage
|
|
@@ -101,6 +102,7 @@ export class PostgresSyncRulesStorage
|
|
|
101
102
|
*/
|
|
102
103
|
private get checksumCache(): storage.ChecksumCache {
|
|
103
104
|
this._checksumCache ??= new storage.ChecksumCache({
|
|
105
|
+
ttlMs: this.options.checksumCacheTtlMs,
|
|
104
106
|
fetchChecksums: (batch) => {
|
|
105
107
|
return this.getChecksumsInternal(batch);
|
|
106
108
|
}
|
|
@@ -167,8 +169,8 @@ export class PostgresSyncRulesStorage
|
|
|
167
169
|
return this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
return this.writeCheckpointAPI.
|
|
172
|
+
createManagedWriteCheckpoints(checkpoints: storage.ManagedWriteCheckpointOptions[]): Promise<Map<string, bigint>> {
|
|
173
|
+
return this.writeCheckpointAPI.createManagedWriteCheckpoints(checkpoints);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
async getCheckpoint(): Promise<storage.ReplicationCheckpoint> {
|
|
@@ -309,7 +311,7 @@ export class PostgresSyncRulesStorage
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
async *getBucketDataBatch(
|
|
312
|
-
checkpoint:
|
|
314
|
+
checkpoint: storage.ReplicationCheckpoint,
|
|
313
315
|
dataBuckets: storage.BucketDataRequest[],
|
|
314
316
|
options?: storage.BucketDataBatchOptions
|
|
315
317
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
@@ -324,7 +326,7 @@ export class PostgresSyncRulesStorage
|
|
|
324
326
|
// Each batch query batch are streamed in separate sets of rows, which may or may
|
|
325
327
|
// not match up with chunks.
|
|
326
328
|
|
|
327
|
-
const end = checkpoint ?? BIGINT_MAX;
|
|
329
|
+
const end = checkpoint.checkpoint ?? BIGINT_MAX;
|
|
328
330
|
const filters = dataBuckets.map((request) => ({ bucket_name: request.bucket, start: request.start }));
|
|
329
331
|
const startOpByBucket = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
330
332
|
|
|
@@ -482,10 +484,11 @@ export class PostgresSyncRulesStorage
|
|
|
482
484
|
}
|
|
483
485
|
|
|
484
486
|
async getChecksums(
|
|
485
|
-
checkpoint:
|
|
486
|
-
buckets: storage.BucketChecksumRequest[]
|
|
487
|
+
checkpoint: storage.ReplicationCheckpoint,
|
|
488
|
+
buckets: storage.BucketChecksumRequest[],
|
|
489
|
+
_options?: storage.BucketChecksumOptions
|
|
487
490
|
): Promise<utils.ChecksumMap> {
|
|
488
|
-
return this.checksumCache.getChecksumMap(checkpoint, buckets);
|
|
491
|
+
return this.checksumCache.getChecksumMap(checkpoint.checkpoint, buckets);
|
|
489
492
|
}
|
|
490
493
|
|
|
491
494
|
clearChecksumCache() {
|
|
@@ -422,7 +422,9 @@ export class PostgresBucketBatch
|
|
|
422
422
|
let processedCount = 0;
|
|
423
423
|
while (lastBatchCount == BATCH_LIMIT) {
|
|
424
424
|
lastBatchCount = 0;
|
|
425
|
+
let clearedError = false;
|
|
425
426
|
await this.withReplicationTransaction(async (db) => {
|
|
427
|
+
clearedError = false;
|
|
426
428
|
const persistedBatch = new PostgresPersistedBatch({
|
|
427
429
|
group_id: this.group_id,
|
|
428
430
|
storageConfig: this.storageConfig,
|
|
@@ -462,10 +464,15 @@ export class PostgresBucketBatch
|
|
|
462
464
|
}
|
|
463
465
|
}
|
|
464
466
|
const { flushedAny } = await persistedBatch.flush(db);
|
|
465
|
-
|
|
467
|
+
clearedError = flushedAny && !this.clearedError;
|
|
468
|
+
if (clearedError) {
|
|
469
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
466
470
|
await this.clearError(db);
|
|
467
471
|
}
|
|
468
472
|
});
|
|
473
|
+
if (clearedError) {
|
|
474
|
+
this.clearedError = true;
|
|
475
|
+
}
|
|
469
476
|
}
|
|
470
477
|
if (processedCount == 0) {
|
|
471
478
|
// The op sequence should not have progressed
|
|
@@ -518,13 +525,21 @@ export class PostgresBucketBatch
|
|
|
518
525
|
await this.hooks?.beforeBatchFlush?.(this);
|
|
519
526
|
|
|
520
527
|
let resumeBatch: OperationBatch | null = null;
|
|
528
|
+
let clearedError = false;
|
|
521
529
|
|
|
522
530
|
const lastOp = await this.withReplicationTransaction(async (db) => {
|
|
523
|
-
|
|
531
|
+
clearedError = false;
|
|
532
|
+
const result = await this.replicateBatch(db, batch);
|
|
533
|
+
resumeBatch = result.resumeBatch;
|
|
534
|
+
clearedError ||= result.clearedError;
|
|
524
535
|
|
|
525
536
|
return this.getLastOpIdSequence(db);
|
|
526
537
|
});
|
|
527
538
|
|
|
539
|
+
if (clearedError) {
|
|
540
|
+
this.clearedError = true;
|
|
541
|
+
}
|
|
542
|
+
|
|
528
543
|
// null if done, set if we need another flush
|
|
529
544
|
this.batch = resumeBatch;
|
|
530
545
|
|
|
@@ -891,7 +906,10 @@ export class PostgresBucketBatch
|
|
|
891
906
|
});
|
|
892
907
|
}
|
|
893
908
|
|
|
894
|
-
protected async replicateBatch(
|
|
909
|
+
protected async replicateBatch(
|
|
910
|
+
db: lib_postgres.WrappedConnection,
|
|
911
|
+
batch: OperationBatch
|
|
912
|
+
): Promise<{ resumeBatch: OperationBatch | null; clearedError: boolean }> {
|
|
895
913
|
let sizes: Map<string, number> | undefined = undefined;
|
|
896
914
|
// Check if any table in this batch needs to store current_data
|
|
897
915
|
const anyTableStoresCurrentData =
|
|
@@ -1015,15 +1033,16 @@ export class PostgresBucketBatch
|
|
|
1015
1033
|
}
|
|
1016
1034
|
}
|
|
1017
1035
|
|
|
1018
|
-
|
|
1036
|
+
const clearedError = didFlush && !this.clearedError;
|
|
1037
|
+
if (clearedError) {
|
|
1019
1038
|
await this.clearError(db);
|
|
1020
1039
|
}
|
|
1021
1040
|
|
|
1022
1041
|
// Don't return empty batches
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1042
|
+
return {
|
|
1043
|
+
resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
|
|
1044
|
+
clearedError
|
|
1045
|
+
};
|
|
1027
1046
|
}
|
|
1028
1047
|
|
|
1029
1048
|
protected async saveOperation(
|
|
@@ -1388,11 +1407,6 @@ export class PostgresBucketBatch
|
|
|
1388
1407
|
protected async clearError(
|
|
1389
1408
|
db: lib_postgres.AbstractPostgresConnection | lib_postgres.DatabaseClient = this.db
|
|
1390
1409
|
): Promise<void> {
|
|
1391
|
-
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
1392
|
-
if (this.clearedError) {
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
1410
|
await db.sql`
|
|
1397
1411
|
UPDATE sync_rules
|
|
1398
1412
|
SET
|
|
@@ -1400,7 +1414,6 @@ export class PostgresBucketBatch
|
|
|
1400
1414
|
WHERE
|
|
1401
1415
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
1402
1416
|
`.execute();
|
|
1403
|
-
this.clearedError = true;
|
|
1404
1417
|
}
|
|
1405
1418
|
|
|
1406
1419
|
private async getLastOpIdSequence(db: lib_postgres.AbstractPostgresConnection) {
|
|
@@ -32,22 +32,43 @@ export class PostgresWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
32
32
|
return batchCreateCustomWriteCheckpoints(this.db, checkpoints);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async
|
|
35
|
+
async createManagedWriteCheckpoints(
|
|
36
|
+
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
37
|
+
): Promise<Map<string, bigint>> {
|
|
36
38
|
if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
|
|
37
39
|
throw new framework.errors.ValidationError(
|
|
38
40
|
`Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
const
|
|
44
|
+
const uniqueCheckpoints = [...new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, checkpoint])).values()];
|
|
45
|
+
if (uniqueCheckpoints.length == 0) {
|
|
46
|
+
return new Map();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mappedCheckpoints = uniqueCheckpoints.map((checkpoint) => ({
|
|
50
|
+
user_id: checkpoint.user_id,
|
|
51
|
+
lsns: checkpoint.heads
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const rows = await this.db.sql`
|
|
55
|
+
WITH
|
|
56
|
+
json_data AS (
|
|
57
|
+
SELECT
|
|
58
|
+
CHECKPOINT ->> 'user_id' AS user_id,
|
|
59
|
+
CHECKPOINT -> 'lsns' AS lsns
|
|
60
|
+
FROM
|
|
61
|
+
jsonb_array_elements(${{ type: 'jsonb', value: mappedCheckpoints }}) AS
|
|
62
|
+
CHECKPOINT
|
|
63
|
+
)
|
|
43
64
|
INSERT INTO
|
|
44
65
|
write_checkpoints (user_id, lsns, write_checkpoint)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
SELECT
|
|
67
|
+
user_id,
|
|
68
|
+
lsns,
|
|
69
|
+
1
|
|
70
|
+
FROM
|
|
71
|
+
json_data
|
|
51
72
|
ON CONFLICT (user_id) DO UPDATE
|
|
52
73
|
SET
|
|
53
74
|
write_checkpoint = write_checkpoints.write_checkpoint + 1,
|
|
@@ -56,8 +77,9 @@ export class PostgresWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
56
77
|
*;
|
|
57
78
|
`
|
|
58
79
|
.decoded(models.WriteCheckpoint)
|
|
59
|
-
.
|
|
60
|
-
|
|
80
|
+
.rows();
|
|
81
|
+
|
|
82
|
+
return new Map(rows.map((row) => [row.user_id, row.write_checkpoint]));
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
async lastWriteCheckpoint(filters: storage.LastWriteCheckpointFilters): Promise<bigint | null> {
|
package/test/src/storage.test.ts
CHANGED
|
@@ -99,7 +99,9 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
|
|
|
99
99
|
|
|
100
100
|
const options: storage.BucketDataBatchOptions = {};
|
|
101
101
|
|
|
102
|
-
const batch1 = await test_utils.fromAsync(
|
|
102
|
+
const batch1 = await test_utils.fromAsync(
|
|
103
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [globalBucket], options)
|
|
104
|
+
);
|
|
103
105
|
expect(test_utils.getBatchData(batch1)).toEqual([
|
|
104
106
|
{ op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
|
|
105
107
|
]);
|
|
@@ -111,7 +113,7 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
|
|
|
111
113
|
|
|
112
114
|
const batch2 = await test_utils.fromAsync(
|
|
113
115
|
bucketStorage.getBucketDataBatch(
|
|
114
|
-
checkpoint,
|
|
116
|
+
test_utils.testCheckpoint(checkpoint),
|
|
115
117
|
[{ ...globalBucket, start: BigInt(batch1[0].chunkData.next_after) }],
|
|
116
118
|
options
|
|
117
119
|
)
|
|
@@ -127,7 +129,7 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
|
|
|
127
129
|
|
|
128
130
|
const batch3 = await test_utils.fromAsync(
|
|
129
131
|
bucketStorage.getBucketDataBatch(
|
|
130
|
-
checkpoint,
|
|
132
|
+
test_utils.testCheckpoint(checkpoint),
|
|
131
133
|
[{ ...globalBucket, start: BigInt(batch2[0].chunkData.next_after) }],
|
|
132
134
|
options
|
|
133
135
|
)
|
|
@@ -143,7 +145,7 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
|
|
|
143
145
|
|
|
144
146
|
const batch4 = await test_utils.fromAsync(
|
|
145
147
|
bucketStorage.getBucketDataBatch(
|
|
146
|
-
checkpoint,
|
|
148
|
+
test_utils.testCheckpoint(checkpoint),
|
|
147
149
|
[{ ...globalBucket, start: BigInt(batch3[0].chunkData.next_after) }],
|
|
148
150
|
options
|
|
149
151
|
)
|
|
@@ -50,7 +50,9 @@ bucket_definitions:
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
const batch = await test_utils.oneFromAsync(
|
|
53
|
-
bucketStorage.getBucketDataBatch(checkpoint, [
|
|
53
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
|
|
54
|
+
bucketRequest(syncRulesContent, 'global[]', 0n)
|
|
55
|
+
])
|
|
54
56
|
);
|
|
55
57
|
|
|
56
58
|
expect(batch.chunkData.data).toMatchObject([
|
|
@@ -108,7 +110,9 @@ bucket_definitions:
|
|
|
108
110
|
})();
|
|
109
111
|
|
|
110
112
|
const checkpoint = result!.flushed_op;
|
|
111
|
-
const rowsBefore = await test_utils.oneFromAsync(
|
|
113
|
+
const rowsBefore = await test_utils.oneFromAsync(
|
|
114
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
|
|
115
|
+
);
|
|
112
116
|
const dataBefore = test_utils.getBatchData(rowsBefore);
|
|
113
117
|
const clearToOpId = BigInt(dataBefore[2].op_id);
|
|
114
118
|
|
|
@@ -119,7 +123,9 @@ bucket_definitions:
|
|
|
119
123
|
);
|
|
120
124
|
|
|
121
125
|
// The method wraps in a transaction; on assertion error the bucket must remain unchanged.
|
|
122
|
-
const rowsAfter = await test_utils.oneFromAsync(
|
|
126
|
+
const rowsAfter = await test_utils.oneFromAsync(
|
|
127
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [request])
|
|
128
|
+
);
|
|
123
129
|
expect(test_utils.getBatchData(rowsAfter)).toEqual(dataBefore);
|
|
124
130
|
});
|
|
125
131
|
});
|
|
@@ -90,7 +90,9 @@ function registerStorageVersionTests(storageVersion: number) {
|
|
|
90
90
|
|
|
91
91
|
const options: storage.BucketDataBatchOptions = {};
|
|
92
92
|
|
|
93
|
-
const batch1 = await test_utils.fromAsync(
|
|
93
|
+
const batch1 = await test_utils.fromAsync(
|
|
94
|
+
bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [globalBucket], options)
|
|
95
|
+
);
|
|
94
96
|
expect(test_utils.getBatchData(batch1)).toEqual([
|
|
95
97
|
{ op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
|
|
96
98
|
]);
|
|
@@ -102,7 +104,7 @@ function registerStorageVersionTests(storageVersion: number) {
|
|
|
102
104
|
|
|
103
105
|
const batch2 = await test_utils.fromAsync(
|
|
104
106
|
bucketStorage.getBucketDataBatch(
|
|
105
|
-
checkpoint,
|
|
107
|
+
test_utils.testCheckpoint(checkpoint),
|
|
106
108
|
[{ ...globalBucket, start: BigInt(batch1[0].chunkData.next_after) }],
|
|
107
109
|
options
|
|
108
110
|
)
|
|
@@ -118,7 +120,7 @@ function registerStorageVersionTests(storageVersion: number) {
|
|
|
118
120
|
|
|
119
121
|
const batch3 = await test_utils.fromAsync(
|
|
120
122
|
bucketStorage.getBucketDataBatch(
|
|
121
|
-
checkpoint,
|
|
123
|
+
test_utils.testCheckpoint(checkpoint),
|
|
122
124
|
[{ ...globalBucket, start: BigInt(batch2[0].chunkData.next_after) }],
|
|
123
125
|
options
|
|
124
126
|
)
|
|
@@ -134,7 +136,7 @@ function registerStorageVersionTests(storageVersion: number) {
|
|
|
134
136
|
|
|
135
137
|
const batch4 = await test_utils.fromAsync(
|
|
136
138
|
bucketStorage.getBucketDataBatch(
|
|
137
|
-
checkpoint,
|
|
139
|
+
test_utils.testCheckpoint(checkpoint),
|
|
138
140
|
[{ ...globalBucket, start: BigInt(batch3[0].chunkData.next_after) }],
|
|
139
141
|
options
|
|
140
142
|
)
|