@backstage/backend-defaults 0.17.9-next.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/config.schema.json +5 -0
  3. package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js +5 -5
  4. package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js.map +1 -1
  5. package/dist/entrypoints/database/DatabaseManager.cjs.js +7 -1
  6. package/dist/entrypoints/database/DatabaseManager.cjs.js.map +1 -1
  7. package/dist/entrypoints/database/connectors/postgres.cjs.js +16 -5
  8. package/dist/entrypoints/database/connectors/postgres.cjs.js.map +1 -1
  9. package/dist/entrypoints/httpAuth/httpAuthServiceFactory.cjs.js +14 -4
  10. package/dist/entrypoints/httpAuth/httpAuthServiceFactory.cjs.js.map +1 -1
  11. package/dist/entrypoints/rootHttpRouter/DefaultRootHttpRouter.cjs.js +1 -1
  12. package/dist/entrypoints/rootHttpRouter/DefaultRootHttpRouter.cjs.js.map +1 -1
  13. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.cjs.js +10 -1
  14. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.cjs.js.map +1 -1
  15. package/dist/entrypoints/scheduler/lib/TaskStatePoller.cjs.js +200 -0
  16. package/dist/entrypoints/scheduler/lib/TaskStatePoller.cjs.js.map +1 -0
  17. package/dist/entrypoints/scheduler/lib/TaskWorker.cjs.js +21 -45
  18. package/dist/entrypoints/scheduler/lib/TaskWorker.cjs.js.map +1 -1
  19. package/dist/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.cjs.js +22 -7
  20. package/dist/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.cjs.js.map +1 -1
  21. package/dist/entrypoints/urlReader/lib/AwsS3UrlReader.cjs.js +22 -7
  22. package/dist/entrypoints/urlReader/lib/AwsS3UrlReader.cjs.js.map +1 -1
  23. package/dist/package.json.cjs.js +1 -1
  24. package/package.json +18 -17
