diff --git a/.changeset/funny-pianos-allow.md b/.changeset/funny-pianos-allow.md new file mode 100644 index 000000000..3df90e3bc --- /dev/null +++ b/.changeset/funny-pianos-allow.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-core': patch +--- + +Fix "operation exceeded time limit" error diff --git a/.changeset/green-books-shout.md b/.changeset/green-books-shout.md new file mode 100644 index 000000000..1b2ef8139 --- /dev/null +++ b/.changeset/green-books-shout.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-core': patch +--- + +Fix storageStats error in metrics endpoint when collections don't exist. diff --git a/.changeset/short-bats-sin.md b/.changeset/short-bats-sin.md deleted file mode 100644 index a4d78cd5a..000000000 --- a/.changeset/short-bats-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@powersync/service-sync-rules': minor ---- - -Warn when identifiers are automatically convererted to lower case. diff --git a/.changeset/stupid-maps-buy.md b/.changeset/stupid-maps-buy.md deleted file mode 100644 index 79edadd48..000000000 --- a/.changeset/stupid-maps-buy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@powersync/service-core': patch -'@powersync/service-image': patch ---- - -Fix checksum cache edge case with compacting diff --git a/.changeset/wet-gorillas-remember.md b/.changeset/wet-gorillas-remember.md deleted file mode 100644 index d80afc7e1..000000000 --- a/.changeset/wet-gorillas-remember.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@powersync/service-sync-rules': minor ---- - -Expand supported combinations of the IN operator diff --git a/.changeset/young-rockets-behave.md b/.changeset/young-rockets-behave.md deleted file mode 100644 index e58472781..000000000 --- a/.changeset/young-rockets-behave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@powersync/service-core': patch ---- - -Fix "JavaScript heap out of memory" on startup (slot health check) diff --git a/packages/service-core/CHANGELOG.md b/packages/service-core/CHANGELOG.md index 355a2301b..e76e019d4 100644 --- a/packages/service-core/CHANGELOG.md +++ b/packages/service-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @powersync/service-core +## 0.8.5 + +### Patch Changes + +- 1fd50a5: Fix checksum cache edge case with compacting +- aa4eb0a: Fix "JavaScript heap out of memory" on startup (slot health check) +- Updated dependencies [9e78ff1] +- Updated dependencies [0e16938] + - @powersync/service-sync-rules@0.19.0 + ## 0.8.4 ### Patch Changes diff --git a/packages/service-core/package.json b/packages/service-core/package.json index 9001441b9..b2fe5b340 100644 --- a/packages/service-core/package.json +++ b/packages/service-core/package.json @@ -5,7 +5,7 @@ "publishConfig": { "access": "public" }, - "version": "0.8.4", + "version": "0.8.5", "main": "dist/index.js", "license": "FSL-1.1-Apache-2.0", "type": "module", diff --git a/packages/service-core/src/db/mongo.ts b/packages/service-core/src/db/mongo.ts index 33d6d5b86..f687705bd 100644 --- a/packages/service-core/src/db/mongo.ts +++ b/packages/service-core/src/db/mongo.ts @@ -23,6 +23,13 @@ export const MONGO_SOCKET_TIMEOUT_MS = 60_000; */ export const MONGO_OPERATION_TIMEOUT_MS = 30_000; +/** + * Same as above, but specifically for clear operations. + * + * These are retried when reaching the timeout. + */ +export const MONGO_CLEAR_OPERATION_TIMEOUT_MS = 5_000; + export function createMongoClient(config: configFile.PowerSyncConfig['storage']) { const normalized = normalizeMongoConfig(config); return new mongo.MongoClient(normalized.uri, { diff --git a/packages/service-core/src/storage/BucketStorage.ts b/packages/service-core/src/storage/BucketStorage.ts index 9c12e805c..84165fb7c 100644 --- a/packages/service-core/src/storage/BucketStorage.ts +++ b/packages/service-core/src/storage/BucketStorage.ts @@ -265,8 +265,6 @@ export interface SyncRulesBucketStorage extends DisposableObserverClient; - setSnapshotDone(lsn: string): Promise; - autoActivate(): Promise; /** diff --git a/packages/service-core/src/storage/MongoBucketStorage.ts b/packages/service-core/src/storage/MongoBucketStorage.ts index c0254da71..9da62758c 100644 --- a/packages/service-core/src/storage/MongoBucketStorage.ts +++ b/packages/service-core/src/storage/MongoBucketStorage.ts @@ -331,39 +331,56 @@ export class MongoBucketStorage } async getStorageMetrics(): Promise { + const ignoreNotExiting = (e: unknown) => { + if (e instanceof mongo.MongoServerError && e.codeName == 'NamespaceNotFound') { + // Collection doesn't exist - return 0 + return [{ storageStats: { size: 0 } }]; + } else { + return Promise.reject(e); + } + }; + + const active_sync_rules = await this.getActiveSyncRules({ defaultSchema: 'public' }); + if (active_sync_rules == null) { + return { + operations_size_bytes: 0, + parameters_size_bytes: 0, + replication_size_bytes: 0 + }; + } const operations_aggregate = await this.db.bucket_data .aggregate([ { $collStats: { - storageStats: {}, - count: {} + storageStats: {} } } ]) - .toArray(); + .toArray() + .catch(ignoreNotExiting); const parameters_aggregate = await this.db.bucket_parameters .aggregate([ { $collStats: { - storageStats: {}, - count: {} + storageStats: {} } } ]) - .toArray(); + .toArray() + .catch(ignoreNotExiting); const replication_aggregate = await this.db.current_data .aggregate([ { $collStats: { - storageStats: {}, - count: {} + storageStats: {} } } ]) - .toArray(); + .toArray() + .catch(ignoreNotExiting); return { operations_size_bytes: operations_aggregate[0].storageStats.size, diff --git a/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts b/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts index 71ab4a531..e4b57788e 100644 --- a/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts +++ b/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts @@ -31,6 +31,8 @@ import { BucketDataDocument, BucketDataKey, SourceKey, SyncRuleState } from './m import { MongoBucketBatch } from './MongoBucketBatch.js'; import { MongoCompactor } from './MongoCompactor.js'; import { BSON_DESERIALIZE_OPTIONS, idPrefixFilter, mapOpEntry, readSingleBatch, serializeLookup } from './util.js'; +import { logger } from '@powersync/lib-services-framework'; +import * as timers from 'timers/promises'; export class MongoSyncBucketStorage extends DisposableObserver @@ -459,10 +461,28 @@ export class MongoSyncBucketStorage } async clear(): Promise { + while (true) { + try { + await this.clearIteration(); + return; + } catch (e: unknown) { + if (e instanceof mongo.MongoServerError && e.codeName == 'MaxTimeMSExpired') { + logger.info( + `Clearing took longer than ${db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, waiting and triggering another iteration.` + ); + await timers.setTimeout(db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5); + continue; + } else { + throw e; + } + } + } + } + + private async clearIteration(): Promise { // Individual operations here may time out with the maxTimeMS option. // It is expected to still make progress, and continue on the next try. - // TODO: Transactional? await this.db.sync_rules.updateOne( { _id: this.group_id @@ -476,48 +496,33 @@ export class MongoSyncBucketStorage no_checkpoint_before: null } }, - { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS } + { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS } ); await this.db.bucket_data.deleteMany( { _id: idPrefixFilter({ g: this.group_id }, ['b', 'o']) }, - { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS } + { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS } ); await this.db.bucket_parameters.deleteMany( { key: idPrefixFilter({ g: this.group_id }, ['t', 'k']) }, - { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS } + { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS } ); await this.db.current_data.deleteMany( { _id: idPrefixFilter({ g: this.group_id }, ['t', 'k']) }, - { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS } + { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS } ); await this.db.source_tables.deleteMany( { group_id: this.group_id }, - { maxTimeMS: db.mongo.MONGO_OPERATION_TIMEOUT_MS } - ); - } - - async setSnapshotDone(lsn: string): Promise { - await this.db.sync_rules.updateOne( - { - _id: this.group_id - }, - { - $set: { - snapshot_done: true, - persisted_lsn: lsn, - last_checkpoint_ts: new Date() - } - } + { maxTimeMS: db.mongo.MONGO_CLEAR_OPERATION_TIMEOUT_MS } ); } diff --git a/packages/service-core/src/storage/mongo/db.ts b/packages/service-core/src/storage/mongo/db.ts index dddfdf918..99bad0948 100644 --- a/packages/service-core/src/storage/mongo/db.ts +++ b/packages/service-core/src/storage/mongo/db.ts @@ -62,6 +62,9 @@ export class PowerSyncMongo { this.locks = this.db.collection('locks'); } + /** + * Clear all collections. + */ async clear() { await this.current_data.deleteMany({}); await this.bucket_data.deleteMany({}); @@ -73,4 +76,13 @@ export class PowerSyncMongo { await this.instance.deleteOne({}); await this.locks.deleteMany({}); } + + /** + * Drop the entire database. + * + * Primarily for tests. + */ + async drop() { + await this.db.dropDatabase(); + } } diff --git a/packages/service-core/test/src/data_storage.test.ts b/packages/service-core/test/src/data_storage.test.ts index 8eceacf40..05885b704 100644 --- a/packages/service-core/test/src/data_storage.test.ts +++ b/packages/service-core/test/src/data_storage.test.ts @@ -1438,4 +1438,26 @@ bucket_definitions: expect(errorCaught).true; expect(isDisposed).true; }); + + test('empty storage metrics', async () => { + const f = await factory({ dropAll: true }); + + const metrics = await f.getStorageMetrics(); + expect(metrics).toEqual({ + operations_size_bytes: 0, + parameters_size_bytes: 0, + replication_size_bytes: 0 + }); + + const r = await f.configureSyncRules('bucket_definitions: {}'); + const storage = f.getInstance(r.persisted_sync_rules!); + await storage.autoActivate(); + + const metrics2 = await f.getStorageMetrics(); + expect(metrics2).toEqual({ + operations_size_bytes: 0, + parameters_size_bytes: 0, + replication_size_bytes: 0 + }); + }); } diff --git a/packages/service-core/test/src/sync.test.ts b/packages/service-core/test/src/sync.test.ts index f1c4c7272..606647038 100644 --- a/packages/service-core/test/src/sync.test.ts +++ b/packages/service-core/test/src/sync.test.ts @@ -5,15 +5,7 @@ import { JSONBig } from '@powersync/service-jsonbig'; import { RequestParameters } from '@powersync/service-sync-rules'; import * as timers from 'timers/promises'; import { describe, expect, test } from 'vitest'; -import { - BATCH_OPTIONS, - makeTestTable, - MONGO_STORAGE_FACTORY, - PARSE_OPTIONS, - StorageFactory, - ZERO_LSN -} from './util.js'; -import { ParseSyncRulesOptions, StartBatchOptions } from '@/storage/BucketStorage.js'; +import { BATCH_OPTIONS, makeTestTable, MONGO_STORAGE_FACTORY, PARSE_OPTIONS, StorageFactory } from './util.js'; describe('sync - mongodb', function () { defineTests(MONGO_STORAGE_FACTORY); @@ -38,8 +30,7 @@ function defineTests(factory: StorageFactory) { content: BASIC_SYNC_RULES }); - const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); + const storage = f.getInstance(syncRules); await storage.autoActivate(); const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => { @@ -91,7 +82,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => { @@ -136,7 +126,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const stream = streamResponse({ @@ -164,7 +153,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const stream = streamResponse({ @@ -226,7 +214,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const exp = Date.now() / 1000 + 0.1; @@ -265,7 +252,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); await storage.startBatch(BATCH_OPTIONS, async (batch) => { diff --git a/packages/service-core/test/src/util.ts b/packages/service-core/test/src/util.ts index cd8c06f2c..98cd613e2 100644 --- a/packages/service-core/test/src/util.ts +++ b/packages/service-core/test/src/util.ts @@ -24,11 +24,22 @@ await Metrics.initialise({ }); Metrics.getInstance().resetCounters(); -export type StorageFactory = () => Promise; +export interface StorageOptions { + /** + * By default, collections are only cleared/ + * Setting this to true will drop the collections completely. + */ + dropAll?: boolean; +} +export type StorageFactory = (options?: StorageOptions) => Promise; -export const MONGO_STORAGE_FACTORY: StorageFactory = async () => { +export const MONGO_STORAGE_FACTORY: StorageFactory = async (options?: StorageOptions) => { const db = await connectMongo(); - await db.clear(); + if (options?.dropAll) { + await db.drop(); + } else { + await db.clear(); + } return new MongoBucketStorage(db, { slot_name_prefix: 'test_' }); }; diff --git a/packages/sync-rules/CHANGELOG.md b/packages/sync-rules/CHANGELOG.md index 8045fd193..099bf11db 100644 --- a/packages/sync-rules/CHANGELOG.md +++ b/packages/sync-rules/CHANGELOG.md @@ -1,5 +1,12 @@ # @powersync/service-sync-rules +## 0.19.0 + +### Minor Changes + +- 9e78ff1: Warn when identifiers are automatically convererted to lower case. +- 0e16938: Expand supported combinations of the IN operator + ## 0.18.3 ### Patch Changes diff --git a/packages/sync-rules/package.json b/packages/sync-rules/package.json index ffe53c975..bb942b1e8 100644 --- a/packages/sync-rules/package.json +++ b/packages/sync-rules/package.json @@ -1,7 +1,7 @@ { "name": "@powersync/service-sync-rules", "repository": "https://github.com/powersync-ja/powersync-service", - "version": "0.18.3", + "version": "0.19.0", "main": "dist/index.js", "types": "dist/index.d.ts", "license": "FSL-1.1-Apache-2.0", diff --git a/service/CHANGELOG.md b/service/CHANGELOG.md index 0dc13b29b..6c12e7529 100644 --- a/service/CHANGELOG.md +++ b/service/CHANGELOG.md @@ -1,5 +1,17 @@ # @powersync/service-image +## 0.5.5 + +### Patch Changes + +- 1fd50a5: Fix checksum cache edge case with compacting +- Updated dependencies [9e78ff1] +- Updated dependencies [1fd50a5] +- Updated dependencies [0e16938] +- Updated dependencies [aa4eb0a] + - @powersync/service-sync-rules@0.19.0 + - @powersync/service-core@0.8.5 + ## 0.5.4 ### Patch Changes diff --git a/service/package.json b/service/package.json index 3a3d6652e..0e2f96478 100644 --- a/service/package.json +++ b/service/package.json @@ -1,6 +1,6 @@ { "name": "@powersync/service-image", - "version": "0.5.4", + "version": "0.5.5", "private": true, "license": "FSL-1.1-Apache-2.0", "type": "module", diff --git a/test-client/CHANGELOG.md b/test-client/CHANGELOG.md index e901b1e55..9c90f775c 100644 --- a/test-client/CHANGELOG.md +++ b/test-client/CHANGELOG.md @@ -1,5 +1,13 @@ # test-client +## 0.1.7 + +### Patch Changes + +- Updated dependencies [1fd50a5] +- Updated dependencies [aa4eb0a] + - @powersync/service-core@0.8.5 + ## 0.1.6 ### Patch Changes diff --git a/test-client/package.json b/test-client/package.json index 075a7bb32..5e311c708 100644 --- a/test-client/package.json +++ b/test-client/package.json @@ -2,7 +2,7 @@ "name": "test-client", "repository": "https://github.com/powersync-ja/powersync-service", "private": true, - "version": "0.1.6", + "version": "0.1.7", "main": "dist/index.js", "bin": "dist/bin.js", "license": "Apache-2.0",