@powersync/service-module-mongodb-storage 0.19.0 → 0.21.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 +64 -0
- package/dist/storage/MongoBucketStorage.d.ts +31 -3
- package/dist/storage/MongoBucketStorage.js +365 -206
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
- package/dist/storage/implementation/MongoBucketBatch.js +35 -9
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
- package/dist/storage/implementation/common/PersistedBatch.js +43 -2
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +14 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
- package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +89 -5
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +23 -1
- package/dist/types/types.js +16 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +495 -268
- package/src/storage/implementation/MongoBucketBatch.ts +54 -14
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +3 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +54 -2
- package/src/storage/implementation/db.ts +21 -2
- package/src/storage/implementation/models.ts +38 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
- package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +96 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +24 -7
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +255 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +714 -244
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +243 -1
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +365 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { addChecksums, InternalOpId } from '@powersync/service-core';
|
|
4
|
+
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
5
|
+
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
6
|
+
import { BucketDataDocumentV3, BucketStateDocumentV3 } from './models.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Perform chunk compaction if at least this many chunks have been added since
|
|
10
|
+
* the latest compaction.
|
|
11
|
+
*
|
|
12
|
+
* A lower value increases compaction frequency and can increase chunk rewrites
|
|
13
|
+
* and object-storage operations. A higher value leaves more small chunks for
|
|
14
|
+
* longer, which can hurt sync performance.
|
|
15
|
+
*/
|
|
16
|
+
const MERGE_CHUNKS_THRESHOLD = 8;
|
|
17
|
+
|
|
18
|
+
export const FULL_COMPACT_RESCHEDULE_MARGIN_MS = 60 * 1000;
|
|
19
|
+
|
|
20
|
+
export interface PendingCompactionGroup {
|
|
21
|
+
/**
|
|
22
|
+
* Input documents are ordered from oldest to newest, matching `ops`.
|
|
23
|
+
* Keeping the inputs intact lets unchanged singletons retain their object.
|
|
24
|
+
*/
|
|
25
|
+
inputs: BucketDataDocumentV3[];
|
|
26
|
+
ops: BucketDataDoc[];
|
|
27
|
+
changed: boolean;
|
|
28
|
+
targetOp: InternalOpId | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompactIntervalConfig {
|
|
32
|
+
readonly minCompactChunkIntervalMs: number;
|
|
33
|
+
readonly minCompactFullIntervalMs: number;
|
|
34
|
+
readonly maxCompactFullIntervalMs: number;
|
|
35
|
+
}
|
|
36
|
+
export interface CompactTargetConfig {
|
|
37
|
+
readonly maxOpIdCap: InternalOpId | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export enum CompactionKind {
|
|
41
|
+
Full = 'full',
|
|
42
|
+
Chunks = 'chunks'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CompactionDecision {
|
|
46
|
+
kind: CompactionKind | null;
|
|
47
|
+
nextCompactCheck: mongo.Document;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ScheduledCompactionOptions {
|
|
51
|
+
/** Process checks scheduled this far after the captured job start. */
|
|
52
|
+
dueAheadMs?: number;
|
|
53
|
+
/** Used by initial replication, which must not run a full compact. */
|
|
54
|
+
forceKind?: CompactionKind;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class CompactionContext {
|
|
58
|
+
constructor(
|
|
59
|
+
readonly lease: CompactionLease,
|
|
60
|
+
readonly kind: CompactionKind,
|
|
61
|
+
readonly decision: CompactionDecision,
|
|
62
|
+
readonly rescheduleNotBefore: Date | undefined,
|
|
63
|
+
readonly targetOp: InternalOpId
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
get state() {
|
|
67
|
+
return this.lease.state;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get startedAt() {
|
|
71
|
+
return this.lease.startedAt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get lastOp() {
|
|
75
|
+
return this.lease.lastOp;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface BucketStats {
|
|
80
|
+
count: number;
|
|
81
|
+
bytes: bigint;
|
|
82
|
+
chunks: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Bucket stats read from bucket-data documents, including their checksum. */
|
|
86
|
+
export interface BucketStatsWithChecksum extends BucketStats {
|
|
87
|
+
checksum: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CompactionResult {
|
|
91
|
+
/** Metadata cached at compacted_state.op_id. */
|
|
92
|
+
compactedState: BucketStatsWithChecksum;
|
|
93
|
+
/** Complete bucket metadata through the op head captured at claim time. */
|
|
94
|
+
bucketStats: BucketStats;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function bucketStats(state: BucketStateDocumentV3): BucketStats {
|
|
98
|
+
return {
|
|
99
|
+
count: state.bucket_stats.count,
|
|
100
|
+
bytes: state.bucket_stats.bytes,
|
|
101
|
+
chunks: state.bucket_stats.chunks
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function emptyBucketStats(): BucketStatsWithChecksum {
|
|
106
|
+
return { count: 0, bytes: 0n, chunks: 0, checksum: 0 };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function statsForDocument(
|
|
110
|
+
document: Pick<BucketDataDocumentV3, 'count' | 'size' | 'checksum'>
|
|
111
|
+
): BucketStatsWithChecksum {
|
|
112
|
+
return {
|
|
113
|
+
count: document.count,
|
|
114
|
+
bytes: BigInt(document.size),
|
|
115
|
+
chunks: 1,
|
|
116
|
+
checksum: addChecksums(0, Number(document.checksum))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function statsForDocuments(
|
|
121
|
+
documents: Iterable<Pick<BucketDataDocumentV3, 'count' | 'size' | 'checksum'>>
|
|
122
|
+
): BucketStatsWithChecksum {
|
|
123
|
+
let result = emptyBucketStats();
|
|
124
|
+
for (const document of documents) {
|
|
125
|
+
result = combineAdjacentStats(result, statsForDocument(document));
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A scheduled bucket always has writes awaiting a full compact. */
|
|
131
|
+
export function firstUncompactedWrite(state: BucketStateDocumentV3): Date {
|
|
132
|
+
if (state.first_uncompacted_write == null) {
|
|
133
|
+
throw new ReplicationAssertionError(`Scheduled V3 bucket ${state._id.b} has no first uncompacted write`);
|
|
134
|
+
}
|
|
135
|
+
return state.first_uncompacted_write;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function combineChunkStats(
|
|
139
|
+
previous: NonNullable<BucketStateDocumentV3['compacted_state']>,
|
|
140
|
+
compactedTail: BucketStatsWithChecksum,
|
|
141
|
+
overlappingCompactedChunk: BucketStatsWithChecksum
|
|
142
|
+
): BucketStatsWithChecksum {
|
|
143
|
+
return {
|
|
144
|
+
count: previous.count - overlappingCompactedChunk.count + compactedTail.count,
|
|
145
|
+
bytes: previous.bytes - overlappingCompactedChunk.bytes + compactedTail.bytes,
|
|
146
|
+
chunks: previous.chunks - 1 + compactedTail.chunks,
|
|
147
|
+
checksum: addChecksums(
|
|
148
|
+
addChecksums(Number(previous.checksum), -overlappingCompactedChunk.checksum),
|
|
149
|
+
compactedTail.checksum
|
|
150
|
+
)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function combineAdjacentStats(
|
|
155
|
+
first: BucketStatsWithChecksum,
|
|
156
|
+
second: BucketStatsWithChecksum
|
|
157
|
+
): BucketStatsWithChecksum {
|
|
158
|
+
return {
|
|
159
|
+
count: first.count + second.count,
|
|
160
|
+
bytes: first.bytes + second.bytes,
|
|
161
|
+
chunks: first.chunks + second.chunks,
|
|
162
|
+
checksum: addChecksums(first.checksum, second.checksum)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function applyCompactionDelta(
|
|
167
|
+
total: BucketStats,
|
|
168
|
+
before: BucketStatsWithChecksum,
|
|
169
|
+
after: BucketStatsWithChecksum
|
|
170
|
+
): BucketStats {
|
|
171
|
+
return {
|
|
172
|
+
count: total.count - before.count + after.count,
|
|
173
|
+
bytes: total.bytes - before.bytes + after.bytes,
|
|
174
|
+
chunks: total.chunks - before.chunks + after.chunks
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Replace one stored range in an accumulated statistics snapshot. */
|
|
179
|
+
export function applyStatsReplacement(
|
|
180
|
+
total: BucketStatsWithChecksum,
|
|
181
|
+
before: BucketStatsWithChecksum,
|
|
182
|
+
after: BucketStatsWithChecksum
|
|
183
|
+
): BucketStatsWithChecksum {
|
|
184
|
+
return {
|
|
185
|
+
...applyCompactionDelta(total, before, after),
|
|
186
|
+
checksum: addChecksums(addChecksums(total.checksum, -before.checksum), after.checksum)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* This checks that the bucket state hasn't changed by a concurrent write since we checked.
|
|
192
|
+
*
|
|
193
|
+
* If it has changed, we'll re-check in the next batch.
|
|
194
|
+
*/
|
|
195
|
+
export function unclaimedSnapshotFilter(state: BucketStateDocumentV3): mongo.Filter<BucketStateDocumentV3> {
|
|
196
|
+
return {
|
|
197
|
+
_id: state._id,
|
|
198
|
+
last_op: state.last_op,
|
|
199
|
+
next_compact_check: state.next_compact_check,
|
|
200
|
+
first_uncompacted_write: state.first_uncompacted_write ?? { $exists: false },
|
|
201
|
+
bucket_stats: state.bucket_stats,
|
|
202
|
+
compacted_state: state.compacted_state ?? { $exists: false },
|
|
203
|
+
last_full_compact: state.last_full_compact ?? { $exists: false },
|
|
204
|
+
...AVAILABLE_LEASE_EXPR
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Calculate the earliest full compaction time from the first uncompacted
|
|
210
|
+
* write, bounded by the maximum retention interval.
|
|
211
|
+
*/
|
|
212
|
+
export function fullCompactionCheckAt(state: BucketStateDocumentV3, config: CompactIntervalConfig): Date {
|
|
213
|
+
const firstWrite = firstUncompactedWrite(state);
|
|
214
|
+
const stats = bucketStats(state);
|
|
215
|
+
const lastFull = state.last_full_compact;
|
|
216
|
+
|
|
217
|
+
// The number of operations since the last full compact.
|
|
218
|
+
// We may make this more specific in the future, to track new updates and deletes only, ignoring
|
|
219
|
+
// full new inserts, but that requires more granular tracking when replicating.
|
|
220
|
+
const uncompactedCount = lastFull == null ? stats.count : Math.max(0, stats.count - lastFull.count);
|
|
221
|
+
const compactedRows = lastFull?.puts ?? 0;
|
|
222
|
+
|
|
223
|
+
// If no full compact has ever been performed: ratio = 1, compact after minCompactFullIntervalMs.
|
|
224
|
+
// If every row has been updated or deleted exactly once since the last full compact: ratio = 0.5, compact after minCompactFullIntervalMs * 2.
|
|
225
|
+
// If 10% of rows has been updated since the last full compact, compact after minCompactFullIntervalMs * 11.
|
|
226
|
+
// If every row has been updated multiple times, the ratio tends closer to 1 again.
|
|
227
|
+
const ratio = uncompactedCount == 0 ? 0 : uncompactedCount / (compactedRows + uncompactedCount);
|
|
228
|
+
const fullIntervalMs = ratio == 0 ? config.maxCompactFullIntervalMs : config.minCompactFullIntervalMs / ratio;
|
|
229
|
+
return new Date(firstWrite.getTime() + Math.min(fullIntervalMs, config.maxCompactFullIntervalMs));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function chooseCompactionKind(
|
|
233
|
+
state: BucketStateDocumentV3,
|
|
234
|
+
now: Date,
|
|
235
|
+
config: CompactIntervalConfig
|
|
236
|
+
): CompactionDecision {
|
|
237
|
+
// For chunk compaction, we consider the number of chunks added.
|
|
238
|
+
// Right now, we trigger a compact if the interval has passed and at least a threshold of chunks were added.
|
|
239
|
+
// A future policy could also use bytes per chunk or records per chunk to
|
|
240
|
+
// decide when to compact.
|
|
241
|
+
const fullCheckAt = fullCompactionCheckAt(state, config);
|
|
242
|
+
// Schedule a little late so a worker using a slightly earlier clock does
|
|
243
|
+
// not wake before the exact full-compaction condition is true.
|
|
244
|
+
const fullCheckWithMargin = new Date(fullCheckAt.getTime() + FULL_COMPACT_RESCHEDULE_MARGIN_MS);
|
|
245
|
+
const compacted = state.compacted_state;
|
|
246
|
+
const chunksSinceCompact = Math.max(0, state.bucket_stats.chunks - (compacted?.chunks ?? 0));
|
|
247
|
+
const shouldCompactChunks = chunksSinceCompact >= MERGE_CHUNKS_THRESHOLD;
|
|
248
|
+
const canCheckChunks =
|
|
249
|
+
compacted == null || now.getTime() - compacted.at.getTime() >= config.minCompactChunkIntervalMs;
|
|
250
|
+
// Too few new chunks cannot make chunk compaction eligible. Do not poll this
|
|
251
|
+
// bucket at the chunk-compaction interval; only wake it for its full-compact check.
|
|
252
|
+
const nextCompactCheck: mongo.Document = !shouldCompactChunks
|
|
253
|
+
? fullCheckWithMargin
|
|
254
|
+
: {
|
|
255
|
+
$min: [
|
|
256
|
+
fullCheckWithMargin,
|
|
257
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: config.minCompactChunkIntervalMs } }
|
|
258
|
+
]
|
|
259
|
+
};
|
|
260
|
+
let kind: CompactionKind | null = null;
|
|
261
|
+
if (now >= fullCheckAt) {
|
|
262
|
+
kind = CompactionKind.Full;
|
|
263
|
+
} else if (canCheckChunks && shouldCompactChunks) {
|
|
264
|
+
kind = CompactionKind.Chunks;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
kind,
|
|
268
|
+
nextCompactCheck
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function forcedCompactionKind(
|
|
273
|
+
state: BucketStateDocumentV3,
|
|
274
|
+
forceKind: CompactionKind | undefined,
|
|
275
|
+
config: CompactTargetConfig
|
|
276
|
+
): CompactionKind | null {
|
|
277
|
+
if (forceKind == null) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const maxOpId = config.maxOpIdCap == null || state.last_op < config.maxOpIdCap ? state.last_op : config.maxOpIdCap;
|
|
281
|
+
return state.compacted_state == null || state.compacted_state.op_id < maxOpId ? forceKind : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Read one bounded prefix from a compaction cursor.
|
|
286
|
+
*
|
|
287
|
+
* The document that would cross the byte limit is deliberately not returned:
|
|
288
|
+
* pagination resumes past the last returned `_id`, so that document remains
|
|
289
|
+
* eligible for the next query. The first document is always accepted to
|
|
290
|
+
* ensure progress when a single document exceeds the configured byte limit.
|
|
291
|
+
*
|
|
292
|
+
* `hasMore` is conservative when the document limit is reached. An extra empty
|
|
293
|
+
* query is preferable to exhausting the cursor just to determine whether the
|
|
294
|
+
* limited MongoDB query contained another document.
|
|
295
|
+
*/
|
|
296
|
+
export async function readCompactionBatch(
|
|
297
|
+
cursor: mongo.AggregationCursor<BucketDataDocumentV3>,
|
|
298
|
+
options: { byteLimit: number; documentLimit: number }
|
|
299
|
+
): Promise<{ documents: BucketDataDocumentV3[]; hasMore: boolean }> {
|
|
300
|
+
const documents: BucketDataDocumentV3[] = [];
|
|
301
|
+
let cumulativeBytes = 0;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
for await (const document of cursor) {
|
|
305
|
+
if (documents.length > 0 && cumulativeBytes + document.size > options.byteLimit) {
|
|
306
|
+
return { documents, hasMore: true };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
documents.push(document);
|
|
310
|
+
cumulativeBytes += document.size;
|
|
311
|
+
|
|
312
|
+
if (documents.length >= options.documentLimit) {
|
|
313
|
+
return { documents, hasMore: true };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { documents, hasMore: false };
|
|
317
|
+
} finally {
|
|
318
|
+
await cursor.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InternalOpId,
|
|
3
|
+
JsonValue,
|
|
3
4
|
PersistedDefinitionMapping,
|
|
4
5
|
SerializedSyncPlan,
|
|
5
6
|
SyncRuleState,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
} from '@powersync/service-core';
|
|
8
9
|
import {
|
|
9
10
|
BucketDefinitionId,
|
|
11
|
+
EventDefinitionId,
|
|
10
12
|
ParameterIndexId,
|
|
11
13
|
ScopedParameterLookup,
|
|
12
14
|
SqliteJsonValue
|
|
@@ -15,7 +17,6 @@ import * as bson from 'bson';
|
|
|
15
17
|
import {
|
|
16
18
|
BucketDataKey,
|
|
17
19
|
BucketParameterDocumentBase,
|
|
18
|
-
BucketStateDocumentBase,
|
|
19
20
|
CurrentBucket,
|
|
20
21
|
OpType,
|
|
21
22
|
ReplicaId,
|
|
@@ -103,6 +104,8 @@ export interface SyncConfigDefinition {
|
|
|
103
104
|
replication_stream_id: number;
|
|
104
105
|
|
|
105
106
|
content: string;
|
|
107
|
+
/** Optional operator-supplied label identifying this sync config version. */
|
|
108
|
+
version_label?: string;
|
|
106
109
|
serialized_plan?: SerializedSyncPlan | null;
|
|
107
110
|
|
|
108
111
|
rule_mapping: PersistedDefinitionMapping;
|
|
@@ -156,13 +159,68 @@ export interface SourceTableDocumentV3 {
|
|
|
156
159
|
snapshot_status: SourceTableDocumentSnapshotStatus | undefined;
|
|
157
160
|
bucket_data_source_ids: BucketDefinitionId[];
|
|
158
161
|
parameter_lookup_source_ids: ParameterIndexId[];
|
|
162
|
+
/**
|
|
163
|
+
* Stream-local ids of the compiled event definitions evaluated by this source table.
|
|
164
|
+
*/
|
|
165
|
+
event_definition_ids: EventDefinitionId[];
|
|
159
166
|
latest_pending_delete?: InternalOpId | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Source-specific metadata. Absent for legacy records.
|
|
169
|
+
*/
|
|
170
|
+
source_metadata?: JsonValue;
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
export interface BucketStateDocumentV3
|
|
163
|
-
_id:
|
|
173
|
+
export interface BucketStateDocumentV3 {
|
|
174
|
+
_id: {
|
|
175
|
+
b: string;
|
|
164
176
|
d: BucketDefinitionId;
|
|
165
177
|
};
|
|
178
|
+
|
|
179
|
+
/** Must always identify an actual operation in this logical stream. */
|
|
180
|
+
last_op: bigint;
|
|
181
|
+
|
|
182
|
+
/** The next time a compact worker should inspect this bucket. */
|
|
183
|
+
next_compact_check: Date | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Scheduling epoch for work not covered by a full compact. Writers set the
|
|
186
|
+
* first actual write time; a partial full compact advances it to completion.
|
|
187
|
+
*/
|
|
188
|
+
first_uncompacted_write: Date | undefined;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A checksum cache and the statistics captured by the latest compact (full
|
|
192
|
+
* or chunk). Keeping these separate from bucket_stats lets writers only
|
|
193
|
+
* update one set of counters.
|
|
194
|
+
*/
|
|
195
|
+
compacted_state?: {
|
|
196
|
+
op_id: InternalOpId;
|
|
197
|
+
checksum: bigint;
|
|
198
|
+
count: number;
|
|
199
|
+
bytes: bigint;
|
|
200
|
+
at: Date;
|
|
201
|
+
chunks: number;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Statistics for the prefix covered by the most recent full compact. */
|
|
205
|
+
last_full_compact?: {
|
|
206
|
+
op_id: InternalOpId;
|
|
207
|
+
count: number;
|
|
208
|
+
at: Date;
|
|
209
|
+
puts: number;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** Current aggregate bucket statistics, maintained by writers and compactors. */
|
|
213
|
+
bucket_stats: {
|
|
214
|
+
count: number;
|
|
215
|
+
bytes: bigint;
|
|
216
|
+
chunks: number;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** A short-lived ownership marker used to distribute bucket compaction. */
|
|
220
|
+
compact_lease?: {
|
|
221
|
+
expires_at: Date;
|
|
222
|
+
id: unknown;
|
|
223
|
+
};
|
|
166
224
|
}
|
|
167
225
|
|
|
168
226
|
export interface BucketOperation {
|
|
@@ -170,6 +228,7 @@ export interface BucketOperation {
|
|
|
170
228
|
op: OpType;
|
|
171
229
|
source_table?: bson.ObjectId;
|
|
172
230
|
source_key?: ReplicaId;
|
|
231
|
+
subkey?: string;
|
|
173
232
|
table?: string;
|
|
174
233
|
row_id?: string;
|
|
175
234
|
checksum: bigint;
|
|
@@ -181,6 +240,15 @@ export interface StorageRef {
|
|
|
181
240
|
file_size: number;
|
|
182
241
|
}
|
|
183
242
|
|
|
243
|
+
export interface ObjectStorageUsageDocument {
|
|
244
|
+
_id: {
|
|
245
|
+
g: number;
|
|
246
|
+
w: string;
|
|
247
|
+
};
|
|
248
|
+
updated_at: Date;
|
|
249
|
+
definitions: Record<BucketDefinitionId, bigint>;
|
|
250
|
+
}
|
|
251
|
+
|
|
184
252
|
/** An S3 object that may be deleted once its grace period has elapsed. */
|
|
185
253
|
export interface ObjectStorageDeletionMarker {
|
|
186
254
|
_id: bson.ObjectId;
|
|
@@ -225,6 +293,31 @@ export interface BucketDataDocumentV3 {
|
|
|
225
293
|
has_clear_op?: true;
|
|
226
294
|
}
|
|
227
295
|
|
|
296
|
+
/**
|
|
297
|
+
* v3 Custom checkpoints (new name for legacy write checkpoints) are related to
|
|
298
|
+
* a replication stream and a specific event (in a sync config) which created them.
|
|
299
|
+
* Unchanged events, between sync configs, can share the documents.
|
|
300
|
+
* For v3, we store each replication stream's custom checkpoint requests in their own collection:
|
|
301
|
+
* `custom_checkpoint_requests_${replicationStreamId}_${eventId}`
|
|
302
|
+
*/
|
|
303
|
+
export interface CustomCheckpointRequestDocumentV3 {
|
|
304
|
+
_id: bson.ObjectId;
|
|
305
|
+
user_id: string;
|
|
306
|
+
checkpoint: bigint;
|
|
307
|
+
/**
|
|
308
|
+
* Unlike managed write checkpoints, custom write checkpoints are flushed together with
|
|
309
|
+
* normal ops. This means we can assign an op_id for ordering / correlating with read checkpoints.
|
|
310
|
+
*
|
|
311
|
+
* This is not unique - multiple write checkpoints can have the same op_id.
|
|
312
|
+
*/
|
|
313
|
+
op_id?: InternalOpId;
|
|
314
|
+
/**
|
|
315
|
+
* Set when this checkpoint was created from a client-supplied checkpoint
|
|
316
|
+
* request rather than a persistent custom write checkpoint.
|
|
317
|
+
*/
|
|
318
|
+
checkpoint_requested_at?: Date | null;
|
|
319
|
+
}
|
|
320
|
+
|
|
228
321
|
export function serializeParameterLookup(lookup: ScopedParameterLookup): bson.Binary {
|
|
229
322
|
return new bson.Binary(bson.serialize({ l: lookup.values.slice(2) }));
|
|
230
323
|
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import { storage } from '@powersync/service-core';
|
|
1
|
+
import { PerformanceTracer, storage } from '@powersync/service-core';
|
|
2
2
|
import * as bson from 'bson';
|
|
3
3
|
import { BucketDataDocumentV3, BucketOperation } from '../models.js';
|
|
4
|
-
import { ObjectStorage } from './ObjectStorage.js';
|
|
4
|
+
import { ObjectStorage, ObjectStorageOperationOptions } from './ObjectStorage.js';
|
|
5
5
|
|
|
6
6
|
export class BucketDataObjectStorage {
|
|
7
7
|
constructor(private readonly storage: ObjectStorage) {}
|
|
8
8
|
|
|
9
|
-
async store(
|
|
9
|
+
async store(
|
|
10
|
+
path: string,
|
|
11
|
+
ops: BucketOperation[],
|
|
12
|
+
options?: ObjectStorageOperationOptions
|
|
13
|
+
): Promise<{ fileSize: number }> {
|
|
10
14
|
const bsonBuffer = bson.serialize({ ops });
|
|
11
|
-
await this.storage.put(path, bsonBuffer, { contentType: 'application/bson', contentEncoding: null });
|
|
15
|
+
await this.storage.put(path, bsonBuffer, { contentType: 'application/bson', contentEncoding: null }, options);
|
|
12
16
|
return { fileSize: bsonBuffer.byteLength };
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
async retrieve(path: string, options:
|
|
19
|
+
async retrieve(path: string, options: ObjectStorageOperationOptions): Promise<BucketOperation[]> {
|
|
16
20
|
const { data, metadata } = await this.storage.get(path, { signal: options.signal });
|
|
17
21
|
if (metadata.contentEncoding != null) {
|
|
18
22
|
throw new Error(`Unexpected content encoding: ${metadata.contentEncoding}`);
|
|
@@ -24,8 +28,8 @@ export class BucketDataObjectStorage {
|
|
|
24
28
|
return wrapper.ops;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
async delete(paths: string[]): Promise<void> {
|
|
28
|
-
return this.storage.delete(paths);
|
|
31
|
+
async delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void> {
|
|
32
|
+
return this.storage.delete(paths, options);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -37,7 +41,7 @@ export class BucketDataObjectStorage {
|
|
|
37
41
|
export async function hydrateBucketDataDocuments(
|
|
38
42
|
documents: BucketDataDocumentV3[],
|
|
39
43
|
objectStorage: ObjectStorage | undefined,
|
|
40
|
-
options: {
|
|
44
|
+
options: ObjectStorageOperationOptions & { tracer?: PerformanceTracer<string> }
|
|
41
45
|
): Promise<void> {
|
|
42
46
|
if (!objectStorage) {
|
|
43
47
|
return;
|
|
@@ -46,6 +50,10 @@ export async function hydrateBucketDataDocuments(
|
|
|
46
50
|
options.signal?.throwIfAborted();
|
|
47
51
|
const store = new BucketDataObjectStorage(objectStorage);
|
|
48
52
|
const storedDocuments = documents.filter((document) => document.storage_ref);
|
|
53
|
+
if (storedDocuments.length == 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
using _ = options?.tracer?.span('s3', 'read');
|
|
49
57
|
await Promise.all(
|
|
50
58
|
storedDocuments.map(async (document) => {
|
|
51
59
|
document.ops = await store.retrieve(document.storage_ref!.path, { signal: options.signal });
|
|
@@ -17,13 +17,28 @@ export function isRetryableObjectStorageError(error: unknown): error is ObjectSt
|
|
|
17
17
|
return error instanceof ObjectStorageError && error.retryable;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface ObjectStorageOperationOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Cancels the operation, including while it waits for a concurrency slot.
|
|
23
|
+
*
|
|
24
|
+
* Operations are bounded by their own timeouts regardless, but a signal releases the slot as
|
|
25
|
+
* soon as the caller no longer needs the result.
|
|
26
|
+
*/
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export interface ObjectStorage {
|
|
21
|
-
put(
|
|
31
|
+
put(
|
|
32
|
+
path: string,
|
|
33
|
+
data: Uint8Array,
|
|
34
|
+
metadata: ObjectStoragePutMetadata,
|
|
35
|
+
options?: ObjectStorageOperationOptions
|
|
36
|
+
): Promise<void>;
|
|
22
37
|
get(
|
|
23
38
|
path: string,
|
|
24
|
-
options?:
|
|
39
|
+
options?: ObjectStorageOperationOptions
|
|
25
40
|
): Promise<{ data: Uint8Array; metadata: ObjectStoragePutMetadata }>;
|
|
26
|
-
list(prefix: string, options?:
|
|
27
|
-
delete(paths: string[]): Promise<void>;
|
|
28
|
-
deletePrefix(prefix: string, options?:
|
|
41
|
+
list(prefix: string, options?: ObjectStorageOperationOptions): AsyncIterable<string>;
|
|
42
|
+
delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void>;
|
|
43
|
+
deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }>;
|
|
29
44
|
}
|
|
@@ -5,7 +5,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { ObjectStorageDeletionMarker } from '../models.js';
|
|
6
6
|
import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
|
|
7
7
|
import { BucketDataObjectStorage } from './BucketDataObjectStorage.js';
|
|
8
|
-
import { ObjectStorage } from './ObjectStorage.js';
|
|
8
|
+
import { ObjectStorage, ObjectStorageOperationOptions } from './ObjectStorage.js';
|
|
9
9
|
|
|
10
10
|
export const OBJECT_STORAGE_UPLOAD_LEASE_MS = 60 * 60 * 1000;
|
|
11
11
|
export const OBJECT_STORAGE_PUBLICATION_SAFETY_MARGIN_MS = 2 * 60 * 1000;
|
|
@@ -137,11 +137,12 @@ export class ObjectStorageLifecycle {
|
|
|
137
137
|
* Discover and delete objects by their storage prefix
|
|
138
138
|
* without touching the database.
|
|
139
139
|
*/
|
|
140
|
-
async deletePrefix(prefix: string, options?:
|
|
140
|
+
async deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }> {
|
|
141
141
|
return this.objectStorage.deletePrefix(prefix, { signal: options?.signal });
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
async cleanup(logger: Logger): Promise<void> {
|
|
144
|
+
async cleanup(logger: Logger, options?: ObjectStorageOperationOptions): Promise<void> {
|
|
145
|
+
const signal = options?.signal;
|
|
145
146
|
const markers = this.db.pendingObjectStorageDeletes(this.replicationStreamId);
|
|
146
147
|
const cursor = markers.find({ delete_after: { $lte: new Date() } });
|
|
147
148
|
let batch: ObjectStorageDeletionMarker[] = [];
|
|
@@ -153,14 +154,24 @@ export class ObjectStorageLifecycle {
|
|
|
153
154
|
const deleting = batch;
|
|
154
155
|
batch = [];
|
|
155
156
|
try {
|
|
156
|
-
await this.bucketData.delete(
|
|
157
|
+
await this.bucketData.delete(
|
|
158
|
+
deleting.map((marker) => marker.path),
|
|
159
|
+
{ signal }
|
|
160
|
+
);
|
|
157
161
|
await markers.deleteMany({ _id: { $in: deleting.map((marker) => marker._id) } });
|
|
158
162
|
} catch (error) {
|
|
163
|
+
if (signal?.aborted) {
|
|
164
|
+
// Shutting down: stop instead of working through the remaining markers. They stay
|
|
165
|
+
// pending, and are cleaned up during the next compaction. Checked on the signal rather
|
|
166
|
+
// than the error, since a caller may abort with any reason.
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
159
169
|
logger.warn('Failed to clean up object storage deletion markers; will retry during the next compaction', error);
|
|
160
170
|
}
|
|
161
171
|
};
|
|
162
172
|
|
|
163
173
|
for await (const marker of cursor) {
|
|
174
|
+
signal?.throwIfAborted();
|
|
164
175
|
batch.push(marker);
|
|
165
176
|
if (batch.length === 500) {
|
|
166
177
|
await deleteBatch();
|