@@ -1 +1 @@
1
- {"version":3,"file":"httpAuthServiceFactory.cjs.js","sources":["../../../src/entrypoints/httpAuth/httpAuthServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstagePrincipalTypes,\n BackstageUserPrincipal,\n DiscoveryService,\n HttpAuthService,\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { parse as parseCookie } from 'cookie';\nimport { Request, Response } from 'express';\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000;\n\nconst BACKSTAGE_AUTH_COOKIE = 'backstage-auth';\n\nfunction getTokenFromRequest(req: Request) {\n let token: string | undefined;\n const authHeader = req.headers.authorization;\n if (typeof authHeader === 'string') {\n const matches = authHeader.match(/^Bearer[ ]+(\\S+)$/i);\n token = matches?.[1];\n }\n\n return { token };\n}\n\nfunction getCookieFromRequest(req: Request) {\n const cookieHeader = req.headers.cookie;\n if (cookieHeader) {\n const cookies = parseCookie(cookieHeader);\n const token = cookies[BACKSTAGE_AUTH_COOKIE];\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nfunction willExpireSoon(expiresAt: Date) {\n return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();\n}\n\nconst credentialsSymbol = Symbol('backstage-credentials');\nconst limitedCredentialsSymbol = Symbol('backstage-limited-credentials');\n\ntype RequestWithCredentials = Request & {\n [credentialsSymbol]?: Promise<BackstageCredentials>;\n [limitedCredentialsSymbol]?: Promise<BackstageCredentials>;\n};\n\n/**\n * @public\n * Options for creating a DefaultHttpAuthService.\n */\nexport interface DefaultHttpAuthServiceOptions {\n auth: AuthService;\n discovery: DiscoveryService;\n pluginId: string;\n /**\n * Optionally override logic for extracting the token from the request.\n */\n getTokenFromRequest?: (req: Request) => { token?: string };\n}\n\n/**\n * @public\n * DefaultHttpAuthService is the default implementation of the HttpAuthService\n */\nexport class DefaultHttpAuthService implements HttpAuthService {\n readonly #auth: AuthService;\n readonly #discovery: DiscoveryService;\n readonly #pluginId: string;\n readonly #getToken: (req: Request) => { token?: string };\n\n private constructor(\n auth: AuthService,\n discovery: DiscoveryService,\n pluginId: string,\n getToken?: (req: Request) => { token?: string },\n ) {\n this.#auth = auth;\n this.#discovery = discovery;\n this.#pluginId = pluginId;\n this.#getToken = getToken ?? getTokenFromRequest;\n }\n\n static create(\n options: DefaultHttpAuthServiceOptions,\n ): DefaultHttpAuthService {\n return new DefaultHttpAuthService(\n options.auth,\n options.discovery,\n options.pluginId,\n options.getTokenFromRequest,\n );\n }\n\n async #extractCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (!token) {\n return await this.#auth.getNoneCredentials();\n }\n\n return await this.#auth.authenticate(token);\n }\n\n async #extractLimitedCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (token) {\n return await this.#auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n }\n\n const cookie = getCookieFromRequest(req);\n if (cookie) {\n return await this.#auth.authenticate(cookie, {\n allowLimitedAccess: true,\n });\n }\n\n return await this.#auth.getNoneCredentials();\n }\n\n async #getCredentials(req: RequestWithCredentials) {\n return (req[credentialsSymbol] ??=\n this.#extractCredentialsFromRequest(req));\n }\n\n async #getLimitedCredentials(req: RequestWithCredentials) {\n return (req[limitedCredentialsSymbol] ??=\n this.#extractLimitedCredentialsFromRequest(req));\n }\n\n async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(\n req: Request,\n options?: {\n allow?: Array<TAllowed>;\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {\n // Limited and full credentials are treated as two separate cases, this lets\n // us avoid internal dependencies between the AuthService and\n // HttpAuthService implementations\n const credentials = options?.allowLimitedAccess\n ? await this.#getLimitedCredentials(req)\n : await this.#getCredentials(req);\n\n const allowed = options?.allow;\n if (!allowed) {\n return credentials as any;\n }\n\n if (this.#auth.isPrincipal(credentials, 'none')) {\n if (allowed.includes('none' as TAllowed)) {\n return credentials as any;\n }\n\n throw new AuthenticationError('Missing credentials');\n } else if (this.#auth.isPrincipal(credentials, 'user')) {\n if (allowed.includes('user' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'user' credentials`,\n );\n } else if (this.#auth.isPrincipal(credentials, 'service')) {\n if (allowed.includes('service' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'service' credentials`,\n );\n }\n\n throw new NotAllowedError(\n 'Unknown principal type, this should never happen',\n );\n }\n\n async issueUserCookie(\n res: Response,\n options?: { credentials?: BackstageCredentials },\n ): Promise<{ expiresAt: Date }> {\n if (res.headersSent) {\n throw new Error('Failed to issue user cookie, headers were already sent');\n }\n\n let credentials: BackstageCredentials<BackstageUserPrincipal>;\n if (options?.credentials) {\n if (this.#auth.isPrincipal(options.credentials, 'none')) {\n res.clearCookie(\n BACKSTAGE_AUTH_COOKIE,\n await this.#getCookieOptions(res.req),\n );\n return { expiresAt: new Date() };\n }\n if (!this.#auth.isPrincipal(options.credentials, 'user')) {\n throw new AuthenticationError(\n 'Refused to issue cookie for non-user principal',\n );\n }\n credentials = options.credentials;\n } else {\n credentials = await this.credentials(res.req, { allow: ['user'] });\n }\n\n const existingExpiresAt = await this.#existingCookieExpiration(res.req);\n if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {\n return { expiresAt: existingExpiresAt };\n }\n\n const { token, expiresAt } = await this.#auth.getLimitedUserToken(\n credentials,\n );\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n\n res.cookie(BACKSTAGE_AUTH_COOKIE, token, {\n ...(await this.#getCookieOptions(res.req)),\n expires: expiresAt,\n });\n\n return { expiresAt };\n }\n\n async #getCookieOptions(_req: Request): Promise<{\n domain: string;\n httpOnly: true;\n secure: boolean;\n priority: 'high';\n sameSite: 'none' | 'lax';\n }> {\n // TODO: eventually we should read from `${req.protocol}://${req.hostname}`\n // once https://github.com/backstage/backstage/issues/24169 has landed\n const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(\n this.#pluginId,\n );\n const externalBaseUrl = new URL(externalBaseUrlStr);\n\n const secure =\n externalBaseUrl.protocol === 'https:' ||\n externalBaseUrl.hostname === 'localhost';\n\n return {\n domain: externalBaseUrl.hostname,\n httpOnly: true,\n secure,\n priority: 'high',\n sameSite: secure ? 'none' : 'lax',\n };\n }\n\n async #existingCookieExpiration(req: Request): Promise<Date | undefined> {\n const existingCookie = getCookieFromRequest(req);\n if (!existingCookie) {\n return undefined;\n }\n\n try {\n const existingCredentials = await this.#auth.authenticate(\n existingCookie,\n {\n allowLimitedAccess: true,\n },\n );\n if (!this.#auth.isPrincipal(existingCredentials, 'user')) {\n return undefined;\n }\n\n return existingCredentials.expiresAt;\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n return undefined;\n }\n throw error;\n }\n }\n}\n\n/**\n * Authentication of HTTP requests.\n *\n * See {@link @backstage/code-plugin-api#HttpAuthService}\n * and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs}\n * for more information.\n *\n * @public\n */\nexport const httpAuthServiceFactory = createServiceFactory({\n service: coreServices.httpAuth,\n deps: {\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n plugin: coreServices.pluginMetadata,\n },\n async factory({ auth, discovery, plugin }) {\n return DefaultHttpAuthService.create({\n auth,\n discovery,\n pluginId: plugin.getId(),\n });\n },\n});\n"],"names":["parseCookie","AuthenticationError","NotAllowedError","createServiceFactory","coreServices"],"mappings":";;;;;;AA8BA,MAAM,eAAA,GAAkB,IAAI,EAAA,GAAK,GAAA;AAEjC,MAAM,qBAAA,GAAwB,gBAAA;AAE9B,SAAS,oBAAoB,GAAA,EAAc;AACzC,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,UAAA,GAAa,IAAI,OAAA,CAAQ,aAAA;AAC/B,EAAA,IAAI,OAAO,eAAe,QAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,KAAA,CAAM,oBAAoB,CAAA;AACrD,IAAA,KAAA,GAAQ,UAAU,CAAC,CAAA;AAAA,EACrB;AAEA,EAAA,OAAO,EAAE,KAAA,EAAM;AACjB;AAEA,SAAS,qBAAqB,GAAA,EAAc;AAC1C,EAAA,MAAM,YAAA,GAAe,IAAI,OAAA,CAAQ,MAAA;AACjC,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,MAAM,OAAA,GAAUA,aAAY,YAAY,CAAA;AACxC,IAAA,MAAM,KAAA,GAAQ,QAAQ,qBAAqB,CAAA;AAC3C,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,eAAe,SAAA,EAAiB;AACvC,EAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,eAAA,GAAkB,UAAU,OAAA,EAAQ;AAC1D;AAEA,MAAM,iBAAA,0BAA2B,uBAAuB,CAAA;AACxD,MAAM,wBAAA,0BAAkC,+BAA+B,CAAA;AAyBhE,MAAM,sBAAA,CAAkD;AAAA,EACpD,KAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EAED,WAAA,CACN,IAAA,EACA,SAAA,EACA,QAAA,EACA,QAAA,EACA;AACA,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,YAAY,QAAA,IAAY,mBAAA;AAAA,EAC/B;AAAA,EAEA,OAAO,OACL,OAAA,EACwB;AACxB,IAAA,OAAO,IAAI,sBAAA;AAAA,MACT,OAAA,CAAQ,IAAA;AAAA,MACR,OAAA,CAAQ,SAAA;AAAA,MACR,OAAA,CAAQ,QAAA;AAAA,MACR,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEA,MAAM,+BAA+B,GAAA,EAAc;AACjD,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,UAAU,GAAG,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,kBAAA,EAAmB;AAAA,IAC7C;AAEA,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA;AAAA,EAC5C;AAAA,EAEA,MAAM,sCAAsC,GAAA,EAAc;AACxD,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,UAAU,GAAG,CAAA;AACpC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,KAAA,EAAO;AAAA,QAC1C,kBAAA,EAAoB;AAAA,OACrB,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,MAAA,GAAS,qBAAqB,GAAG,CAAA;AACvC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,MAAA,EAAQ;AAAA,QAC3C,kBAAA,EAAoB;AAAA,OACrB,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,kBAAA,EAAmB;AAAA,EAC7C;AAAA,EAEA,MAAM,gBAAgB,GAAA,EAA6B;AACjD,IAAA,OAAQ,GAAA,CAAI,iBAAiB,CAAA,KAC3B,IAAA,CAAK,+BAA+B,GAAG,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,uBAAuB,GAAA,EAA6B;AACxD,IAAA,OAAQ,GAAA,CAAI,wBAAwB,CAAA,KAClC,IAAA,CAAK,sCAAsC,GAAG,CAAA;AAAA,EAClD;AAAA,EAEA,MAAM,WAAA,CACJ,GAAA,EACA,OAAA,EAIkE;AAIlE,IAAA,MAAM,WAAA,GAAc,OAAA,EAAS,kBAAA,GACzB,MAAM,IAAA,CAAK,sBAAA,CAAuB,GAAG,CAAA,GACrC,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAElC,IAAA,MAAM,UAAU,OAAA,EAAS,KAAA;AACzB,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,WAAA;AAAA,IACT;AAEA,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AAC/C,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,MAAkB,CAAA,EAAG;AACxC,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIC,2BAAoB,qBAAqB,CAAA;AAAA,IACrD,WAAW,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AACtD,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,MAAkB,CAAA,EAAG;AACxC,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIC,sBAAA;AAAA,QACR,CAAA,+CAAA;AAAA,OACF;AAAA,IACF,WAAW,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,SAAS,CAAA,EAAG;AACzD,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,SAAqB,CAAA,EAAG;AAC3C,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIA,sBAAA;AAAA,QACR,CAAA,kDAAA;AAAA,OACF;AAAA,IACF;AAEA,IAAA,MAAM,IAAIA,sBAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAA,CACJ,GAAA,EACA,OAAA,EAC8B;AAC9B,IAAA,IAAI,IAAI,WAAA,EAAa;AACnB,MAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,IAC1E;AAEA,IAAA,IAAI,WAAA;AACJ,IAAA,IAAI,SAAS,WAAA,EAAa;AACxB,MAAA,IAAI,KAAK,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,WAAA,EAAa,MAAM,CAAA,EAAG;AACvD,QAAA,GAAA,CAAI,WAAA;AAAA,UACF,qBAAA;AAAA,UACA,MAAM,IAAA,CAAK,iBAAA,CAAkB,GAAA,CAAI,GAAG;AAAA,SACtC;AACA,QAAA,OAAO,EAAE,SAAA,kBAAW,IAAI,IAAA,EAAK,EAAE;AAAA,MACjC;AACA,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,YAAY,OAAA,CAAQ,WAAA,EAAa,MAAM,CAAA,EAAG;AACxD,QAAA,MAAM,IAAID,0BAAA;AAAA,UACR;AAAA,SACF;AAAA,MACF;AACA,MAAA,WAAA,GAAc,OAAA,CAAQ,WAAA;AAAA,IACxB,CAAA,MAAO;AACL,MAAA,WAAA,GAAc,MAAM,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,iBAAA,GAAoB,MAAM,IAAA,CAAK,yBAAA,CAA0B,IAAI,GAAG,CAAA;AACtE,IAAA,IAAI,iBAAA,IAAqB,CAAC,cAAA,CAAe,iBAAiB,CAAA,EAAG;AAC3D,MAAA,OAAO,EAAE,WAAW,iBAAA,EAAkB;AAAA,IACxC;AAEA,IAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,MAAM,KAAK,KAAA,CAAM,mBAAA;AAAA,MAC5C;AAAA,KACF;AACA,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AAEA,IAAA,GAAA,CAAI,MAAA,CAAO,uBAAuB,KAAA,EAAO;AAAA,MACvC,GAAI,MAAM,IAAA,CAAK,iBAAA,CAAkB,IAAI,GAAG,CAAA;AAAA,MACxC,OAAA,EAAS;AAAA,KACV,CAAA;AAED,IAAA,OAAO,EAAE,SAAA,EAAU;AAAA,EACrB;AAAA,EAEA,MAAM,kBAAkB,IAAA,EAMrB;AAGD,IAAA,MAAM,kBAAA,GAAqB,MAAM,IAAA,CAAK,UAAA,CAAW,kBAAA;AAAA,MAC/C,IAAA,CAAK;AAAA,KACP;AACA,IAAA,MAAM,eAAA,GAAkB,IAAI,GAAA,CAAI,kBAAkB,CAAA;AAElD,IAAA,MAAM,MAAA,GACJ,eAAA,CAAgB,QAAA,KAAa,QAAA,IAC7B,gBAAgB,QAAA,KAAa,WAAA;AAE/B,IAAA,OAAO;AAAA,MACL,QAAQ,eAAA,CAAgB,QAAA;AAAA,MACxB,QAAA,EAAU,IAAA;AAAA,MACV,MAAA;AAAA,MACA,QAAA,EAAU,MAAA;AAAA,MACV,QAAA,EAAU,SAAS,MAAA,GAAS;AAAA,KAC9B;AAAA,EACF;AAAA,EAEA,MAAM,0BAA0B,GAAA,EAAyC;AACvE,IAAA,MAAM,cAAA,GAAiB,qBAAqB,GAAG,CAAA;AAC/C,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,mBAAA,GAAsB,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA;AAAA,QAC3C,cAAA;AAAA,QACA;AAAA,UACE,kBAAA,EAAoB;AAAA;AACtB,OACF;AACA,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,mBAAA,EAAqB,MAAM,CAAA,EAAG;AACxD,QAAA,OAAO,KAAA,CAAA;AAAA,MACT;AAEA,MAAA,OAAO,mBAAA,CAAoB,SAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,KAAA,CAAM,SAAS,qBAAA,EAAuB;AACxC,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF;AAWO,MAAM,yBAAyBE,qCAAA,CAAqB;AAAA,EACzD,SAASC,6BAAA,CAAa,QAAA;AAAA,EACtB,IAAA,EAAM;AAAA,IACJ,MAAMA,6BAAA,CAAa,IAAA;AAAA,IACnB,WAAWA,6BAAA,CAAa,SAAA;AAAA,IACxB,QAAQA,6BAAA,CAAa;AAAA,GACvB;AAAA,EACA,MAAM,OAAA,CAAQ,EAAE,IAAA,EAAM,SAAA,EAAW,QAAO,EAAG;AACzC,IAAA,OAAO,uBAAuB,MAAA,CAAO;AAAA,MACnC,IAAA;AAAA,MACA,SAAA;AAAA,MACA,QAAA,EAAU,OAAO,KAAA;AAAM,KACxB,CAAA;AAAA,EACH;AACF,CAAC;;;;;"}
1
+ {"version":3,"file":"httpAuthServiceFactory.cjs.js","sources":["../../../src/entrypoints/httpAuth/httpAuthServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstagePrincipalTypes,\n BackstageUserPrincipal,\n DiscoveryService,\n HttpAuthService,\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { parse as parseCookie } from 'cookie';\nimport { Request, Response } from 'express';\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000;\n\nconst BACKSTAGE_AUTH_COOKIE = 'backstage-auth';\n\nfunction getTokenFromRequest(req: Request) {\n let token: string | undefined;\n const authHeader = req.headers.authorization;\n if (typeof authHeader === 'string') {\n const matches = authHeader.match(/^Bearer[ ]+(\\S+)$/i);\n token = matches?.[1];\n }\n\n return { token };\n}\n\nfunction getCookieFromRequest(req: Request) {\n const cookieHeader = req.headers.cookie;\n if (cookieHeader) {\n const cookies = parseCookie(cookieHeader);\n const token = cookies[BACKSTAGE_AUTH_COOKIE];\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nfunction willExpireSoon(expiresAt: Date) {\n return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();\n}\n\n/**\n * @public\n * Options for creating a DefaultHttpAuthService.\n */\nexport interface DefaultHttpAuthServiceOptions {\n auth: AuthService;\n discovery: DiscoveryService;\n pluginId: string;\n /**\n * Optionally override logic for extracting the token from the request.\n */\n getTokenFromRequest?: (req: Request) => { token?: string };\n}\n\n/**\n * @public\n * DefaultHttpAuthService is the default implementation of the HttpAuthService\n */\nexport class DefaultHttpAuthService implements HttpAuthService {\n readonly #credentialsCache = new WeakMap<\n Request,\n Promise<BackstageCredentials>\n >();\n readonly #limitedCredentialsCache = new WeakMap<\n Request,\n Promise<BackstageCredentials>\n >();\n readonly #auth: AuthService;\n readonly #discovery: DiscoveryService;\n readonly #pluginId: string;\n readonly #getToken: (req: Request) => { token?: string };\n\n private constructor(\n auth: AuthService,\n discovery: DiscoveryService,\n pluginId: string,\n getToken?: (req: Request) => { token?: string },\n ) {\n this.#auth = auth;\n this.#discovery = discovery;\n this.#pluginId = pluginId;\n this.#getToken = getToken ?? getTokenFromRequest;\n }\n\n static create(\n options: DefaultHttpAuthServiceOptions,\n ): DefaultHttpAuthService {\n return new DefaultHttpAuthService(\n options.auth,\n options.discovery,\n options.pluginId,\n options.getTokenFromRequest,\n );\n }\n\n async #extractCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (!token) {\n return await this.#auth.getNoneCredentials();\n }\n\n return await this.#auth.authenticate(token);\n }\n\n async #extractLimitedCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (token) {\n return await this.#auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n }\n\n const cookie = getCookieFromRequest(req);\n if (cookie) {\n return await this.#auth.authenticate(cookie, {\n allowLimitedAccess: true,\n });\n }\n\n return await this.#auth.getNoneCredentials();\n }\n\n async #getCredentials(req: Request) {\n let credentials = this.#credentialsCache.get(req);\n if (!credentials) {\n credentials = this.#extractCredentialsFromRequest(req);\n this.#credentialsCache.set(req, credentials);\n }\n return credentials;\n }\n\n async #getLimitedCredentials(req: Request) {\n let credentials = this.#limitedCredentialsCache.get(req);\n if (!credentials) {\n credentials = this.#extractLimitedCredentialsFromRequest(req);\n this.#limitedCredentialsCache.set(req, credentials);\n }\n return credentials;\n }\n\n async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(\n req: Request,\n options?: {\n allow?: Array<TAllowed>;\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {\n // Limited and full credentials are treated as two separate cases, this lets\n // us avoid internal dependencies between the AuthService and\n // HttpAuthService implementations\n const credentials = options?.allowLimitedAccess\n ? await this.#getLimitedCredentials(req)\n : await this.#getCredentials(req);\n\n const allowed = options?.allow;\n if (!allowed) {\n return credentials as any;\n }\n\n if (this.#auth.isPrincipal(credentials, 'none')) {\n if (allowed.includes('none' as TAllowed)) {\n return credentials as any;\n }\n\n throw new AuthenticationError('Missing credentials');\n } else if (this.#auth.isPrincipal(credentials, 'user')) {\n if (allowed.includes('user' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'user' credentials`,\n );\n } else if (this.#auth.isPrincipal(credentials, 'service')) {\n if (allowed.includes('service' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'service' credentials`,\n );\n }\n\n throw new NotAllowedError(\n 'Unknown principal type, this should never happen',\n );\n }\n\n async issueUserCookie(\n res: Response,\n options?: { credentials?: BackstageCredentials },\n ): Promise<{ expiresAt: Date }> {\n if (res.headersSent) {\n throw new Error('Failed to issue user cookie, headers were already sent');\n }\n\n let credentials: BackstageCredentials<BackstageUserPrincipal>;\n if (options?.credentials) {\n if (this.#auth.isPrincipal(options.credentials, 'none')) {\n res.clearCookie(\n BACKSTAGE_AUTH_COOKIE,\n await this.#getCookieOptions(res.req),\n );\n return { expiresAt: new Date() };\n }\n if (!this.#auth.isPrincipal(options.credentials, 'user')) {\n throw new AuthenticationError(\n 'Refused to issue cookie for non-user principal',\n );\n }\n credentials = options.credentials;\n } else {\n credentials = await this.credentials(res.req, { allow: ['user'] });\n }\n\n const existingExpiresAt = await this.#existingCookieExpiration(res.req);\n if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {\n return { expiresAt: existingExpiresAt };\n }\n\n const { token, expiresAt } = await this.#auth.getLimitedUserToken(\n credentials,\n );\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n\n res.cookie(BACKSTAGE_AUTH_COOKIE, token, {\n ...(await this.#getCookieOptions(res.req)),\n expires: expiresAt,\n });\n\n return { expiresAt };\n }\n\n async #getCookieOptions(_req: Request): Promise<{\n domain: string;\n httpOnly: true;\n secure: boolean;\n priority: 'high';\n sameSite: 'none' | 'lax';\n }> {\n // TODO: eventually we should read from `${req.protocol}://${req.hostname}`\n // once https://github.com/backstage/backstage/issues/24169 has landed\n const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(\n this.#pluginId,\n );\n const externalBaseUrl = new URL(externalBaseUrlStr);\n\n const secure =\n externalBaseUrl.protocol === 'https:' ||\n externalBaseUrl.hostname === 'localhost';\n\n return {\n domain: externalBaseUrl.hostname,\n httpOnly: true,\n secure,\n priority: 'high',\n sameSite: secure ? 'none' : 'lax',\n };\n }\n\n async #existingCookieExpiration(req: Request): Promise<Date | undefined> {\n const existingCookie = getCookieFromRequest(req);\n if (!existingCookie) {\n return undefined;\n }\n\n try {\n const existingCredentials = await this.#auth.authenticate(\n existingCookie,\n {\n allowLimitedAccess: true,\n },\n );\n if (!this.#auth.isPrincipal(existingCredentials, 'user')) {\n return undefined;\n }\n\n return existingCredentials.expiresAt;\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n return undefined;\n }\n throw error;\n }\n }\n}\n\n/**\n * Authentication of HTTP requests.\n *\n * See {@link @backstage/code-plugin-api#HttpAuthService}\n * and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs}\n * for more information.\n *\n * @public\n */\nexport const httpAuthServiceFactory = createServiceFactory({\n service: coreServices.httpAuth,\n deps: {\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n plugin: coreServices.pluginMetadata,\n },\n async factory({ auth, discovery, plugin }) {\n return DefaultHttpAuthService.create({\n auth,\n discovery,\n pluginId: plugin.getId(),\n });\n },\n});\n"],"names":["parseCookie","AuthenticationError","NotAllowedError","createServiceFactory","coreServices"],"mappings":"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"}
@@ -8,7 +8,7 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
8
8
  var trimEnd__default = /*#__PURE__*/_interopDefaultCompat(trimEnd);
