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/packages/service-core/src/storage/BucketStorage.ts b/packages/service-core/src/storage/BucketStorage.ts index 24d46e25f..b3f567bd1 100644 --- a/packages/service-core/src/storage/BucketStorage.ts +++ b/packages/service-core/src/storage/BucketStorage.ts @@ -253,8 +253,6 @@ export interface SyncRulesBucketStorage { */ clear(): Promise; - 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 de7a9901a..919fb24e9 100644 --- a/packages/service-core/src/storage/MongoBucketStorage.ts +++ b/packages/service-core/src/storage/MongoBucketStorage.ts @@ -294,6 +294,15 @@ export class MongoBucketStorage implements BucketStorageFactory { } 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(); if (active_sync_rules == null) { return { @@ -307,34 +316,34 @@ export class MongoBucketStorage implements BucketStorageFactory { .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 855fce8ee..762b05f63 100644 --- a/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts +++ b/packages/service-core/src/storage/mongo/MongoSyncBucketStorage.ts @@ -505,21 +505,6 @@ export class MongoSyncBucketStorage implements SyncRulesBucketStorage { ); } - 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() - } - } - ); - } - async autoActivate(): Promise { await this.db.client.withSession(async (session) => { await session.withTransaction(async () => { diff --git a/packages/service-core/src/storage/mongo/db.ts b/packages/service-core/src/storage/mongo/db.ts index 1cc3f8471..05b0ab6fc 100644 --- a/packages/service-core/src/storage/mongo/db.ts +++ b/packages/service-core/src/storage/mongo/db.ts @@ -59,6 +59,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({}); @@ -70,4 +73,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 088cb5614..4abbc79d1 100644 --- a/packages/service-core/test/src/data_storage.test.ts +++ b/packages/service-core/test/src/data_storage.test.ts @@ -1294,4 +1294,26 @@ bucket_definitions: expect(getBatchMeta(batch3)).toEqual(null); }); + + 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!.parsed()); + 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 bd66ea357..c367169ab 100644 --- a/packages/service-core/test/src/sync.test.ts +++ b/packages/service-core/test/src/sync.test.ts @@ -5,7 +5,6 @@ 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 { ZERO_LSN } from '../../src/replication/WalStream.js'; import { streamResponse } from '../../src/sync/sync.js'; import { makeTestTable, MONGO_STORAGE_FACTORY, StorageFactory } from './util.js'; @@ -33,7 +32,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const result = await storage.startBatch({}, async (batch) => { @@ -82,7 +80,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const result = await storage.startBatch({}, async (batch) => { @@ -125,7 +122,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const stream = streamResponse({ @@ -152,7 +148,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const stream = streamResponse({ @@ -211,7 +206,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); const exp = Date.now() / 1000 + 0.1; @@ -249,7 +243,6 @@ function defineTests(factory: StorageFactory) { }); const storage = await f.getInstance(syncRules.parsed()); - await storage.setSnapshotDone(ZERO_LSN); await storage.autoActivate(); await storage.startBatch({}, async (batch) => { diff --git a/packages/service-core/test/src/util.ts b/packages/service-core/test/src/util.ts index 06fe8a7e7..c2722bbd1 100644 --- a/packages/service-core/test/src/util.ts +++ b/packages/service-core/test/src/util.ts @@ -22,11 +22,22 @@ Metrics.getInstance().resetCounters(); export const TEST_URI = env.PG_TEST_URL; -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_' }); };