Skip to content

Commit f9e8673

Browse files
authored
[MongoDB Storage] Handle "topology is closed" (#278)
* Handle MongoDB TopologyIsClosed errors by closing the process. * Connect to storage database on startup. * Add changeset.
1 parent d235f7b commit f9e8673

File tree

8 files changed

+73
-5
lines changed

8 files changed

+73
-5
lines changed

.changeset/yellow-icons-cross.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@powersync/service-module-mongodb-storage': patch
3+
'@powersync/service-errors': patch
4+
'@powersync/service-core': patch
5+
'@powersync/lib-services-framework': patch
6+
'@powersync/lib-service-mongodb': patch
7+
---
8+
9+
[MongoDB Storage] Handle connection errors on startup

libs/lib-services/src/system/LifeCycledSystem.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* A System can contain anything but should offer a `start` and `stop` operation
77
*/
88

9+
import { ServiceError } from '@powersync/service-errors';
910
import { container } from '../container.js';
11+
import { logger } from '../logger/Logger.js';
1012

1113
export type LifecycleCallback<T> = (singleton: T) => Promise<void> | void;
1214

@@ -46,4 +48,17 @@ export class LifeCycledSystem {
4648
await lifecycle.stop?.(lifecycle.component);
4749
}
4850
};
51+
52+
stopWithError = async (error: ServiceError) => {
53+
try {
54+
logger.error('Stopping process due to fatal error', error);
55+
await this.stop();
56+
} catch (e) {
57+
logger.error('Error while stopping', e);
58+
} finally {
59+
// Custom error code to distinguish from other common errors
60+
logger.warn(`Exiting with code 151`);
61+
process.exit(151);
62+
}
63+
};
4964
}

modules/module-mongodb-storage/src/storage/implementation/MongoStorageProvider.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as lib_mongo from '@powersync/lib-service-mongodb';
2-
import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
2+
import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
33
import { storage } from '@powersync/service-core';
44
import { MongoStorageConfig } from '../../types/types.js';
55
import { MongoBucketStorage } from '../MongoBucketStorage.js';
@@ -26,6 +26,15 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
2626
maxPoolSize: resolvedConfig.storage.max_pool_size ?? 8
2727
});
2828

29+
let shuttingDown = false;
30+
31+
// Explicitly connect on startup.
32+
// Connection errors during startup are typically not recoverable - we get topologyClosed.
33+
// This helps to catch the error early, along with the cause, and before the process starts
34+
// to serve API requests.
35+
// Errors here will cause the process to exit.
36+
await client.connect();
37+
2938
const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
3039
const factory = new MongoBucketStorage(database, {
3140
// TODO currently need the entire resolved config due to this
@@ -34,12 +43,29 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
3443
return {
3544
storage: factory,
3645
shutDown: async () => {
46+
shuttingDown = true;
3747
await factory[Symbol.asyncDispose]();
3848
await client.close();
3949
},
4050
tearDown: () => {
4151
logger.info(`Tearing down storage: ${database.db.namespace}...`);
4252
return database.db.dropDatabase();
53+
},
54+
onFatalError: (callback) => {
55+
client.addListener('topologyClosed', () => {
56+
// If we're shutting down, this is expected and we can ignore it.
57+
if (!shuttingDown) {
58+
// Unfortunately there is no simple way to catch the cause of this issue.
59+
// It most commonly happens when the process fails to _ever_ connect - connection issues after
60+
// the initial connection are usually recoverable.
61+
callback(
62+
new ServiceError({
63+
code: ErrorCode.PSYNC_S2402,
64+
description: 'MongoDB topology closed - failed to connect to MongoDB storage.'
65+
})
66+
);
67+
}
68+
});
4369
}
4470
} satisfies storage.ActiveStorage;
4571
}

packages/service-core/src/entry/cli-entry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export function generateEntryProgram(startHandlers?: Record<utils.ServiceRunner,
3636
try {
3737
await entryProgram.parseAsync();
3838
} catch (e) {
39-
logger.error('Fatal error', e);
40-
process.exit(1);
39+
logger.error('Fatal startup error - exiting with code 150.', e);
40+
process.exit(150);
4141
}
4242
}
4343
};

packages/service-core/src/storage/StorageEngine.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { BaseObserver, logger } from '@powersync/lib-services-framework';
1+
import { BaseObserver, logger, ServiceError } from '@powersync/lib-services-framework';
22
import { ResolvedPowerSyncConfig } from '../util/util-index.js';
3-
import { ActiveStorage, BucketStorageProvider } from './StorageProvider.js';
43
import { BucketStorageFactory } from './BucketStorageFactory.js';
4+
import { ActiveStorage, BucketStorageProvider } from './StorageProvider.js';
55

66
export type StorageEngineOptions = {
77
configuration: ResolvedPowerSyncConfig;
88
};
99

1010
export interface StorageEngineListener {
1111
storageActivated: (storage: BucketStorageFactory) => void;
12+
storageFatalError: (error: ServiceError) => void;
1213
}
1314

1415
export class StorageEngine extends BaseObserver<StorageEngineListener> {
@@ -47,6 +48,9 @@ export class StorageEngine extends BaseObserver<StorageEngineListener> {
4748
resolvedConfig: configuration
4849
});
4950
this.iterateListeners((cb) => cb.storageActivated?.(this.activeBucketStorage));
51+
this.currentActiveStorage.onFatalError?.((error) => {
52+
this.iterateListeners((cb) => cb.storageFatalError?.(error));
53+
});
5054
logger.info(`Successfully activated storage: ${configuration.storage.type}.`);
5155
logger.info('Successfully started Storage Engine.');
5256
}

packages/service-core/src/storage/StorageProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ServiceError } from '@powersync/lib-services-framework';
12
import * as util from '../util/util-index.js';
23
import { BucketStorageFactory } from './BucketStorageFactory.js';
34

@@ -9,6 +10,8 @@ export interface ActiveStorage {
910
* Tear down / drop the storage permanently
1011
*/
1112
tearDown(): Promise<boolean>;
13+
14+
onFatalError?(callback: (error: ServiceError) => void): void;
1215
}
1316

1417
export interface GetStorageOptions {

packages/service-core/src/system/ServiceContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export class ServiceContextContainer implements ServiceContext {
5959
this.storageEngine = new storage.StorageEngine({
6060
configuration
6161
});
62+
this.storageEngine.registerListener({
63+
storageFatalError: (error) => {
64+
// Propagate the error to the lifecycle engine
65+
this.lifeCycleEngine.stopWithError(error);
66+
}
67+
});
6268

6369
this.lifeCycleEngine.withLifecycle(this.storageEngine, {
6470
start: (storageEngine) => storageEngine.start(),

packages/service-errors/src/codes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,11 @@ export enum ErrorCode {
423423
*/
424424
PSYNC_S2401 = 'PSYNC_S2401',
425425

426+
/**
427+
* Failed to connect to the MongoDB storage database.
428+
*/
429+
PSYNC_S2402 = 'PSYNC_S2402',
430+
426431
// ## PSYNC_S23xx: Sync API errors - Postgres Storage
427432

428433
// ## PSYNC_S3xxx: Service configuration issues

0 commit comments

Comments
 (0)