9
9
 
10
10
  function normalizePath(path) {
11
- return `${trimEnd__default.default(path, "/")}/`;
11
+ return `${trimEnd__default.default(path, "/")}/`.toLowerCase();
12
12
  }
13
13
  class DefaultRootHttpRouter {
14
14
  #indexPath;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultRootHttpRouter.cjs.js","sources":["../../../src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RootHttpRouterService } from '@backstage/backend-plugin-api';\nimport { Handler, Router } from 'express';\nimport trimEnd from 'lodash/trimEnd';\n\nfunction normalizePath(path: string): string {\n return `${trimEnd(path, '/')}/`;\n}\n\n/**\n * Options for the {@link DefaultRootHttpRouter} class.\n *\n * @public\n */\nexport interface DefaultRootHttpRouterOptions {\n /**\n * The path to forward all unmatched requests to. Defaults to '/api/app' if\n * not given. Disables index path behavior if false is given.\n */\n indexPath?: string | false;\n}\n\n/**\n * The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for\n * {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.\n *\n * @public\n */\nexport class DefaultRootHttpRouter implements RootHttpRouterService {\n #indexPath?: string;\n\n #router = Router();\n #namedRoutes = Router();\n #indexRouter = Router();\n #existingPaths = new Array<string>();\n\n static create(options?: DefaultRootHttpRouterOptions) {\n let indexPath;\n if (options?.indexPath === false) {\n indexPath = undefined;\n } else if (options?.indexPath === undefined) {\n indexPath = '/api/app';\n } else if (options?.indexPath === '') {\n throw new Error('indexPath option may not be an empty string');\n } else {\n indexPath = options.indexPath;\n }\n return new DefaultRootHttpRouter(indexPath);\n }\n\n private constructor(indexPath?: string) {\n this.#indexPath = indexPath;\n this.#router.use(this.#namedRoutes);\n\n // Any request with a /api/ prefix will skip the index router, even if no named router matches\n this.#router.use('/api/', (_req, _res, next) => {\n next('router');\n });\n\n if (this.#indexPath) {\n this.#router.use(this.#indexRouter);\n }\n }\n\n use(path: string, handler: Handler) {\n if (path.match(/^[/\\s]*$/)) {\n throw new Error(`Root router path may not be empty`);\n }\n const conflictingPath = this.#findConflictingPath(path);\n if (conflictingPath) {\n throw new Error(\n `Path ${path} conflicts with the existing path ${conflictingPath}`,\n );\n }\n this.#existingPaths.push(path);\n this.#namedRoutes.use(path, handler);\n\n if (this.#indexPath === path) {\n this.#indexRouter.use(handler);\n }\n }\n\n handler(): Handler {\n return this.#router;\n }\n\n #findConflictingPath(newPath: string): string | undefined {\n const normalizedNewPath = normalizePath(newPath);\n for (const path of this.#existingPaths) {\n const normalizedPath = normalizePath(path);\n if (normalizedPath.startsWith(normalizedNewPath)) {\n return path;\n }\n if (normalizedNewPath.startsWith(normalizedPath)) {\n return path;\n }\n }\n return undefined;\n }\n}\n"],"names":["trimEnd","Router"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultRootHttpRouter.cjs.js","sources":["../../../src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RootHttpRouterService } from '@backstage/backend-plugin-api';\nimport { Handler, Router } from 'express';\nimport trimEnd from 'lodash/trimEnd';\n\nfunction normalizePath(path: string): string {\n return `${trimEnd(path, '/')}/`.toLowerCase();\n}\n\n/**\n * Options for the {@link DefaultRootHttpRouter} class.\n *\n * @public\n */\nexport interface DefaultRootHttpRouterOptions {\n /**\n * The path to forward all unmatched requests to. Defaults to '/api/app' if\n * not given. Disables index path behavior if false is given.\n */\n indexPath?: string | false;\n}\n\n/**\n * The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for\n * {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.\n *\n * @public\n */\nexport class DefaultRootHttpRouter implements RootHttpRouterService {\n #indexPath?: string;\n\n #router = Router();\n #namedRoutes = Router();\n #indexRouter = Router();\n #existingPaths = new Array<string>();\n\n static create(options?: DefaultRootHttpRouterOptions) {\n let indexPath;\n if (options?.indexPath === false) {\n indexPath = undefined;\n } else if (options?.indexPath === undefined) {\n indexPath = '/api/app';\n } else if (options?.indexPath === '') {\n throw new Error('indexPath option may not be an empty string');\n } else {\n indexPath = options.indexPath;\n }\n return new DefaultRootHttpRouter(indexPath);\n }\n\n private constructor(indexPath?: string) {\n this.#indexPath = indexPath;\n this.#router.use(this.#namedRoutes);\n\n // Any request with a /api/ prefix will skip the index router, even if no named router matches\n this.#router.use('/api/', (_req, _res, next) => {\n next('router');\n });\n\n if (this.#indexPath) {\n this.#router.use(this.#indexRouter);\n }\n }\n\n use(path: string, handler: Handler) {\n if (path.match(/^[/\\s]*$/)) {\n throw new Error(`Root router path may not be empty`);\n }\n const conflictingPath = this.#findConflictingPath(path);\n if (conflictingPath) {\n throw new Error(\n `Path ${path} conflicts with the existing path ${conflictingPath}`,\n );\n }\n this.#existingPaths.push(path);\n this.#namedRoutes.use(path, handler);\n\n if (this.#indexPath === path) {\n this.#indexRouter.use(handler);\n }\n }\n\n handler(): Handler {\n return this.#router;\n }\n\n #findConflictingPath(newPath: string): string | undefined {\n const normalizedNewPath = normalizePath(newPath);\n for (const path of this.#existingPaths) {\n const normalizedPath = normalizePath(path);\n if (normalizedPath.startsWith(normalizedNewPath)) {\n return path;\n }\n if (normalizedNewPath.startsWith(normalizedPath)) {\n return path;\n }\n }\n return undefined;\n }\n}\n"],"names":["trimEnd","Router"],"mappings":"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"}
@@ -4,6 +4,7 @@ var api = require('@opentelemetry/api');
4
4
  var luxon = require('luxon');
5
5
  var Router = require('express-promise-router');
6
6
  var LocalTaskWorker = require('./LocalTaskWorker.cjs.js');
7
+ var TaskStatePoller = require('./TaskStatePoller.cjs.js');
7
8
  var TaskWorker = require('./TaskWorker.cjs.js');
8
9
  var util = require('./util.cjs.js');
9
10
 
@@ -24,6 +25,7 @@ class PluginTaskSchedulerImpl {
24
25
  pluginId;
25
26
  databaseFactory;
26
27
  logger;
28
+ poller;
27
29
  constructor(pluginId, databaseFactory, logger, metrics, rootLifecycle) {
28
30
  this.pluginId = pluginId;
29
31
  this.databaseFactory = databaseFactory;
@@ -84,11 +86,18 @@ class PluginTaskSchedulerImpl {
84
86
  this.shutdownInitiated.then(() => abortController.abort());
85
87
  if (scope === "global") {
86
88
  const knex = await this.databaseFactory();
89
+ if (!this.poller) {
90
+ this.poller = new TaskStatePoller.TaskStatePoller({
91
+ knex,
92
+ logger: this.logger.child({ type: "taskStatePoller" })
93
+ });
94
+ }
87
95
  const worker = new TaskWorker.TaskWorker(
88
96
  task.id,
89
97
  this.instrumentedFunction(task, scope),
90
98
  knex,
91
- this.logger.child({ task: task.id })
99
+ this.logger.child({ task: task.id }),
100
+ this.poller
92
101
  );
93
102
  await worker.start(settings, { signal: abortController.signal });
94
103
  this.globalWorkersById.set(task.id, worker);
@@ -1 +1 @@
1
- {"version":3,"file":"PluginTaskSchedulerImpl.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n LoggerService,\n RootLifecycleService,\n SchedulerService,\n SchedulerServiceTaskDescriptor,\n SchedulerServiceTaskFunction,\n SchedulerServiceTaskInvocationDefinition,\n SchedulerServiceTaskRunner,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { trace } from '@opentelemetry/api';\nimport {\n MetricsService,\n MetricsServiceCounter,\n MetricsServiceGauge,\n MetricsServiceHistogram,\n} from '@backstage/backend-plugin-api/alpha';\nimport { Knex } from 'knex';\nimport { Duration } from 'luxon';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { LocalTaskWorker } from './LocalTaskWorker';\nimport { TaskWorker } from './TaskWorker';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, TRACER_ID, validateId } from './util';\n\nconst tracer = trace.getTracer(TRACER_ID);\n\n/**\n * Implements the actual task management.\n */\nexport class PluginTaskSchedulerImpl implements SchedulerService {\n private readonly localWorkersById = new Map<string, LocalTaskWorker>();\n private readonly globalWorkersById = new Map<string, TaskWorker>();\n private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = [];\n private readonly shutdownInitiated: Promise<boolean>;\n\n private readonly counter: MetricsServiceCounter;\n private readonly duration: MetricsServiceHistogram;\n private readonly lastStarted: MetricsServiceGauge;\n private readonly lastCompleted: MetricsServiceGauge;\n\n private readonly pluginId: string;\n private readonly databaseFactory: () => Promise<Knex>;\n private readonly logger: LoggerService;\n\n constructor(\n pluginId: string,\n databaseFactory: () => Promise<Knex>,\n logger: LoggerService,\n metrics: MetricsService,\n rootLifecycle: RootLifecycleService,\n ) {\n this.pluginId = pluginId;\n this.databaseFactory = databaseFactory;\n this.logger = logger;\n this.counter = metrics.createCounter('backend_tasks.task.runs.count', {\n description: 'Total number of times a task has been run',\n });\n this.duration = metrics.createHistogram(\n 'backend_tasks.task.runs.duration',\n {\n description: 'Histogram of task run durations',\n unit: 'seconds',\n },\n );\n this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', {\n description: 'Epoch timestamp seconds when the task was last started',\n unit: 'seconds',\n });\n this.lastCompleted = metrics.createGauge(\n 'backend_tasks.task.runs.completed',\n {\n description: 'Epoch timestamp seconds when the task was last completed',\n unit: 'seconds',\n },\n );\n this.shutdownInitiated = new Promise(shutdownInitiated => {\n rootLifecycle.addShutdownHook(() => shutdownInitiated(true));\n });\n }\n\n async triggerTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.trigger();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.trigger(knex, id);\n }\n\n async cancelTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.cancel();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.cancel(knex, id);\n }\n\n async scheduleTask(\n task: SchedulerServiceTaskScheduleDefinition &\n SchedulerServiceTaskInvocationDefinition,\n ): Promise<void> {\n validateId(task.id);\n const scope = task.scope ?? 'global';\n\n const settings: TaskSettingsV2 = {\n version: 2,\n cadence: parseDuration(task.frequency),\n initialDelayDuration:\n task.initialDelay && parseDuration(task.initialDelay),\n timeoutAfterDuration: parseDuration(task.timeout),\n };\n\n // Delegated abort controller that will abort either when the provided\n // controller aborts, or when a root lifecycle shutdown happens\n const abortController = delegateAbortController(task.signal);\n this.shutdownInitiated.then(() => abortController.abort());\n\n if (scope === 'global') {\n const knex = await this.databaseFactory();\n const worker = new TaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n knex,\n this.logger.child({ task: task.id }),\n );\n await worker.start(settings, { signal: abortController.signal });\n this.globalWorkersById.set(task.id, worker);\n } else {\n const worker = new LocalTaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n this.logger.child({ task: task.id }),\n );\n worker.start(settings, { signal: abortController.signal });\n this.localWorkersById.set(task.id, worker);\n }\n\n this.allScheduledTasks.push({\n id: task.id,\n scope: scope,\n settings: settings,\n });\n }\n\n createScheduledTaskRunner(\n schedule: SchedulerServiceTaskScheduleDefinition,\n ): SchedulerServiceTaskRunner {\n return {\n run: async task => {\n await this.scheduleTask({ ...task, ...schedule });\n },\n };\n }\n\n async getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]> {\n return this.allScheduledTasks;\n }\n\n getRouter(): express.Router {\n const router = Router();\n\n router.get('/.backstage/scheduler/v1/tasks', async (_, res) => {\n const globalState = await TaskWorker.taskStates(\n await this.databaseFactory(),\n );\n\n const tasks = new Array<TaskApiTasksResponse>();\n for (const task of this.allScheduledTasks) {\n tasks.push({\n taskId: task.id,\n pluginId: this.pluginId,\n scope: task.scope,\n settings: task.settings,\n taskState:\n this.localWorkersById.get(task.id)?.taskState() ??\n globalState.get(task.id) ??\n null,\n workerState:\n this.localWorkersById.get(task.id)?.workerState() ??\n this.globalWorkersById.get(task.id)?.workerState() ??\n null,\n });\n }\n\n res.json({ tasks });\n });\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/trigger',\n async (req, res) => {\n const { id } = req.params;\n await this.triggerTask(id);\n res.status(200).end();\n },\n );\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/cancel',\n async (req, res) => {\n const { id } = req.params;\n await this.cancelTask(id);\n res.status(200).end();\n },\n );\n\n return router;\n }\n\n private instrumentedFunction(\n task: SchedulerServiceTaskInvocationDefinition,\n scope: string,\n ): SchedulerServiceTaskFunction {\n return async abort => {\n const labels: Record<string, string> = {\n taskId: task.id,\n scope,\n };\n this.counter.add(1, { ...labels, result: 'started' });\n this.lastStarted.record(Date.now() / 1000, { taskId: task.id });\n\n const startTime = process.hrtime();\n\n try {\n await tracer.startActiveSpan(`task ${task.id}`, async span => {\n try {\n span.setAttributes(labels);\n await task.fn(abort);\n } catch (error) {\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n }\n });\n labels.result = 'completed';\n } catch (ex) {\n labels.result = 'failed';\n throw ex;\n } finally {\n const delta = process.hrtime(startTime);\n const endTime = delta[0] + delta[1] / 1e9;\n this.counter.add(1, labels);\n this.duration.record(endTime, labels);\n this.lastCompleted.record(Date.now() / 1000, labels);\n }\n };\n }\n}\n\nexport function parseDuration(\n frequency: SchedulerServiceTaskScheduleDefinition['frequency'],\n): string {\n if (typeof frequency === 'object' && 'cron' in frequency) {\n return frequency.cron;\n }\n if (typeof frequency === 'object' && 'trigger' in frequency) {\n return frequency.trigger;\n }\n\n const parsed = Duration.isDuration(frequency)\n ? frequency\n : Duration.fromObject(frequency);\n\n if (!parsed.isValid) {\n throw new Error(\n `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`,\n );\n }\n\n return parsed.toISO()!;\n}\n"],"names":["trace","TRACER_ID","TaskWorker","validateId","delegateAbortController","LocalTaskWorker","Router","Duration"],"mappings":"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"}
1
+ {"version":3,"file":"PluginTaskSchedulerImpl.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n LoggerService,\n RootLifecycleService,\n SchedulerService,\n SchedulerServiceTaskDescriptor,\n SchedulerServiceTaskFunction,\n SchedulerServiceTaskInvocationDefinition,\n SchedulerServiceTaskRunner,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { trace } from '@opentelemetry/api';\nimport {\n MetricsService,\n MetricsServiceCounter,\n MetricsServiceGauge,\n MetricsServiceHistogram,\n} from '@backstage/backend-plugin-api/alpha';\nimport { Knex } from 'knex';\nimport { Duration } from 'luxon';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { LocalTaskWorker } from './LocalTaskWorker';\nimport { TaskStatePoller } from './TaskStatePoller';\nimport { TaskWorker } from './TaskWorker';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, TRACER_ID, validateId } from './util';\n\nconst tracer = trace.getTracer(TRACER_ID);\n\n/**\n * Implements the actual task management.\n */\nexport class PluginTaskSchedulerImpl implements SchedulerService {\n private readonly localWorkersById = new Map<string, LocalTaskWorker>();\n private readonly globalWorkersById = new Map<string, TaskWorker>();\n private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = [];\n private readonly shutdownInitiated: Promise<boolean>;\n\n private readonly counter: MetricsServiceCounter;\n private readonly duration: MetricsServiceHistogram;\n private readonly lastStarted: MetricsServiceGauge;\n private readonly lastCompleted: MetricsServiceGauge;\n\n private readonly pluginId: string;\n private readonly databaseFactory: () => Promise<Knex>;\n private readonly logger: LoggerService;\n private poller?: TaskStatePoller;\n\n constructor(\n pluginId: string,\n databaseFactory: () => Promise<Knex>,\n logger: LoggerService,\n metrics: MetricsService,\n rootLifecycle: RootLifecycleService,\n ) {\n this.pluginId = pluginId;\n this.databaseFactory = databaseFactory;\n this.logger = logger;\n this.counter = metrics.createCounter('backend_tasks.task.runs.count', {\n description: 'Total number of times a task has been run',\n });\n this.duration = metrics.createHistogram(\n 'backend_tasks.task.runs.duration',\n {\n description: 'Histogram of task run durations',\n unit: 'seconds',\n },\n );\n this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', {\n description: 'Epoch timestamp seconds when the task was last started',\n unit: 'seconds',\n });\n this.lastCompleted = metrics.createGauge(\n 'backend_tasks.task.runs.completed',\n {\n description: 'Epoch timestamp seconds when the task was last completed',\n unit: 'seconds',\n },\n );\n this.shutdownInitiated = new Promise(shutdownInitiated => {\n rootLifecycle.addShutdownHook(() => shutdownInitiated(true));\n });\n }\n\n async triggerTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.trigger();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.trigger(knex, id);\n }\n\n async cancelTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.cancel();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.cancel(knex, id);\n }\n\n async scheduleTask(\n task: SchedulerServiceTaskScheduleDefinition &\n SchedulerServiceTaskInvocationDefinition,\n ): Promise<void> {\n validateId(task.id);\n const scope = task.scope ?? 'global';\n\n const settings: TaskSettingsV2 = {\n version: 2,\n cadence: parseDuration(task.frequency),\n initialDelayDuration:\n task.initialDelay && parseDuration(task.initialDelay),\n timeoutAfterDuration: parseDuration(task.timeout),\n };\n\n // Delegated abort controller that will abort either when the provided\n // controller aborts, or when a root lifecycle shutdown happens\n const abortController = delegateAbortController(task.signal);\n this.shutdownInitiated.then(() => abortController.abort());\n\n if (scope === 'global') {\n const knex = await this.databaseFactory();\n\n if (!this.poller) {\n this.poller = new TaskStatePoller({\n knex,\n logger: this.logger.child({ type: 'taskStatePoller' }),\n });\n }\n\n const worker = new TaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n knex,\n this.logger.child({ task: task.id }),\n this.poller,\n );\n await worker.start(settings, { signal: abortController.signal });\n this.globalWorkersById.set(task.id, worker);\n } else {\n const worker = new LocalTaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n this.logger.child({ task: task.id }),\n );\n worker.start(settings, { signal: abortController.signal });\n this.localWorkersById.set(task.id, worker);\n }\n\n this.allScheduledTasks.push({\n id: task.id,\n scope: scope,\n settings: settings,\n });\n }\n\n createScheduledTaskRunner(\n schedule: SchedulerServiceTaskScheduleDefinition,\n ): SchedulerServiceTaskRunner {\n return {\n run: async task => {\n await this.scheduleTask({ ...task, ...schedule });\n },\n };\n }\n\n async getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]> {\n return this.allScheduledTasks;\n }\n\n getRouter(): express.Router {\n const router = Router();\n\n router.get('/.backstage/scheduler/v1/tasks', async (_, res) => {\n const globalState = await TaskWorker.taskStates(\n await this.databaseFactory(),\n );\n\n const tasks = new Array<TaskApiTasksResponse>();\n for (const task of this.allScheduledTasks) {\n tasks.push({\n taskId: task.id,\n pluginId: this.pluginId,\n scope: task.scope,\n settings: task.settings,\n taskState:\n this.localWorkersById.get(task.id)?.taskState() ??\n globalState.get(task.id) ??\n null,\n workerState:\n this.localWorkersById.get(task.id)?.workerState() ??\n this.globalWorkersById.get(task.id)?.workerState() ??\n null,\n });\n }\n\n res.json({ tasks });\n });\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/trigger',\n async (req, res) => {\n const { id } = req.params;\n await this.triggerTask(id);\n res.status(200).end();\n },\n );\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/cancel',\n async (req, res) => {\n const { id } = req.params;\n await this.cancelTask(id);\n res.status(200).end();\n },\n );\n\n return router;\n }\n\n private instrumentedFunction(\n task: SchedulerServiceTaskInvocationDefinition,\n scope: string,\n ): SchedulerServiceTaskFunction {\n return async abort => {\n const labels: Record<string, string> = {\n taskId: task.id,\n scope,\n };\n this.counter.add(1, { ...labels, result: 'started' });\n this.lastStarted.record(Date.now() / 1000, { taskId: task.id });\n\n const startTime = process.hrtime();\n\n try {\n await tracer.startActiveSpan(`task ${task.id}`, async span => {\n try {\n span.setAttributes(labels);\n await task.fn(abort);\n } catch (error) {\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n }\n });\n labels.result = 'completed';\n } catch (ex) {\n labels.result = 'failed';\n throw ex;\n } finally {\n const delta = process.hrtime(startTime);\n const endTime = delta[0] + delta[1] / 1e9;\n this.counter.add(1, labels);\n this.duration.record(endTime, labels);\n this.lastCompleted.record(Date.now() / 1000, labels);\n }\n };\n }\n}\n\nexport function parseDuration(\n frequency: SchedulerServiceTaskScheduleDefinition['frequency'],\n): string {\n if (typeof frequency === 'object' && 'cron' in frequency) {\n return frequency.cron;\n }\n if (typeof frequency === 'object' && 'trigger' in frequency) {\n return frequency.trigger;\n }\n\n const parsed = Duration.isDuration(frequency)\n ? frequency\n : Duration.fromObject(frequency);\n\n if (!parsed.isValid) {\n throw new Error(\n `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`,\n );\n }\n\n return parsed.toISO()!;\n}\n"],"names":["trace","TRACER_ID","TaskWorker","validateId","delegateAbortController","TaskStatePoller","LocalTaskWorker","Router","Duration"],"mappings":"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"}
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ var types = require('@backstage/types');
4
+ var tables = require('../database/tables.cjs.js');
5
+ var types$1 = require('./types.cjs.js');
6
+
7
+ const MIN_POLL_INTERVAL_MS = 100;
8
+ class TaskStatePoller {
9
+ #knex;
10
+ #logger;
11
+ #waiters = /* @__PURE__ */ new Map();
12
+ #pollTimer;
13
+ #pollTimerDueAt;
14
+ #nextPollAllowedAt;
15
+ #pollCycleRunning = false;
16
+ constructor(options) {
17
+ this.#knex = options.knex;
18
+ this.#logger = options.logger;
19
+ }
20
+ waitForTask(taskId, options) {
21
+ if (options.signal.aborted) {
22
+ return Promise.resolve({ result: "abort" });
23
+ }
24
+ const requestedPollIntervalMs = options.pollInterval.as("milliseconds");
25
+ if (!Number.isFinite(requestedPollIntervalMs) || requestedPollIntervalMs <= 0) {
26
+ throw new TypeError("pollInterval must be a finite positive duration");
27
+ }
28
+ const pollIntervalMs = Math.max(
29
+ requestedPollIntervalMs,
30
+ MIN_POLL_INTERVAL_MS
31
+ );
32
+ const deferred = types.createDeferred();
33
+ const waiter = {
34
+ taskId,
35
+ deferred,
36
+ pollIntervalMs,
37
+ signal: options.signal,
38
+ abortHandler: () => {
39
+ this.#removeWaiter(waiter);
40
+ deferred.resolve({ result: "abort" });
41
+ }
42
+ };
43
+ let waiters = this.#waiters.get(taskId);
44
+ if (!waiters) {
45
+ waiters = /* @__PURE__ */ new Set();
46
+ this.#waiters.set(taskId, waiters);
47
+ }
48
+ waiters.add(waiter);
49
+ options.signal.addEventListener("abort", waiter.abortHandler, {
50
+ once: true
51
+ });
52
+ this.#ensurePolling();
53
+ this.#rescheduleFor(waiter);
54
+ return deferred;
55
+ }
56
+ #ensurePolling() {
57
+ if (this.#pollCycleRunning || this.#pollTimer) {
58
+ return;
59
+ }
60
+ const delayMs = (this.#nextPollAllowedAt ?? 0) - Date.now();
61
+ if (delayMs > 0) {
62
+ this.#schedulePoll(delayMs);
63
+ return;
64
+ }
65
+ this.#pollCycleRunning = true;
66
+ Promise.resolve().then(() => this.#runPollCycle());
67
+ }
68
+ async #runPollCycle() {
69
+ const cyclePollIntervalMs = this.#nextPollIntervalMs();
70
+ if (!Number.isFinite(cyclePollIntervalMs)) {
71
+ this.#pollCycleRunning = false;
72
+ this.#nextPollAllowedAt = void 0;
73
+ return;
74
+ }
75
+ try {
76
+ await this.#poll();
77
+ } catch (error) {
78
+ this.#logger.warn("Task state poll failed", error);
79
+ }
80
+ this.#pollCycleRunning = false;
81
+ const currentPollIntervalMs = this.#nextPollIntervalMs();
82
+ const nextPollCooldownMs = Math.min(
83
+ cyclePollIntervalMs,
84
+ currentPollIntervalMs
85
+ );
86
+ this.#nextPollAllowedAt = Date.now() + nextPollCooldownMs;
87
+ if (this.#waiters.size > 0) {
88
+ this.#schedulePoll(currentPollIntervalMs);
89
+ }
90
+ }
91
+ #schedulePoll(delayMs) {
92
+ this.#pollTimerDueAt = Date.now() + delayMs;
93
+ this.#pollTimer = setTimeout(() => {
94
+ this.#pollTimer = void 0;
95
+ this.#pollTimerDueAt = void 0;
96
+ if (this.#waiters.size === 0) {
97
+ return;
98
+ }
99
+ this.#pollCycleRunning = true;
100
+ this.#runPollCycle();
101
+ }, delayMs);
102
+ }
103
+ #rescheduleFor(waiter) {
104
+ if (!this.#pollTimer || this.#pollTimerDueAt === void 0) {
105
+ return;
106
+ }
107
+ const desiredPollAt = Date.now() + waiter.pollIntervalMs;
108
+ if (desiredPollAt >= this.#pollTimerDueAt) {
109
+ return;
110
+ }
111
+ clearTimeout(this.#pollTimer);
112
+ this.#schedulePoll(waiter.pollIntervalMs);
113
+ }
114
+ #nextPollIntervalMs() {
115
+ let intervalMs = Infinity;
116
+ for (const waiters of this.#waiters.values()) {
117
+ for (const waiter of waiters) {
118
+ intervalMs = Math.min(intervalMs, waiter.pollIntervalMs);
119
+ }
120
+ }
121
+ return intervalMs;
122
+ }
123
+ async #poll() {
124
+ const taskIds = [...this.#waiters.keys()];
125
+ if (taskIds.length === 0) {
126
+ return;
127
+ }
128
+ const rows = await this.#knex(tables.DB_TASKS_TABLE).whereIn("id", taskIds).select({
129
+ id: "id",
130
+ settingsJson: "settings_json",
131
+ ready: this.#knex.raw(
132
+ `CASE
133
+ WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE
134
+ ELSE FALSE
135
+ END`,
136
+ [this.#knex.fn.now()]
137
+ )
138
+ });
139
+ const foundIds = /* @__PURE__ */ new Set();
140
+ for (const row of rows) {
141
+ foundIds.add(row.id);
142
+ if (!row.ready) {
143
+ continue;
144
+ }
145
+ try {
146
+ const settings = types$1.taskSettingsV2Schema.parse(
147
+ JSON.parse(row.settingsJson)
148
+ );
149
+ this.#resolveAll(row.id, { result: "ready", settings });
150
+ } catch (error) {
151
+ this.#logger.info(
152
+ `Task "${row.id}" is no longer able to parse task settings; aborting and assuming that a newer version of the task has been issued and is being handled by other workers, ${error}`
153
+ );
154
+ this.#resolveAll(row.id, { result: "abort" });
155
+ }
156
+ }
157
+ for (const taskId of taskIds) {
158
+ if (!foundIds.has(taskId)) {
159
+ this.#logger.info(
160
+ `No longer able to find task "${taskId}"; aborting and assuming that it has been unregistered or expired`
161
+ );
162
+ this.#resolveAll(taskId, { result: "abort" });
163
+ }
164
+ }
165
+ }
166
+ #resolveAll(taskId, result) {
167
+ const waiters = this.#waiters.get(taskId);
168
+ if (!waiters) {
169
+ return;
170
+ }
171
+ this.#waiters.delete(taskId);
172
+ for (const waiter of waiters) {
173
+ this.#cleanupWaiter(waiter);
174
+ waiter.deferred.resolve(result);
175
+ }
176
+ }
177
+ #removeWaiter(waiter) {
178
+ const waiters = this.#waiters.get(waiter.taskId);
179
+ if (!waiters) {
180
+ return;
181
+ }
182
+ waiters.delete(waiter);
183
+ this.#cleanupWaiter(waiter);
184
+ if (waiters.size === 0) {
185
+ this.#waiters.delete(waiter.taskId);
186
+ }
187
+ if (this.#waiters.size === 0 && this.#pollTimer) {
188
+ clearTimeout(this.#pollTimer);
189
+ this.#pollTimer = void 0;
190
+ this.#pollTimerDueAt = void 0;
191
+ this.#nextPollAllowedAt = void 0;
192
+ }
193
+ }
194
+ #cleanupWaiter(waiter) {
195
+ waiter.signal.removeEventListener("abort", waiter.abortHandler);
196
+ }
197
+ }
198
+
199
+ exports.TaskStatePoller = TaskStatePoller;
200
+ //# sourceMappingURL=TaskStatePoller.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskStatePoller.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/TaskStatePoller.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { createDeferred, DeferredPromise } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { Duration } from 'luxon';\nimport { DB_TASKS_TABLE, DbTasksRow } from '../database/tables';\nimport { TaskSettingsV2, taskSettingsV2Schema } from './types';\n\ntype TaskPollResult =\n | { result: 'abort' }\n | { result: 'ready'; settings: TaskSettingsV2 };\n\ninterface PendingWaiter {\n taskId: string;\n deferred: DeferredPromise<TaskPollResult>;\n pollIntervalMs: number;\n signal: AbortSignal;\n abortHandler: () => void;\n}\n\nconst MIN_POLL_INTERVAL_MS = 100;\n\n/**\n * Batches readiness checks for all waiting tasks of a plugin.\n */\nexport class TaskStatePoller {\n readonly #knex: Knex;\n readonly #logger: LoggerService;\n readonly #waiters = new Map<string, Set<PendingWaiter>>();\n #pollTimer: ReturnType<typeof setTimeout> | undefined;\n #pollTimerDueAt: number | undefined;\n #nextPollAllowedAt: number | undefined;\n #pollCycleRunning = false;\n\n constructor(options: { knex: Knex; logger: LoggerService }) {\n this.#knex = options.knex;\n this.#logger = options.logger;\n }\n\n waitForTask(\n taskId: string,\n options: { signal: AbortSignal; pollInterval: Duration },\n ): Promise<TaskPollResult> {\n if (options.signal.aborted) {\n return Promise.resolve({ result: 'abort' });\n }\n\n const requestedPollIntervalMs = options.pollInterval.as('milliseconds');\n if (\n !Number.isFinite(requestedPollIntervalMs) ||\n requestedPollIntervalMs <= 0\n ) {\n throw new TypeError('pollInterval must be a finite positive duration');\n }\n const pollIntervalMs = Math.max(\n requestedPollIntervalMs,\n MIN_POLL_INTERVAL_MS,\n );\n\n const deferred = createDeferred<TaskPollResult>();\n const waiter: PendingWaiter = {\n taskId,\n deferred,\n pollIntervalMs,\n signal: options.signal,\n abortHandler: () => {\n this.#removeWaiter(waiter);\n deferred.resolve({ result: 'abort' });\n },\n };\n\n let waiters = this.#waiters.get(taskId);\n if (!waiters) {\n waiters = new Set();\n this.#waiters.set(taskId, waiters);\n }\n waiters.add(waiter);\n options.signal.addEventListener('abort', waiter.abortHandler, {\n once: true,\n });\n\n this.#ensurePolling();\n this.#rescheduleFor(waiter);\n return deferred;\n }\n\n #ensurePolling(): void {\n if (this.#pollCycleRunning || this.#pollTimer) {\n return;\n }\n\n const delayMs = (this.#nextPollAllowedAt ?? 0) - Date.now();\n if (delayMs > 0) {\n this.#schedulePoll(delayMs);\n return;\n }\n\n this.#pollCycleRunning = true;\n Promise.resolve().then(() => this.#runPollCycle());\n }\n\n async #runPollCycle(): Promise<void> {\n const cyclePollIntervalMs = this.#nextPollIntervalMs();\n if (!Number.isFinite(cyclePollIntervalMs)) {\n this.#pollCycleRunning = false;\n this.#nextPollAllowedAt = undefined;\n return;\n }\n\n try {\n await this.#poll();\n } catch (error) {\n this.#logger.warn('Task state poll failed', error);\n }\n this.#pollCycleRunning = false;\n\n const currentPollIntervalMs = this.#nextPollIntervalMs();\n const nextPollCooldownMs = Math.min(\n cyclePollIntervalMs,\n currentPollIntervalMs,\n );\n this.#nextPollAllowedAt = Date.now() + nextPollCooldownMs;\n if (this.#waiters.size > 0) {\n this.#schedulePoll(currentPollIntervalMs);\n }\n }\n\n #schedulePoll(delayMs: number): void {\n this.#pollTimerDueAt = Date.now() + delayMs;\n this.#pollTimer = setTimeout(() => {\n this.#pollTimer = undefined;\n this.#pollTimerDueAt = undefined;\n if (this.#waiters.size === 0) {\n return;\n }\n this.#pollCycleRunning = true;\n this.#runPollCycle();\n }, delayMs);\n }\n\n #rescheduleFor(waiter: PendingWaiter): void {\n if (!this.#pollTimer || this.#pollTimerDueAt === undefined) {\n return;\n }\n\n const desiredPollAt = Date.now() + waiter.pollIntervalMs;\n if (desiredPollAt >= this.#pollTimerDueAt) {\n return;\n }\n\n clearTimeout(this.#pollTimer);\n this.#schedulePoll(waiter.pollIntervalMs);\n }\n\n #nextPollIntervalMs(): number {\n let intervalMs = Infinity;\n for (const waiters of this.#waiters.values()) {\n for (const waiter of waiters) {\n intervalMs = Math.min(intervalMs, waiter.pollIntervalMs);\n }\n }\n return intervalMs;\n }\n\n async #poll(): Promise<void> {\n const taskIds = [...this.#waiters.keys()];\n if (taskIds.length === 0) {\n return;\n }\n\n const rows = await this.#knex<DbTasksRow>(DB_TASKS_TABLE)\n .whereIn('id', taskIds)\n .select({\n id: 'id',\n settingsJson: 'settings_json',\n ready: this.#knex.raw(\n `CASE\n WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE\n ELSE FALSE\n END`,\n [this.#knex.fn.now()],\n ),\n });\n\n const foundIds = new Set<string>();\n\n for (const row of rows) {\n foundIds.add(row.id);\n\n if (!row.ready) {\n continue;\n }\n\n try {\n const settings = taskSettingsV2Schema.parse(\n JSON.parse(row.settingsJson),\n );\n this.#resolveAll(row.id, { result: 'ready', settings });\n } catch (error) {\n this.#logger.info(\n `Task \"${row.id}\" is no longer able to parse task settings; aborting and assuming that a newer version of the task has been issued and is being handled by other workers, ${error}`,\n );\n this.#resolveAll(row.id, { result: 'abort' });\n }\n }\n\n for (const taskId of taskIds) {\n if (!foundIds.has(taskId)) {\n this.#logger.info(\n `No longer able to find task \"${taskId}\"; aborting and assuming that it has been unregistered or expired`,\n );\n this.#resolveAll(taskId, { result: 'abort' });\n }\n }\n }\n\n #resolveAll(taskId: string, result: TaskPollResult): void {\n const waiters = this.#waiters.get(taskId);\n if (!waiters) {\n return;\n }\n this.#waiters.delete(taskId);\n\n for (const waiter of waiters) {\n this.#cleanupWaiter(waiter);\n waiter.deferred.resolve(result);\n }\n }\n\n #removeWaiter(waiter: PendingWaiter): void {\n const waiters = this.#waiters.get(waiter.taskId);\n if (!waiters) {\n return;\n }\n waiters.delete(waiter);\n this.#cleanupWaiter(waiter);\n if (waiters.size === 0) {\n this.#waiters.delete(waiter.taskId);\n }\n if (this.#waiters.size === 0 && this.#pollTimer) {\n clearTimeout(this.#pollTimer);\n this.#pollTimer = undefined;\n this.#pollTimerDueAt = undefined;\n this.#nextPollAllowedAt = undefined;\n }\n }\n\n #cleanupWaiter(waiter: PendingWaiter): void {\n waiter.signal.removeEventListener('abort', waiter.abortHandler);\n }\n}\n"],"names":["createDeferred","DB_TASKS_TABLE","taskSettingsV2Schema"],"mappings":"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"}
@@ -17,12 +17,14 @@ class TaskWorker {
17
17
  fn;
18
18
  knex;
19
19
  logger;
20
+ poller;
20
21
  workCheckFrequency;
21
- constructor(taskId, fn, knex, logger, workCheckFrequency = DEFAULT_WORK_CHECK_FREQUENCY) {
22
+ constructor(taskId, fn, knex, logger, poller, workCheckFrequency = DEFAULT_WORK_CHECK_FREQUENCY) {
22
23
  this.taskId = taskId;
23
24
  this.fn = fn;
24
25
  this.knex = knex;
25
26
  this.logger = logger;
27
+ this.poller = poller;
26
28
  this.workCheckFrequency = workCheckFrequency;
27
29
  }
28
30
  async start(settings, options) {
@@ -35,10 +37,10 @@ class TaskWorker {
35
37
  `Registered scheduled task: ${this.taskId}, ${JSON.stringify(settings)}`
36
38
  );
37
39
  let workCheckFrequency = this.workCheckFrequency;
38
- const isDuration = settings?.cadence.startsWith("P");
40
+ const isDuration = settings.cadence.startsWith("P");
39
41
  if (isDuration) {
40
42
  const cadence = luxon.Duration.fromISO(settings.cadence);
41
- if (cadence < workCheckFrequency) {
43
+ if (cadence.as("milliseconds") > 0 && cadence < workCheckFrequency) {
42
44
  workCheckFrequency = cadence;
43
45
  }
44
46
  }
@@ -48,11 +50,13 @@ class TaskWorker {
48
50
  try {
49
51
  await this.performInitialWait(settings, options.signal);
50
52
  while (!options.signal.aborted) {
51
- const runResult = await this.runOnce(options.signal);
53
+ const runResult = await this.runOnce(
54
+ options.signal,
55
+ workCheckFrequency
56
+ );
52
57
  if (runResult.result === "abort") {
53
58
  break;
54
59
  }
55
- await util.sleep(workCheckFrequency, options.signal);
56
60
  }
57
61
  this.logger.info(`Task worker finished: ${this.taskId}`);
58
62
  attemptNum = 0;
@@ -150,19 +154,24 @@ class TaskWorker {
150
154
  }
151
155
  /**
152
156
  * Makes a single attempt at running the task to completion, if ready.
153
- *
154
- * @returns The outcome of the attempt
157
+ * Waits for the shared poller to signal readiness instead of querying
158
+ * the database directly.
155
159
  */
156
- async runOnce(signal) {
157
- const findResult = await this.findReadyTask();
158
- if (findResult.result === "not-ready-yet" || findResult.result === "abort") {
160
+ async runOnce(signal, workCheckFrequency) {
161
+ const findResult = await this.poller.waitForTask(this.taskId, {
162
+ signal,
163
+ pollInterval: workCheckFrequency
164
+ });
165
+ if (findResult.result === "abort") {
159
166
  return findResult;
160
167
  }
161
- const taskSettings = findResult.settings;
168
+ return this.claimAndRun(findResult.settings, signal);
169
+ }
170
+ async claimAndRun(taskSettings, signal) {
162
171
  const ticket = node_crypto.randomUUID();
163
172
  const claimed = await this.tryClaimTask(ticket, taskSettings);
164
173
  if (!claimed) {
165
- return { result: "not-ready-yet" };
174
+ return { result: "claim-lost" };
166
175
  }
167
176
  const taskAbortController = util.delegateAbortController(signal);
168
177
  const timeoutHandle = setTimeout(() => {
@@ -280,39 +289,6 @@ class TaskWorker {
280
289
  );
281
290
  }
282
291
  }
283
- /**
284
- * Check if the task is ready to run
285
- */
286
- async findReadyTask() {
287
- const [row] = await this.knex(tables.DB_TASKS_TABLE).where("id", "=", this.taskId).select({
288
- settingsJson: "settings_json",
289
- ready: this.knex.raw(
290
- `CASE
291
- WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE
292
- ELSE FALSE
293
- END`,
294
- [this.knex.fn.now()]
295
- )
296
- });
297
- if (!row) {
298
- this.logger.info(
299
- "No longer able to find task; aborting and assuming that it has been unregistered or expired"
300
- );
301
- return { result: "abort" };
302
- } else if (!row.ready) {
303
- return { result: "not-ready-yet" };
304
- }
305
- try {
306
- const obj = JSON.parse(row.settingsJson);
307
- const settings = types.taskSettingsV2Schema.parse(obj);
308
- return { result: "ready", settings };
309
- } catch (e) {
310
- this.logger.info(
311
- `Task "${this.taskId}" is no longer able to parse task settings; aborting and assuming that a newer version of the task has been issued and being handled by other workers, ${e}`
312
- );
313
- return { result: "abort" };
314
- }
315
- }
316
292
  /**
317
293
  * Attempts to claim a task that's ready for execution, on this worker's
318
294
  * behalf. We should not attempt to perform the work unless the claim really