diff --git a/.changeset/silver-comics-attend.md b/.changeset/silver-comics-attend.md new file mode 100644 index 0000000000..f13553b53f --- /dev/null +++ b/.changeset/silver-comics-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Fix for backend shutdown hanging during local development due to SQLite connection shutdown never resolving. diff --git a/packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts index c7e739c7c7..27cb511477 100644 --- a/packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts @@ -182,7 +182,9 @@ describe('DatabaseManagerImpl', () => { const rootLifecycle = { addShutdownHook: jest.fn() } as unknown as any; const destroy = jest.fn(); const connector1 = { - getClient: jest.fn().mockResolvedValue({ destroy }), + getClient: jest + .fn() + .mockResolvedValue({ destroy, client: { config: 'pg' } }), } satisfies Connector; const impl = new DatabaseManagerImpl( new ConfigReader({ @@ -207,4 +209,44 @@ describe('DatabaseManagerImpl', () => { // Then the destroy method should have been called on the resolved client expect(destroy).toHaveBeenCalled(); }); + + it('does not attempt to destroy connection when using SQLite', async () => { + // Same us the previous test, but with SQLite + const rootLifecycle = { addShutdownHook: jest.fn() } as unknown as any; + + // Make sure we're actually checking the client, since we're ignoring errors + const getConfig = jest.fn().mockReturnValue('sqlite3'); + + const destroy = jest.fn(); + const connector1 = { + getClient: jest.fn().mockResolvedValue({ + destroy, + client: { + get config() { + return getConfig(); + }, + }, + }), + } satisfies Connector; + const impl = new DatabaseManagerImpl( + new ConfigReader({ + client: 'pg', + }), + { + pg: connector1, + }, + { rootLifecycle }, + ); + + expect(rootLifecycle.addShutdownHook).toHaveBeenCalled(); + const shutdownHook = rootLifecycle.addShutdownHook.mock.calls[0][0]; + + await impl.forPlugin('plugin1', deps).getClient(); + + await shutdownHook(); + + // Destroy should not have been called, but we should have read the config + expect(destroy).not.toHaveBeenCalled(); + expect(getConfig).toHaveBeenCalled(); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts index 0f6207ef07..7fddb17987 100644 --- a/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts +++ b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts @@ -117,6 +117,9 @@ export class DatabaseManagerImpl { const connection = await this.databaseCache.get(pluginId); if (connection) { + if (connection.client.config.includes('sqlite3')) { + return; // sqlite3 does not support destroy, it hangs + } await connection.destroy().catch((error: unknown) => { deps?.logger?.error( `Problem closing database connection for ${pluginId}: ${stringifyError(