Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/yellow-icons-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@powersync/service-module-mongodb-storage': patch
'@powersync/service-errors': patch
'@powersync/service-core': patch
'@powersync/lib-services-framework': patch
'@powersync/lib-service-mongodb': patch
---

[MongoDB Storage] Handle connection errors on startup
15 changes: 15 additions & 0 deletions libs/lib-services/src/system/LifeCycledSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* A System can contain anything but should offer a `start` and `stop` operation
*/

import { ServiceError } from '@powersync/service-errors';
import { container } from '../container.js';
import { logger } from '../logger/Logger.js';

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

Expand Down Expand Up @@ -46,4 +48,17 @@ export class LifeCycledSystem {
await lifecycle.stop?.(lifecycle.component);
}
};

stopWithError = async (error: ServiceError) => {
try {
logger.error('Stopping process due to fatal error', error);
await this.stop();
} catch (e) {
logger.error('Error while stopping', e);
} finally {
// Custom error code to distinguish from other common errors
logger.warn(`Exiting with code 151`);
process.exit(151);
}
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as lib_mongo from '@powersync/lib-service-mongodb';
import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
import { storage } from '@powersync/service-core';
import { MongoStorageConfig } from '../../types/types.js';
import { MongoBucketStorage } from '../MongoBucketStorage.js';
Expand All @@ -26,6 +26,15 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
maxPoolSize: resolvedConfig.storage.max_pool_size ?? 8
});

let shuttingDown = false;

// Explicitly connect on startup.
// Connection errors during startup are typically not recoverable - we get topologyClosed.
// This helps to catch the error early, along with the cause, and before the process starts
// to serve API requests.
// Errors here will cause the process to exit.
await client.connect();

const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
const factory = new MongoBucketStorage(database, {
// TODO currently need the entire resolved config due to this
Expand All @@ -34,12 +43,29 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
return {
storage: factory,
shutDown: async () => {
shuttingDown = true;
await factory[Symbol.asyncDispose]();
await client.close();
},
tearDown: () => {
logger.info(`Tearing down storage: ${database.db.namespace}...`);
return database.db.dropDatabase();
},
onFatalError: (callback) => {
client.addListener('topologyClosed', () => {
// If we're shutting down, this is expected and we can ignore it.
if (!shuttingDown) {
// Unfortunately there is no simple way to catch the cause of this issue.
// It most commonly happens when the process fails to _ever_ connect - connection issues after
// the initial connection are usually recoverable.
callback(
new ServiceError({
code: ErrorCode.PSYNC_S2402,
description: 'MongoDB topology closed - failed to connect to MongoDB storage.'
})
);
}
});
}
} satisfies storage.ActiveStorage;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/service-core/src/entry/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function generateEntryProgram(startHandlers?: Record<utils.ServiceRunner,
try {
await entryProgram.parseAsync();
} catch (e) {
logger.error('Fatal error', e);
process.exit(1);
logger.error('Fatal startup error - exiting with code 150.', e);
process.exit(150);
}
}
};
Expand Down
8 changes: 6 additions & 2 deletions packages/service-core/src/storage/StorageEngine.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { BaseObserver, logger } from '@powersync/lib-services-framework';
import { BaseObserver, logger, ServiceError } from '@powersync/lib-services-framework';
import { ResolvedPowerSyncConfig } from '../util/util-index.js';
import { ActiveStorage, BucketStorageProvider } from './StorageProvider.js';
import { BucketStorageFactory } from './BucketStorageFactory.js';
import { ActiveStorage, BucketStorageProvider } from './StorageProvider.js';

export type StorageEngineOptions = {
configuration: ResolvedPowerSyncConfig;
};

export interface StorageEngineListener {
storageActivated: (storage: BucketStorageFactory) => void;
storageFatalError: (error: ServiceError) => void;
}

export class StorageEngine extends BaseObserver<StorageEngineListener> {
Expand Down Expand Up @@ -47,6 +48,9 @@ export class StorageEngine extends BaseObserver<StorageEngineListener> {
resolvedConfig: configuration
});
this.iterateListeners((cb) => cb.storageActivated?.(this.activeBucketStorage));
this.currentActiveStorage.onFatalError?.((error) => {
this.iterateListeners((cb) => cb.storageFatalError?.(error));
});
logger.info(`Successfully activated storage: ${configuration.storage.type}.`);
logger.info('Successfully started Storage Engine.');
}
Expand Down
3 changes: 3 additions & 0 deletions packages/service-core/src/storage/StorageProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServiceError } from '@powersync/lib-services-framework';
import * as util from '../util/util-index.js';
import { BucketStorageFactory } from './BucketStorageFactory.js';

Expand All @@ -9,6 +10,8 @@ export interface ActiveStorage {
* Tear down / drop the storage permanently
*/
tearDown(): Promise<boolean>;

onFatalError?(callback: (error: ServiceError) => void): void;
}

export interface GetStorageOptions {
Expand Down
6 changes: 6 additions & 0 deletions packages/service-core/src/system/ServiceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export class ServiceContextContainer implements ServiceContext {
this.storageEngine = new storage.StorageEngine({
configuration
});
this.storageEngine.registerListener({
storageFatalError: (error) => {
// Propagate the error to the lifecycle engine
this.lifeCycleEngine.stopWithError(error);
}
});

this.lifeCycleEngine.withLifecycle(this.storageEngine, {
start: (storageEngine) => storageEngine.start(),
Expand Down
5 changes: 5 additions & 0 deletions packages/service-errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,11 @@ export enum ErrorCode {
*/
PSYNC_S2401 = 'PSYNC_S2401',

/**
* Failed to connect to the MongoDB storage database.
*/
PSYNC_S2402 = 'PSYNC_S2402',

// ## PSYNC_S23xx: Sync API errors - Postgres Storage

// ## PSYNC_S3xxx: Service configuration issues
Expand Down