diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.test.ts deleted file mode 100644 index 31eecf14bb..0000000000 --- a/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createLifecycleMiddleware } from './createLifecycleMiddleware'; -import { BackendLifecycleImpl } from '../rootLifecycle/rootLifecycleServiceFactory'; -import { mockServices } from '@backstage/backend-test-utils'; -import { ServiceUnavailableError } from '@backstage/errors'; - -describe('createLifecycleMiddleware', () => { - it('should pause requests when plugin is not ready', async () => { - const lifecycle = new BackendLifecycleImpl(mockServices.rootLogger()); - - const middleware = createLifecycleMiddleware({ lifecycle }); - - const next = jest.fn(); - middleware({} as any, {} as any, next); - expect(next).not.toHaveBeenCalled(); - await lifecycle.startup(); - - // pending call - expect(next).toHaveBeenCalledWith(); - - // new call - const next2 = jest.fn(); - middleware({} as any, {} as any, next2); - expect(next2).toHaveBeenCalledWith(); - }); - - it('should throw ServiceUnavailableError after shutdown', async () => { - const lifecycle = new BackendLifecycleImpl(mockServices.rootLogger()); - const middleware = createLifecycleMiddleware({ lifecycle }); - - const next = jest.fn(); - middleware({} as any, {} as any, next); - expect(next).not.toHaveBeenCalled(); - await lifecycle.shutdown(); - - // pending call - expect(next).toHaveBeenCalledWith( - new ServiceUnavailableError('Service is shutting down'), - ); - - // new call - const next2 = jest.fn(); - middleware({} as any, {} as any, next2); - expect(next2).toHaveBeenCalledWith( - new ServiceUnavailableError('Service is shutting down'), - ); - }); - - it('should throw ServiceUnavailableError after timeout', async () => { - const lifecycle = new BackendLifecycleImpl(mockServices.rootLogger()); - const middleware = createLifecycleMiddleware({ - lifecycle, - startupRequestPauseTimeout: { milliseconds: 1 }, - }); - - const next = jest.fn(); - middleware({} as any, {} as any, next); - expect(next).not.toHaveBeenCalled(); - - await new Promise(r => setTimeout(r, 2)); - - expect(next).toHaveBeenCalledWith( - new ServiceUnavailableError('Service has not started up yet'), - ); - }); -}); diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts deleted file mode 100644 index 99fb82a41e..0000000000 --- a/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { ServiceUnavailableError } from '@backstage/errors'; -import { HumanDuration, durationToMilliseconds } from '@backstage/types'; -import { RequestHandler } from 'express'; - -export const DEFAULT_STARTUP_REQUEST_PAUSE_TIMEOUT = { seconds: 5 }; -export const DEFAULT_SERVER_SHUTDOWN_TIMEOUT = { seconds: 0 }; - -/** - * Options for {@link createLifecycleMiddleware}. - * @public - */ -export interface LifecycleMiddlewareOptions { - lifecycle: RootLifecycleService; - /** - * The maximum time that paused requests will wait for the service to start, before returning an error. - * - * Defaults to 5 seconds. - */ - startupRequestPauseTimeout?: HumanDuration; -} - -/** - * Creates a middleware that pauses requests until the service has started. - * - * @remarks - * - * Requests that arrive before the service has started will be paused until startup is complete. - * If the service does not start within the provided timeout, the request will be rejected with a - * {@link @backstage/errors#ServiceUnavailableError}. - * - * If the service is shutting down, all requests will be rejected with a - * {@link @backstage/errors#ServiceUnavailableError}. - * - * @public - */ -export function createLifecycleMiddleware( - options: LifecycleMiddlewareOptions, -): RequestHandler { - const { lifecycle, startupRequestPauseTimeout } = options; - - let state: 'init' | 'up' | 'down' = 'init'; - const waiting = new Set<{ - next: (err?: Error) => void; - timeout: NodeJS.Timeout; - }>(); - - lifecycle.addStartupHook(async () => { - if (state === 'init') { - state = 'up'; - for (const item of waiting) { - clearTimeout(item.timeout); - item.next(); - } - waiting.clear(); - } - }); - - lifecycle.addShutdownHook(async () => { - state = 'down'; - for (const item of waiting) { - clearTimeout(item.timeout); - item.next(new ServiceUnavailableError('Service is shutting down')); - } - waiting.clear(); - }); - - return (_req, _res, next) => { - if (state === 'up') { - next(); - return; - } else if (state === 'down') { - next(new ServiceUnavailableError('Service is shutting down')); - return; - } - - const timeoutMs = durationToMilliseconds( - startupRequestPauseTimeout ?? DEFAULT_STARTUP_REQUEST_PAUSE_TIMEOUT, - ); - - const item = { - next, - timeout: setTimeout(() => { - if (waiting.delete(item)) { - next(new ServiceUnavailableError('Service has not started up yet')); - } - }, timeoutMs), - }; - - waiting.add(item); - }; -}