@shipfox/api-runners 12.3.0 → 12.4.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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +22 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -2
- package/dist/config.js.map +1 -1
- package/dist/core/demand.d.ts +4 -2
- package/dist/core/demand.d.ts.map +1 -1
- package/dist/core/demand.js +22 -6
- package/dist/core/demand.js.map +1 -1
- package/dist/core/entities/reservation.d.ts +1 -0
- package/dist/core/entities/reservation.d.ts.map +1 -1
- package/dist/core/entities/reservation.js.map +1 -1
- package/dist/core/entities/runner-instance.d.ts +3 -0
- package/dist/core/entities/runner-instance.d.ts.map +1 -1
- package/dist/core/entities/runner-instance.js.map +1 -1
- package/dist/core/runner-activation.d.ts +4 -1
- package/dist/core/runner-activation.d.ts.map +1 -1
- package/dist/core/runner-activation.js +32 -3
- package/dist/core/runner-activation.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts +8 -4
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +55 -9
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/core/runner-instances.d.ts +1 -0
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js +1 -0
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/db/db.d.ts +68 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +1 -1
- package/dist/db/ephemeral-registration-tokens.js.map +1 -1
- package/dist/db/reservation-locks.d.ts +6 -0
- package/dist/db/reservation-locks.d.ts.map +1 -0
- package/dist/db/reservation-locks.js +15 -0
- package/dist/db/reservation-locks.js.map +1 -0
- package/dist/db/reservations.d.ts +17 -3
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +246 -51
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +17 -0
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +103 -8
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +2 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +124 -24
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-states.d.ts +2 -0
- package/dist/db/runner-states.d.ts.map +1 -0
- package/dist/db/runner-states.js +7 -0
- package/dist/db/runner-states.js.map +1 -0
- package/dist/db/schema/reservations.d.ts +18 -0
- package/dist/db/schema/reservations.d.ts.map +1 -1
- package/dist/db/schema/reservations.js +7 -1
- package/dist/db/schema/reservations.js.map +1 -1
- package/dist/db/schema/runner-instances.d.ts +18 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +8 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/metrics/index.d.ts +2 -2
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +24 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +23 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/dto/poll-demand.d.ts.map +1 -1
- package/dist/presentation/dto/poll-demand.js +3 -0
- package/dist/presentation/dto/poll-demand.js.map +1 -1
- package/dist/presentation/dto/runner-instances.d.ts +4 -1
- package/dist/presentation/dto/runner-instances.d.ts.map +1 -1
- package/dist/presentation/dto/runner-instances.js +4 -1
- package/dist/presentation/dto/runner-instances.js.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.js +3 -3
- package/dist/presentation/routes/assign-runner-instances.js.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +1 -0
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.js +4 -2
- package/dist/presentation/routes/reconcile-runner-instances.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +4 -1
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +11 -6
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0005_previous_lenny_balinger.sql +5 -0
- package/drizzle/meta/0005_snapshot.json +2220 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +2 -2
- package/src/config.test.ts +22 -0
- package/src/config.ts +15 -2
- package/src/core/demand.test.ts +192 -3
- package/src/core/demand.ts +44 -4
- package/src/core/entities/reservation.ts +1 -0
- package/src/core/entities/runner-instance.ts +4 -0
- package/src/core/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +64 -3
- package/src/core/runner-control-sessions.ts +62 -9
- package/src/core/runner-instances.test.ts +19 -0
- package/src/core/runner-instances.ts +2 -0
- package/src/core/runner-sessions.test.ts +121 -1
- package/src/db/ephemeral-registration-tokens.test.ts +20 -1
- package/src/db/ephemeral-registration-tokens.ts +1 -0
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +733 -22
- package/src/db/reservations.ts +408 -144
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +178 -9
- package/src/db/runner-instances.test.ts +265 -2
- package/src/db/runner-instances.ts +215 -55
- package/src/db/runner-states.ts +7 -0
- package/src/db/schema/reservations.ts +5 -1
- package/src/db/schema/runner-instances.ts +9 -0
- package/src/metrics/index.ts +10 -1
- package/src/metrics/instance.ts +59 -2
- package/src/metrics/service.test.ts +8 -1
- package/src/presentation/dto/poll-demand.ts +3 -0
- package/src/presentation/dto/runner-instances.ts +8 -1
- package/src/presentation/routes/assign-runner-instances.ts +6 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +219 -2
- package/src/presentation/routes/poll-demand.test.ts +56 -0
- package/src/presentation/routes/poll-demand.ts +1 -0
- package/src/presentation/routes/reconcile-runner-instances.test.ts +34 -2
- package/src/presentation/routes/reconcile-runner-instances.ts +7 -1
- package/src/presentation/routes/runner-enrollment.test.ts +316 -3
- package/src/presentation/routes/runner-enrollment.ts +8 -5
- package/test/factories/reservation.ts +2 -0
- package/test/factories/runner-instance.ts +6 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/ephemeral-registration-tokens.ts"],"sourcesContent":["import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';\nimport {and, asc, eq, gt, inArray, isNull, lt, sql} from 'drizzle-orm';\nimport type {EphemeralRegistrationToken} from '#core/entities/ephemeral-registration-token.js';\nimport {\n ActiveEphemeralRegistrationTokenExistsError,\n ActiveEphemeralRegistrationTokensExistError,\n EmptyRunnerLabelsError,\n RegistrationTokenBatchExceedsReservationError,\n RegistrationTokenConsumedError,\n RegistrationTokenExpiredError,\n RegistrationTokenWorkspaceMismatchError,\n ReservationExpiredError,\n ReservationNotFoundError,\n} from '#core/errors.js';\nimport {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';\nimport {db} from './db.js';\nimport {\n ephemeralRegistrationTokens,\n toEphemeralRegistrationToken,\n} from './schema/ephemeral-registration-tokens.js';\nimport {provisionerTokens} from './schema/provisioner-tokens.js';\nimport {reservations} from './schema/reservations.js';\nimport {providerRunners} from './schema/runner-instances.js';\nimport {runnerSessions, toRunnerSession} from './schema/runner-sessions.js';\n\nexport interface CreateEphemeralRegistrationTokenParams {\n workspaceId: string;\n provisionerId: string;\n reservationId?: string | null | undefined;\n providerRunnerId: string;\n hashedToken: string;\n prefix: string;\n expiresAt: Date;\n}\n\nexport interface CreateEphemeralRegistrationTokensBatchRow {\n providerRunnerId: string;\n hashedToken: string;\n prefix: string;\n}\n\nexport interface CreateEphemeralRegistrationTokensBatchParams {\n workspaceId: string;\n provisionerId: string;\n reservationId: string;\n expiresAt: Date;\n rows: CreateEphemeralRegistrationTokensBatchRow[];\n}\n\nexport async function createEphemeralRegistrationToken(\n params: CreateEphemeralRegistrationTokenParams,\n): Promise<EphemeralRegistrationToken> {\n const rows = await db().transaction(async (tx) => {\n const providerRunnerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n params.providerRunnerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${providerRunnerLockKey}))`);\n\n const [active] = await tx\n .select({id: ephemeralRegistrationTokens.id})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n eq(ephemeralRegistrationTokens.providerRunnerId, params.providerRunnerId),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n if (active) {\n throw new ActiveEphemeralRegistrationTokenExistsError(\n params.workspaceId,\n params.provisionerId,\n params.providerRunnerId,\n );\n }\n\n return await tx\n .insert(ephemeralRegistrationTokens)\n .values({\n workspaceId: params.workspaceId,\n provisionerId: params.provisionerId,\n reservationId: params.reservationId ?? null,\n providerRunnerId: params.providerRunnerId,\n hashedToken: params.hashedToken,\n prefix: params.prefix,\n expiresAt: params.expiresAt,\n })\n .returning();\n });\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toEphemeralRegistrationToken(row);\n}\n\nexport async function createEphemeralRegistrationTokensBatch(\n params: CreateEphemeralRegistrationTokensBatchParams,\n): Promise<EphemeralRegistrationToken[]> {\n const rows = await db().transaction(async (tx) => {\n const provisionerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${provisionerLockKey}))`);\n for (const providerRunnerId of [\n ...new Set(params.rows.map((row) => row.providerRunnerId)),\n ].sort()) {\n const providerRunnerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n providerRunnerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${providerRunnerLockKey}))`);\n }\n\n const [reservation] = await tx\n .select({\n count: reservations.count,\n isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,\n })\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.workspaceId, params.workspaceId),\n eq(reservations.provisionerId, params.provisionerId),\n ),\n )\n .limit(1);\n\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.isExpired) throw new ReservationExpiredError(params.reservationId);\n if (params.rows.length > reservation.count) {\n throw new RegistrationTokenBatchExceedsReservationError(\n params.rows.length,\n reservation.count,\n );\n }\n\n const [existingForReservation] = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n eq(ephemeralRegistrationTokens.reservationId, params.reservationId),\n ),\n );\n const alreadyMinted = existingForReservation?.count ?? 0;\n if (alreadyMinted + params.rows.length > reservation.count) {\n throw new RegistrationTokenBatchExceedsReservationError(\n params.rows.length,\n reservation.count,\n alreadyMinted,\n );\n }\n\n const providerRunnerIds = params.rows.map((row) => row.providerRunnerId);\n const activeRows = await tx\n .select({providerRunnerId: ephemeralRegistrationTokens.providerRunnerId})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n inArray(ephemeralRegistrationTokens.providerRunnerId, providerRunnerIds),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n );\n\n if (activeRows.length > 0) {\n throw new ActiveEphemeralRegistrationTokensExistError(\n activeRows.map((row) => row.providerRunnerId),\n );\n }\n\n return await tx\n .insert(ephemeralRegistrationTokens)\n .values(\n params.rows.map((row) => ({\n workspaceId: params.workspaceId,\n provisionerId: params.provisionerId,\n reservationId: params.reservationId,\n providerRunnerId: row.providerRunnerId,\n hashedToken: row.hashedToken,\n prefix: row.prefix,\n expiresAt: params.expiresAt,\n })),\n )\n .returning();\n });\n\n return rows.map(toEphemeralRegistrationToken);\n}\n\nexport async function resolveEphemeralRegistrationTokenByHash(\n hashedToken: string,\n): Promise<EphemeralRegistrationToken | undefined> {\n const rows = await db()\n .select()\n .from(ephemeralRegistrationTokens)\n .where(eq(ephemeralRegistrationTokens.hashedToken, hashedToken))\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toEphemeralRegistrationToken(row);\n}\n\nexport interface DeleteExpiredEphemeralRegistrationTokensParams {\n retentionDays: number;\n limit?: number;\n}\n\n/**\n * Deletes ephemeral registration tokens that have reached a terminal state and\n * outlived the retention window. A token is terminal once it is consumed\n * (single use spent) or expired without consumption; the retention window is\n * measured from that terminal event so the audit record survives a bounded\n * debugging window. Active tokens that are neither consumed nor expired are\n * never deleted, so a live single-use credential is never removed early.\n */\nexport async function deleteExpiredEphemeralRegistrationTokens(\n params: DeleteExpiredEphemeralRegistrationTokensParams,\n): Promise<number> {\n // coalesce(consumed_at, expires_at) is the terminal timestamp: consumed_at\n // when the token was spent, otherwise its expiry. Comparing it to the cutoff\n // both selects deletable rows and orders the batch, and it matches the\n // terminal index so idle GC ticks never scan live rows.\n const terminalAt = sql`coalesce(${ephemeralRegistrationTokens.consumedAt}, ${ephemeralRegistrationTokens.expiresAt})`;\n const retentionCutoff = sql`now() - (${params.retentionDays} || ' days')::interval`;\n\n const deletableIds = db()\n .select({id: ephemeralRegistrationTokens.id})\n .from(ephemeralRegistrationTokens)\n .where(lt(terminalAt, retentionCutoff))\n .orderBy(asc(terminalAt), asc(ephemeralRegistrationTokens.id))\n .limit(params.limit ?? 1000);\n\n const deleted = await db()\n .delete(ephemeralRegistrationTokens)\n .where(inArray(ephemeralRegistrationTokens.id, deletableIds))\n .returning({id: ephemeralRegistrationTokens.id});\n\n return deleted.length;\n}\n\nexport async function createRunnerSessionConsumingEphemeralToken(params: {\n ephemeralTokenId: string;\n workspaceId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n maxClaims: number;\n}) {\n return await db().transaction(async (tx) => {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);\n\n const consumed = await tx\n .update(ephemeralRegistrationTokens)\n .set({consumedAt: sql`now()`})\n .where(\n and(\n eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId),\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n )\n .returning({\n id: ephemeralRegistrationTokens.id,\n provisionerId: ephemeralRegistrationTokens.provisionerId,\n providerRunnerId: ephemeralRegistrationTokens.providerRunnerId,\n });\n\n if (!consumed[0]) {\n const [token] = await tx\n .select({\n workspaceId: ephemeralRegistrationTokens.workspaceId,\n consumedAt: ephemeralRegistrationTokens.consumedAt,\n isExpired: sql<boolean>`${ephemeralRegistrationTokens.expiresAt} <= now()`,\n })\n .from(ephemeralRegistrationTokens)\n .where(eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId))\n .limit(1);\n\n if (!token) {\n throw new Error(`Ephemeral registration token not found: ${params.ephemeralTokenId}`);\n }\n if (token.workspaceId !== params.workspaceId) {\n throw new RegistrationTokenWorkspaceMismatchError(\n params.ephemeralTokenId,\n params.workspaceId,\n );\n }\n if (token.consumedAt) throw new RegistrationTokenConsumedError(params.ephemeralTokenId);\n if (token.isExpired) throw new RegistrationTokenExpiredError(params.ephemeralTokenId);\n throw new Error(\n `Ephemeral registration token could not be consumed: ${params.ephemeralTokenId}`,\n );\n }\n\n const [provisioner] = await tx\n .select({scope: provisionerTokens.scope})\n .from(provisionerTokens)\n .where(eq(provisionerTokens.id, consumed[0].provisionerId))\n .limit(1);\n const labels = sanitizeRunnerLabelsOrThrow(params.labels, {\n scope: provisioner?.scope ?? 'workspace',\n source: 'ephemeral runner registration',\n });\n if (labels.length === 0) throw new EmptyRunnerLabelsError();\n\n const [session] = await tx\n .insert(runnerSessions)\n .values({\n workspaceId: params.workspaceId,\n scope: 'workspace',\n registrationTokenId: params.ephemeralTokenId,\n registrationTokenKind: 'ephemeral',\n provisionerId: consumed[0].provisionerId,\n providerRunnerId: consumed[0].providerRunnerId,\n labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: params.maxClaims,\n claimsUsed: 0,\n })\n .returning();\n\n if (!session) throw new Error('Insert returned no rows');\n\n await tx\n .update(ephemeralRegistrationTokens)\n .set({consumedSessionId: session.id})\n .where(eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId));\n\n await tx\n .update(providerRunners)\n .set({runnerSessionId: session.id, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.workspaceId, params.workspaceId),\n eq(providerRunners.provisionerId, consumed[0].provisionerId),\n eq(providerRunners.providerRunnerId, consumed[0].providerRunnerId),\n isNull(providerRunners.reservationReleasedAt),\n ),\n );\n\n return toRunnerSession(session);\n });\n}\n"],"names":["and","asc","eq","gt","inArray","isNull","lt","sql","ActiveEphemeralRegistrationTokenExistsError","ActiveEphemeralRegistrationTokensExistError","EmptyRunnerLabelsError","RegistrationTokenBatchExceedsReservationError","RegistrationTokenConsumedError","RegistrationTokenExpiredError","RegistrationTokenWorkspaceMismatchError","ReservationExpiredError","ReservationNotFoundError","sanitizeRunnerLabelsOrThrow","db","ephemeralRegistrationTokens","toEphemeralRegistrationToken","provisionerTokens","reservations","providerRunners","runnerSessions","toRunnerSession","createEphemeralRegistrationToken","params","rows","transaction","tx","providerRunnerLockKey","workspaceId","provisionerId","providerRunnerId","join","execute","active","select","id","from","where","consumedAt","expiresAt","limit","insert","values","reservationId","hashedToken","prefix","returning","row","Error","createEphemeralRegistrationTokensBatch","provisionerLockKey","Set","map","sort","reservation","count","isExpired","length","existingForReservation","alreadyMinted","providerRunnerIds","activeRows","resolveEphemeralRegistrationTokenByHash","undefined","deleteExpiredEphemeralRegistrationTokens","terminalAt","retentionCutoff","retentionDays","deletableIds","orderBy","deleted","delete","createRunnerSessionConsumingEphemeralToken","consumed","update","set","ephemeralTokenId","token","provisioner","scope","labels","source","session","registrationTokenId","registrationTokenKind","toolCapabilities","toolCapabilitiesReportedAt","maxClaims","claimsUsed","consumedSessionId","runnerSessionId","updatedAt","reservationReleasedAt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/ephemeral-registration-tokens.ts"],"sourcesContent":["import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';\nimport {and, asc, eq, gt, inArray, isNull, lt, sql} from 'drizzle-orm';\nimport type {EphemeralRegistrationToken} from '#core/entities/ephemeral-registration-token.js';\nimport {\n ActiveEphemeralRegistrationTokenExistsError,\n ActiveEphemeralRegistrationTokensExistError,\n EmptyRunnerLabelsError,\n RegistrationTokenBatchExceedsReservationError,\n RegistrationTokenConsumedError,\n RegistrationTokenExpiredError,\n RegistrationTokenWorkspaceMismatchError,\n ReservationExpiredError,\n ReservationNotFoundError,\n} from '#core/errors.js';\nimport {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';\nimport {db} from './db.js';\nimport {\n ephemeralRegistrationTokens,\n toEphemeralRegistrationToken,\n} from './schema/ephemeral-registration-tokens.js';\nimport {provisionerTokens} from './schema/provisioner-tokens.js';\nimport {reservations} from './schema/reservations.js';\nimport {providerRunners} from './schema/runner-instances.js';\nimport {runnerSessions, toRunnerSession} from './schema/runner-sessions.js';\n\nexport interface CreateEphemeralRegistrationTokenParams {\n workspaceId: string;\n provisionerId: string;\n reservationId?: string | null | undefined;\n providerRunnerId: string;\n hashedToken: string;\n prefix: string;\n expiresAt: Date;\n}\n\nexport interface CreateEphemeralRegistrationTokensBatchRow {\n providerRunnerId: string;\n hashedToken: string;\n prefix: string;\n}\n\nexport interface CreateEphemeralRegistrationTokensBatchParams {\n workspaceId: string;\n provisionerId: string;\n reservationId: string;\n expiresAt: Date;\n rows: CreateEphemeralRegistrationTokensBatchRow[];\n}\n\nexport async function createEphemeralRegistrationToken(\n params: CreateEphemeralRegistrationTokenParams,\n): Promise<EphemeralRegistrationToken> {\n const rows = await db().transaction(async (tx) => {\n const providerRunnerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n params.providerRunnerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${providerRunnerLockKey}))`);\n\n const [active] = await tx\n .select({id: ephemeralRegistrationTokens.id})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n eq(ephemeralRegistrationTokens.providerRunnerId, params.providerRunnerId),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n if (active) {\n throw new ActiveEphemeralRegistrationTokenExistsError(\n params.workspaceId,\n params.provisionerId,\n params.providerRunnerId,\n );\n }\n\n return await tx\n .insert(ephemeralRegistrationTokens)\n .values({\n workspaceId: params.workspaceId,\n provisionerId: params.provisionerId,\n reservationId: params.reservationId ?? null,\n providerRunnerId: params.providerRunnerId,\n hashedToken: params.hashedToken,\n prefix: params.prefix,\n expiresAt: params.expiresAt,\n })\n .returning();\n });\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toEphemeralRegistrationToken(row);\n}\n\nexport async function createEphemeralRegistrationTokensBatch(\n params: CreateEphemeralRegistrationTokensBatchParams,\n): Promise<EphemeralRegistrationToken[]> {\n const rows = await db().transaction(async (tx) => {\n const provisionerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${provisionerLockKey}))`);\n for (const providerRunnerId of [\n ...new Set(params.rows.map((row) => row.providerRunnerId)),\n ].sort()) {\n const providerRunnerLockKey = [\n 'runners_ephemeral_registration_tokens',\n params.workspaceId,\n params.provisionerId,\n providerRunnerId,\n ].join(':');\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${providerRunnerLockKey}))`);\n }\n\n const [reservation] = await tx\n .select({\n count: reservations.count,\n isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,\n })\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.workspaceId, params.workspaceId),\n eq(reservations.provisionerId, params.provisionerId),\n eq(reservations.kind, 'launch'),\n ),\n )\n .limit(1);\n\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.isExpired) throw new ReservationExpiredError(params.reservationId);\n if (params.rows.length > reservation.count) {\n throw new RegistrationTokenBatchExceedsReservationError(\n params.rows.length,\n reservation.count,\n );\n }\n\n const [existingForReservation] = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n eq(ephemeralRegistrationTokens.reservationId, params.reservationId),\n ),\n );\n const alreadyMinted = existingForReservation?.count ?? 0;\n if (alreadyMinted + params.rows.length > reservation.count) {\n throw new RegistrationTokenBatchExceedsReservationError(\n params.rows.length,\n reservation.count,\n alreadyMinted,\n );\n }\n\n const providerRunnerIds = params.rows.map((row) => row.providerRunnerId);\n const activeRows = await tx\n .select({providerRunnerId: ephemeralRegistrationTokens.providerRunnerId})\n .from(ephemeralRegistrationTokens)\n .where(\n and(\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n eq(ephemeralRegistrationTokens.provisionerId, params.provisionerId),\n inArray(ephemeralRegistrationTokens.providerRunnerId, providerRunnerIds),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n );\n\n if (activeRows.length > 0) {\n throw new ActiveEphemeralRegistrationTokensExistError(\n activeRows.map((row) => row.providerRunnerId),\n );\n }\n\n return await tx\n .insert(ephemeralRegistrationTokens)\n .values(\n params.rows.map((row) => ({\n workspaceId: params.workspaceId,\n provisionerId: params.provisionerId,\n reservationId: params.reservationId,\n providerRunnerId: row.providerRunnerId,\n hashedToken: row.hashedToken,\n prefix: row.prefix,\n expiresAt: params.expiresAt,\n })),\n )\n .returning();\n });\n\n return rows.map(toEphemeralRegistrationToken);\n}\n\nexport async function resolveEphemeralRegistrationTokenByHash(\n hashedToken: string,\n): Promise<EphemeralRegistrationToken | undefined> {\n const rows = await db()\n .select()\n .from(ephemeralRegistrationTokens)\n .where(eq(ephemeralRegistrationTokens.hashedToken, hashedToken))\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toEphemeralRegistrationToken(row);\n}\n\nexport interface DeleteExpiredEphemeralRegistrationTokensParams {\n retentionDays: number;\n limit?: number;\n}\n\n/**\n * Deletes ephemeral registration tokens that have reached a terminal state and\n * outlived the retention window. A token is terminal once it is consumed\n * (single use spent) or expired without consumption; the retention window is\n * measured from that terminal event so the audit record survives a bounded\n * debugging window. Active tokens that are neither consumed nor expired are\n * never deleted, so a live single-use credential is never removed early.\n */\nexport async function deleteExpiredEphemeralRegistrationTokens(\n params: DeleteExpiredEphemeralRegistrationTokensParams,\n): Promise<number> {\n // coalesce(consumed_at, expires_at) is the terminal timestamp: consumed_at\n // when the token was spent, otherwise its expiry. Comparing it to the cutoff\n // both selects deletable rows and orders the batch, and it matches the\n // terminal index so idle GC ticks never scan live rows.\n const terminalAt = sql`coalesce(${ephemeralRegistrationTokens.consumedAt}, ${ephemeralRegistrationTokens.expiresAt})`;\n const retentionCutoff = sql`now() - (${params.retentionDays} || ' days')::interval`;\n\n const deletableIds = db()\n .select({id: ephemeralRegistrationTokens.id})\n .from(ephemeralRegistrationTokens)\n .where(lt(terminalAt, retentionCutoff))\n .orderBy(asc(terminalAt), asc(ephemeralRegistrationTokens.id))\n .limit(params.limit ?? 1000);\n\n const deleted = await db()\n .delete(ephemeralRegistrationTokens)\n .where(inArray(ephemeralRegistrationTokens.id, deletableIds))\n .returning({id: ephemeralRegistrationTokens.id});\n\n return deleted.length;\n}\n\nexport async function createRunnerSessionConsumingEphemeralToken(params: {\n ephemeralTokenId: string;\n workspaceId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n maxClaims: number;\n}) {\n return await db().transaction(async (tx) => {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);\n\n const consumed = await tx\n .update(ephemeralRegistrationTokens)\n .set({consumedAt: sql`now()`})\n .where(\n and(\n eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId),\n eq(ephemeralRegistrationTokens.workspaceId, params.workspaceId),\n isNull(ephemeralRegistrationTokens.consumedAt),\n gt(ephemeralRegistrationTokens.expiresAt, sql`now()`),\n ),\n )\n .returning({\n id: ephemeralRegistrationTokens.id,\n provisionerId: ephemeralRegistrationTokens.provisionerId,\n providerRunnerId: ephemeralRegistrationTokens.providerRunnerId,\n });\n\n if (!consumed[0]) {\n const [token] = await tx\n .select({\n workspaceId: ephemeralRegistrationTokens.workspaceId,\n consumedAt: ephemeralRegistrationTokens.consumedAt,\n isExpired: sql<boolean>`${ephemeralRegistrationTokens.expiresAt} <= now()`,\n })\n .from(ephemeralRegistrationTokens)\n .where(eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId))\n .limit(1);\n\n if (!token) {\n throw new Error(`Ephemeral registration token not found: ${params.ephemeralTokenId}`);\n }\n if (token.workspaceId !== params.workspaceId) {\n throw new RegistrationTokenWorkspaceMismatchError(\n params.ephemeralTokenId,\n params.workspaceId,\n );\n }\n if (token.consumedAt) throw new RegistrationTokenConsumedError(params.ephemeralTokenId);\n if (token.isExpired) throw new RegistrationTokenExpiredError(params.ephemeralTokenId);\n throw new Error(\n `Ephemeral registration token could not be consumed: ${params.ephemeralTokenId}`,\n );\n }\n\n const [provisioner] = await tx\n .select({scope: provisionerTokens.scope})\n .from(provisionerTokens)\n .where(eq(provisionerTokens.id, consumed[0].provisionerId))\n .limit(1);\n const labels = sanitizeRunnerLabelsOrThrow(params.labels, {\n scope: provisioner?.scope ?? 'workspace',\n source: 'ephemeral runner registration',\n });\n if (labels.length === 0) throw new EmptyRunnerLabelsError();\n\n const [session] = await tx\n .insert(runnerSessions)\n .values({\n workspaceId: params.workspaceId,\n scope: 'workspace',\n registrationTokenId: params.ephemeralTokenId,\n registrationTokenKind: 'ephemeral',\n provisionerId: consumed[0].provisionerId,\n providerRunnerId: consumed[0].providerRunnerId,\n labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: params.maxClaims,\n claimsUsed: 0,\n })\n .returning();\n\n if (!session) throw new Error('Insert returned no rows');\n\n await tx\n .update(ephemeralRegistrationTokens)\n .set({consumedSessionId: session.id})\n .where(eq(ephemeralRegistrationTokens.id, params.ephemeralTokenId));\n\n await tx\n .update(providerRunners)\n .set({runnerSessionId: session.id, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.workspaceId, params.workspaceId),\n eq(providerRunners.provisionerId, consumed[0].provisionerId),\n eq(providerRunners.providerRunnerId, consumed[0].providerRunnerId),\n isNull(providerRunners.reservationReleasedAt),\n ),\n );\n\n return toRunnerSession(session);\n });\n}\n"],"names":["and","asc","eq","gt","inArray","isNull","lt","sql","ActiveEphemeralRegistrationTokenExistsError","ActiveEphemeralRegistrationTokensExistError","EmptyRunnerLabelsError","RegistrationTokenBatchExceedsReservationError","RegistrationTokenConsumedError","RegistrationTokenExpiredError","RegistrationTokenWorkspaceMismatchError","ReservationExpiredError","ReservationNotFoundError","sanitizeRunnerLabelsOrThrow","db","ephemeralRegistrationTokens","toEphemeralRegistrationToken","provisionerTokens","reservations","providerRunners","runnerSessions","toRunnerSession","createEphemeralRegistrationToken","params","rows","transaction","tx","providerRunnerLockKey","workspaceId","provisionerId","providerRunnerId","join","execute","active","select","id","from","where","consumedAt","expiresAt","limit","insert","values","reservationId","hashedToken","prefix","returning","row","Error","createEphemeralRegistrationTokensBatch","provisionerLockKey","Set","map","sort","reservation","count","isExpired","kind","length","existingForReservation","alreadyMinted","providerRunnerIds","activeRows","resolveEphemeralRegistrationTokenByHash","undefined","deleteExpiredEphemeralRegistrationTokens","terminalAt","retentionCutoff","retentionDays","deletableIds","orderBy","deleted","delete","createRunnerSessionConsumingEphemeralToken","consumed","update","set","ephemeralTokenId","token","provisioner","scope","labels","source","session","registrationTokenId","registrationTokenKind","toolCapabilities","toolCapabilitiesReportedAt","maxClaims","claimsUsed","consumedSessionId","runnerSessionId","updatedAt","reservationReleasedAt"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reservation-locks.d.ts","sourceRoot":"","sources":["../../src/db/reservation-locks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC,wBAAsB,mCAAmC,CACvD,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;CAAC,GACjE,OAAO,CAAC,IAAI,CAAC,CAef"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
export async function lockRunnerReservationAdvisoryKeysTx(tx, params) {
|
|
3
|
+
const reservationIds = [
|
|
4
|
+
...new Set(params.reservationIds)
|
|
5
|
+
].sort();
|
|
6
|
+
if (reservationIds.length === 0) return;
|
|
7
|
+
const lockKeys = sql.join(reservationIds.map((reservationId)=>sql`(${`runners_assignment:${params.provisionerId}:${reservationId}`})`), sql`, `);
|
|
8
|
+
await tx.execute(sql`
|
|
9
|
+
select pg_advisory_xact_lock(hashtext(lock_key))
|
|
10
|
+
from (values ${lockKeys}) as reservation_locks(lock_key)
|
|
11
|
+
order by lock_key
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=reservation-locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/db/reservation-locks.ts"],"sourcesContent":["import {sql} from 'drizzle-orm';\nimport type {Tx} from './db.js';\n\nexport async function lockRunnerReservationAdvisoryKeysTx(\n tx: Tx,\n params: {provisionerId: string; reservationIds: readonly string[]},\n): Promise<void> {\n const reservationIds = [...new Set(params.reservationIds)].sort();\n if (reservationIds.length === 0) return;\n\n const lockKeys = sql.join(\n reservationIds.map(\n (reservationId) => sql`(${`runners_assignment:${params.provisionerId}:${reservationId}`})`,\n ),\n sql`, `,\n );\n await tx.execute(sql`\n select pg_advisory_xact_lock(hashtext(lock_key))\n from (values ${lockKeys}) as reservation_locks(lock_key)\n order by lock_key\n `);\n}\n"],"names":["sql","lockRunnerReservationAdvisoryKeysTx","tx","params","reservationIds","Set","sort","length","lockKeys","join","map","reservationId","provisionerId","execute"],"mappings":"AAAA,SAAQA,GAAG,QAAO,cAAc;AAGhC,OAAO,eAAeC,oCACpBC,EAAM,EACNC,MAAkE;IAElE,MAAMC,iBAAiB;WAAI,IAAIC,IAAIF,OAAOC,cAAc;KAAE,CAACE,IAAI;IAC/D,IAAIF,eAAeG,MAAM,KAAK,GAAG;IAEjC,MAAMC,WAAWR,IAAIS,IAAI,CACvBL,eAAeM,GAAG,CAChB,CAACC,gBAAkBX,GAAG,CAAC,CAAC,EAAE,CAAC,mBAAmB,EAAEG,OAAOS,aAAa,CAAC,CAAC,EAAED,eAAe,CAAC,CAAC,CAAC,GAE5FX,GAAG,CAAC,EAAE,CAAC;IAET,MAAME,GAAGW,OAAO,CAACb,GAAG,CAAC;;iBAEN,EAAEQ,SAAS;;EAE1B,CAAC;AACH"}
|
|
@@ -25,6 +25,8 @@ export interface PollDemandAndReserveParams {
|
|
|
25
25
|
provisionerId: string;
|
|
26
26
|
maxReservations: number;
|
|
27
27
|
ttlSeconds: number;
|
|
28
|
+
/** Lifetime of the short reservation that gives rebound runners time to activate. */
|
|
29
|
+
activationGraceSeconds?: number;
|
|
28
30
|
templates: ReservationTemplate[];
|
|
29
31
|
capabilityWindowSeconds?: number;
|
|
30
32
|
}
|
|
@@ -32,23 +34,34 @@ export interface InstallationPollDemandAndReserveParams {
|
|
|
32
34
|
provisionerId: string;
|
|
33
35
|
maxReservations: number;
|
|
34
36
|
ttlSeconds: number;
|
|
37
|
+
/** Lifetime of the short reservation that gives rebound runners time to activate. */
|
|
38
|
+
activationGraceSeconds?: number;
|
|
35
39
|
templates: ReservationTemplate[];
|
|
36
40
|
capabilityWindowSeconds: number;
|
|
37
41
|
eligibleWorkspaceIds: ReadonlySet<string>;
|
|
38
42
|
signal?: AbortSignal;
|
|
39
43
|
onReservations?: (reservations: ReservationGrant[]) => void;
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
interface NewReservationUnits {
|
|
46
|
+
labels: string[];
|
|
47
|
+
count: number;
|
|
48
|
+
}
|
|
49
|
+
interface PollDemandAndReserveResult {
|
|
42
50
|
stats: DemandStat[];
|
|
51
|
+
/** Only launch reservations are exposed to the provisioner. */
|
|
43
52
|
reservations: ReservationGrant[];
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
/** Internal allocation accounting for bound and launch rows together. */
|
|
54
|
+
newlyReservedUnits: NewReservationUnits[];
|
|
55
|
+
}
|
|
56
|
+
export declare function pollDemandAndReserve(params: PollDemandAndReserveParams): Promise<{
|
|
46
57
|
stats: DemandStat[];
|
|
47
58
|
reservations: ReservationGrant[];
|
|
48
59
|
}>;
|
|
60
|
+
export declare function pollDemandAndReserveTx(tx: Tx, params: PollDemandAndReserveParams): Promise<PollDemandAndReserveResult>;
|
|
49
61
|
export declare function pollInstallationDemandAndReserve(params: InstallationPollDemandAndReserveParams): Promise<{
|
|
50
62
|
stats: DemandStat[];
|
|
51
63
|
reservations: ReservationGrant[];
|
|
64
|
+
newlyReservedCount?: number;
|
|
52
65
|
}>;
|
|
53
66
|
export declare function listQueuedDemandWorkspaceIds(): Promise<string[]>;
|
|
54
67
|
export declare function deleteExpiredReservations(params?: {
|
|
@@ -63,4 +76,5 @@ export declare function releaseReservationUnits(tx: Tx, params: {
|
|
|
63
76
|
count: number;
|
|
64
77
|
}>;
|
|
65
78
|
}): Promise<number>;
|
|
79
|
+
export {};
|
|
66
80
|
//# sourceMappingURL=reservations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reservations.d.ts","sourceRoot":"","sources":["../../src/db/reservations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reservations.d.ts","sourceRoot":"","sources":["../../src/db/reservations.ts"],"names":[],"mappings":"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"}
|
package/dist/db/reservations.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { canonicalizeLabels } from '@shipfox/runner-labels';
|
|
2
|
-
import { and, arrayContains, asc, eq, exists, gt, inArray, isNotNull, isNull, lt, notExists, or, sql } from 'drizzle-orm';
|
|
2
|
+
import { and, arrayContains, asc, eq, exists, gt, inArray, isNotNull, isNull, lt, notExists, notInArray, or, sql } from 'drizzle-orm';
|
|
3
|
+
import { recordProviderRunnerActivationOutcome } from '#metrics/instance.js';
|
|
3
4
|
import { db } from './db.js';
|
|
5
|
+
import { lockRunnerReservationAdvisoryKeysTx } from './reservation-locks.js';
|
|
6
|
+
import { terminalStates } from './runner-states.js';
|
|
4
7
|
import { pendingJobExecutions } from './schema/pending-job-executions.js';
|
|
5
8
|
import { provisionerCapabilitySnapshots } from './schema/provisioner-capability-snapshots.js';
|
|
6
9
|
import { provisionerTokens } from './schema/provisioner-tokens.js';
|
|
@@ -9,9 +12,13 @@ import { runnerActivationTokens } from './schema/runner-activation-tokens.js';
|
|
|
9
12
|
import { runnerControlSessions } from './schema/runner-control-sessions.js';
|
|
10
13
|
import { providerRunners } from './schema/runner-instances.js';
|
|
11
14
|
export async function pollDemandAndReserve(params) {
|
|
12
|
-
|
|
15
|
+
const result = await db().transaction(async (tx)=>{
|
|
13
16
|
return await pollDemandAndReserveTx(tx, params);
|
|
14
17
|
});
|
|
18
|
+
return {
|
|
19
|
+
stats: result.stats,
|
|
20
|
+
reservations: result.reservations
|
|
21
|
+
};
|
|
15
22
|
}
|
|
16
23
|
export async function pollDemandAndReserveTx(tx, params) {
|
|
17
24
|
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);
|
|
@@ -38,13 +45,17 @@ export async function pollInstallationDemandAndReserve(params) {
|
|
|
38
45
|
const locked = lockResult.rows[0];
|
|
39
46
|
if (!locked?.locked) return {
|
|
40
47
|
stats: [],
|
|
41
|
-
reservations: []
|
|
48
|
+
reservations: [],
|
|
49
|
+
newlyReservedUnits: []
|
|
42
50
|
};
|
|
43
51
|
return await pollDemandAndReserveLockedTx(tx, {
|
|
44
52
|
workspaceId,
|
|
45
53
|
provisionerId: params.provisionerId,
|
|
46
54
|
maxReservations: remainingMaxReservations,
|
|
47
55
|
ttlSeconds: params.ttlSeconds,
|
|
56
|
+
...params.activationGraceSeconds !== undefined ? {
|
|
57
|
+
activationGraceSeconds: params.activationGraceSeconds
|
|
58
|
+
} : {},
|
|
48
59
|
templates: remainingTemplates.map((template)=>({
|
|
49
60
|
...template,
|
|
50
61
|
availableSlots: template.remainingSlots
|
|
@@ -56,15 +67,21 @@ export async function pollInstallationDemandAndReserve(params) {
|
|
|
56
67
|
results.push(result);
|
|
57
68
|
consumeInstallationTemplateSlots(remainingTemplates, result.reservations);
|
|
58
69
|
params.onReservations?.(result.reservations);
|
|
59
|
-
remainingMaxReservations -= result.
|
|
70
|
+
remainingMaxReservations -= result.newlyReservedUnits.reduce((total, reservation)=>total + reservation.count, 0);
|
|
60
71
|
}
|
|
72
|
+
const newlyReservedCount = results.reduce((total, result)=>total + result.newlyReservedUnits.reduce((subtotal, reservation)=>subtotal + reservation.count, 0), 0);
|
|
61
73
|
return {
|
|
62
74
|
stats: results.flatMap((result)=>result.stats),
|
|
63
|
-
reservations: results.flatMap((result)=>result.reservations)
|
|
75
|
+
reservations: results.flatMap((result)=>result.reservations),
|
|
76
|
+
...newlyReservedCount > 0 ? {
|
|
77
|
+
newlyReservedCount
|
|
78
|
+
} : {}
|
|
64
79
|
};
|
|
65
80
|
}
|
|
66
|
-
function consumeInstallationTemplateSlots(templates,
|
|
67
|
-
|
|
81
|
+
function consumeInstallationTemplateSlots(templates, launchGrants) {
|
|
82
|
+
// Adopted runners are already included in the running count behind availableSlots.
|
|
83
|
+
// Only units that still need a launch consume advertised template capacity.
|
|
84
|
+
for (const reservation of launchGrants){
|
|
68
85
|
const satisfyingTemplates = templates.filter((template)=>isSubset(reservation.labels, template.labels)).sort((a, b)=>a.labels.length - b.labels.length || a.templateKey.localeCompare(b.templateKey));
|
|
69
86
|
drawSlots(satisfyingTemplates, reservation.count);
|
|
70
87
|
}
|
|
@@ -93,10 +110,10 @@ async function pollDemandAndReserveLockedTx(tx, params) {
|
|
|
93
110
|
labelKey(row.requiredLabels),
|
|
94
111
|
row.reserved
|
|
95
112
|
]));
|
|
96
|
-
const activeProvisionerReservationRows = await tx
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})
|
|
113
|
+
const activeProvisionerReservationRows = await listActiveProvisionerReservationRowsTx(tx, {
|
|
114
|
+
workspaceId: params.workspaceId,
|
|
115
|
+
provisionerId: params.provisionerId
|
|
116
|
+
});
|
|
100
117
|
const templates = params.templates.map((template)=>({
|
|
101
118
|
templateKey: template.templateKey,
|
|
102
119
|
labels: [
|
|
@@ -107,6 +124,7 @@ async function pollDemandAndReserveLockedTx(tx, params) {
|
|
|
107
124
|
deductProvisionerReservations(templates, activeProvisionerReservationRows);
|
|
108
125
|
const stats = [];
|
|
109
126
|
const grants = [];
|
|
127
|
+
const newlyReservedUnits = [];
|
|
110
128
|
let remainingMaxReservations = params.maxReservations;
|
|
111
129
|
const workspaceIdField = params.capabilityWindowSeconds === undefined ? {} : {
|
|
112
130
|
workspaceId: params.workspaceId
|
|
@@ -120,35 +138,68 @@ async function pollDemandAndReserveLockedTx(tx, params) {
|
|
|
120
138
|
const grant = Math.min(unreserved, capacity, remainingMaxReservations);
|
|
121
139
|
let reservedAfterGrant = reserved;
|
|
122
140
|
if (grant > 0 && params.maxReservations > 0) {
|
|
123
|
-
const
|
|
124
|
-
workspaceId: params.workspaceId,
|
|
125
|
-
provisionerId: params.provisionerId,
|
|
126
|
-
requiredLabels: demand.requiredLabels,
|
|
127
|
-
count: grant,
|
|
128
|
-
expiresAt: sql`now() + (${params.ttlSeconds} || ' seconds')::interval`
|
|
129
|
-
}).returning({
|
|
130
|
-
id: reservations.id,
|
|
131
|
-
expiresAt: reservations.expiresAt
|
|
132
|
-
});
|
|
133
|
-
if (!inserted) throw new Error('Insert returned no rows');
|
|
134
|
-
remainingMaxReservations -= grant;
|
|
135
|
-
drawSlots(satisfyingTemplates, grant);
|
|
136
|
-
await bindIdleRunnerInstancesTx(tx, {
|
|
141
|
+
const idleRunners = await listIdleRunnerInstancesTx(tx, {
|
|
137
142
|
provisionerId: params.provisionerId,
|
|
138
|
-
reservationId: inserted.id,
|
|
139
143
|
requiredLabels: demand.requiredLabels,
|
|
140
144
|
count: grant,
|
|
141
145
|
workspaceId: params.workspaceId,
|
|
142
146
|
scope: params.scope
|
|
143
147
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
remainingMaxReservations -= grant;
|
|
149
|
+
const boundCount = idleRunners.length;
|
|
150
|
+
if (boundCount > 0) {
|
|
151
|
+
const [boundReservation] = await tx.insert(reservations).values({
|
|
152
|
+
workspaceId: params.workspaceId,
|
|
153
|
+
provisionerId: params.provisionerId,
|
|
154
|
+
requiredLabels: demand.requiredLabels,
|
|
155
|
+
count: boundCount,
|
|
156
|
+
kind: 'bound',
|
|
157
|
+
expiresAt: sql`now() + (${params.activationGraceSeconds ?? params.ttlSeconds} || ' seconds')::interval`
|
|
158
|
+
}).returning({
|
|
159
|
+
id: reservations.id
|
|
160
|
+
});
|
|
161
|
+
if (!boundReservation) throw new Error('Insert returned no rows');
|
|
162
|
+
await bindIdleRunnerInstancesTx(tx, {
|
|
163
|
+
provisionerId: params.provisionerId,
|
|
164
|
+
reservationId: boundReservation.id,
|
|
165
|
+
workspaceId: params.workspaceId,
|
|
166
|
+
requiredLabels: demand.requiredLabels,
|
|
167
|
+
scope: params.scope,
|
|
168
|
+
idleRunners
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const launchCount = grant - boundCount;
|
|
172
|
+
drawSlots(satisfyingTemplates, launchCount);
|
|
173
|
+
let launchReservation;
|
|
174
|
+
if (launchCount > 0) {
|
|
175
|
+
const [inserted] = await tx.insert(reservations).values({
|
|
176
|
+
workspaceId: params.workspaceId,
|
|
177
|
+
provisionerId: params.provisionerId,
|
|
178
|
+
requiredLabels: demand.requiredLabels,
|
|
179
|
+
count: launchCount,
|
|
180
|
+
kind: 'launch',
|
|
181
|
+
expiresAt: sql`now() + (${params.ttlSeconds} || ' seconds')::interval`
|
|
182
|
+
}).returning({
|
|
183
|
+
id: reservations.id,
|
|
184
|
+
expiresAt: reservations.expiresAt
|
|
185
|
+
});
|
|
186
|
+
if (!inserted) throw new Error('Insert returned no rows');
|
|
187
|
+
launchReservation = inserted;
|
|
188
|
+
}
|
|
189
|
+
newlyReservedUnits.push({
|
|
148
190
|
labels: demand.requiredLabels,
|
|
149
|
-
count: grant
|
|
150
|
-
expiresAt: inserted.expiresAt
|
|
191
|
+
count: grant
|
|
151
192
|
});
|
|
193
|
+
reservedAfterGrant += grant;
|
|
194
|
+
if (launchReservation) {
|
|
195
|
+
grants.push({
|
|
196
|
+
reservationId: launchReservation.id,
|
|
197
|
+
...workspaceIdField,
|
|
198
|
+
labels: demand.requiredLabels,
|
|
199
|
+
count: launchCount,
|
|
200
|
+
expiresAt: launchReservation.expiresAt
|
|
201
|
+
});
|
|
202
|
+
}
|
|
152
203
|
}
|
|
153
204
|
stats.push({
|
|
154
205
|
...workspaceIdField,
|
|
@@ -160,33 +211,160 @@ async function pollDemandAndReserveLockedTx(tx, params) {
|
|
|
160
211
|
}
|
|
161
212
|
return {
|
|
162
213
|
stats,
|
|
163
|
-
reservations: grants
|
|
214
|
+
reservations: grants,
|
|
215
|
+
newlyReservedUnits
|
|
164
216
|
};
|
|
165
217
|
}
|
|
166
|
-
async function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
async function listActiveProvisionerReservationRowsTx(tx, params) {
|
|
219
|
+
const candidateRows = await tx.select({
|
|
220
|
+
id: reservations.id
|
|
221
|
+
}).from(reservations).where(and(eq(reservations.workspaceId, params.workspaceId), eq(reservations.provisionerId, params.provisionerId), gt(reservations.expiresAt, sql`now()`)));
|
|
222
|
+
const candidateIds = candidateRows.map((row)=>row.id);
|
|
223
|
+
if (candidateIds.length === 0) return [];
|
|
224
|
+
// Assignment and provider-report transactions use this key before changing runner
|
|
225
|
+
// links. Lock it before counting so an in-flight assignment cannot expose its unit twice.
|
|
226
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
227
|
+
provisionerId: params.provisionerId,
|
|
228
|
+
reservationIds: candidateIds
|
|
229
|
+
});
|
|
230
|
+
const activeRows = await tx.select({
|
|
231
|
+
id: reservations.id,
|
|
232
|
+
requiredLabels: reservations.requiredLabels,
|
|
233
|
+
count: reservations.count
|
|
234
|
+
}).from(reservations).where(and(eq(reservations.workspaceId, params.workspaceId), eq(reservations.provisionerId, params.provisionerId), gt(reservations.expiresAt, sql`now()`))).orderBy(asc(reservations.id)).for('update');
|
|
235
|
+
if (activeRows.length === 0) return [];
|
|
236
|
+
const activeIds = activeRows.map((row)=>row.id);
|
|
237
|
+
const activeIdSet = new Set(activeIds);
|
|
238
|
+
const usedRunnerRows = await tx.select({
|
|
239
|
+
id: providerRunners.id,
|
|
240
|
+
reservationId: providerRunners.reservationId,
|
|
241
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
242
|
+
state: providerRunners.state
|
|
243
|
+
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), isNull(providerRunners.reservationReleasedAt), or(inArray(providerRunners.reservationId, activeIds), and(inArray(providerRunners.intendedReservationId, activeIds), notInArray(providerRunners.state, [
|
|
244
|
+
...terminalStates
|
|
245
|
+
])))));
|
|
246
|
+
const usedByReservation = new Map();
|
|
247
|
+
for (const runner of usedRunnerRows){
|
|
248
|
+
if (runner.reservationId && activeIdSet.has(runner.reservationId)) {
|
|
249
|
+
addUsedRunner(usedByReservation, runner.reservationId, runner.id);
|
|
250
|
+
}
|
|
251
|
+
if (runner.intendedReservationId && activeIdSet.has(runner.intendedReservationId) && !terminalStates.includes(runner.state)) {
|
|
252
|
+
addUsedRunner(usedByReservation, runner.intendedReservationId, runner.id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return activeRows.flatMap((reservation)=>{
|
|
256
|
+
const used = usedByReservation.get(reservation.id)?.size ?? 0;
|
|
257
|
+
const pending = Math.max(0, reservation.count - used);
|
|
258
|
+
return pending > 0 ? [
|
|
259
|
+
{
|
|
260
|
+
requiredLabels: reservation.requiredLabels,
|
|
261
|
+
reserved: pending
|
|
262
|
+
}
|
|
263
|
+
] : [];
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function addUsedRunner(usedByReservation, reservationId, runnerId) {
|
|
267
|
+
const runnerIds = usedByReservation.get(reservationId);
|
|
268
|
+
if (runnerIds) runnerIds.add(runnerId);
|
|
269
|
+
else usedByReservation.set(reservationId, new Set([
|
|
270
|
+
runnerId
|
|
271
|
+
]));
|
|
272
|
+
}
|
|
273
|
+
// An expired or missing reservation is stale. A live reservation protects a runner that is
|
|
274
|
+
// still booting or waiting for its activation grace period.
|
|
275
|
+
function canBindRunner(tx, params) {
|
|
276
|
+
return and(or(and(isNull(providerRunners.workspaceId), isNull(providerRunners.reservationId)), and(isNotNull(providerRunners.reservationId), params.scope === 'installation' ? undefined : or(isNull(providerRunners.workspaceId), eq(providerRunners.workspaceId, params.workspaceId)), notExists(tx.select({
|
|
277
|
+
id: reservations.id
|
|
278
|
+
}).from(reservations).where(and(eq(reservations.id, providerRunners.reservationId), gt(reservations.expiresAt, sql`now()`)))))), or(isNull(providerRunners.intendedReservationId), notExists(tx.select({
|
|
279
|
+
id: reservations.id
|
|
280
|
+
}).from(reservations).where(and(eq(reservations.id, providerRunners.intendedReservationId), gt(reservations.expiresAt, sql`now()`))))), isNull(providerRunners.reservationReleasedAt), isNull(providerRunners.runnerSessionId));
|
|
281
|
+
}
|
|
282
|
+
function isBindableRunner(tx, params) {
|
|
283
|
+
return and(eq(providerRunners.provisionerId, params.provisionerId), canBindRunner(tx, params), isNotNull(providerRunners.providerRunnerId), eq(providerRunners.state, 'running'), arrayContains(providerRunners.labels, params.requiredLabels), exists(tx.select({
|
|
177
284
|
id: runnerControlSessions.id
|
|
178
|
-
}).from(runnerControlSessions).where(and(eq(runnerControlSessions.runnerInstanceId, providerRunners.id), eq(runnerControlSessions.provisionerId, params.provisionerId), isNull(runnerControlSessions.closedAt), gt(runnerControlSessions.expiresAt, sql`now()`)))))
|
|
179
|
-
|
|
285
|
+
}).from(runnerControlSessions).where(and(eq(runnerControlSessions.runnerInstanceId, providerRunners.id), eq(runnerControlSessions.provisionerId, params.provisionerId), isNull(runnerControlSessions.closedAt), gt(runnerControlSessions.expiresAt, sql`now()`)))));
|
|
286
|
+
}
|
|
287
|
+
async function listIdleRunnerInstancesTx(tx, params) {
|
|
288
|
+
const lockedRunners = await selectIdleRunnerInstancesTx(tx, params);
|
|
289
|
+
if (lockedRunners.length === 0) return lockedRunners;
|
|
290
|
+
// A SELECT ... FOR UPDATE can wake with a fresh target row but the original
|
|
291
|
+
// statement snapshot for reservation subqueries. Re-check the full predicate
|
|
292
|
+
// in a new statement while retaining the row locks from the first query.
|
|
293
|
+
return await selectIdleRunnerInstancesTx(tx, {
|
|
294
|
+
...params,
|
|
295
|
+
runnerIds: lockedRunners.map((runner)=>runner.id),
|
|
296
|
+
count: lockedRunners.length
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function selectIdleRunnerInstancesTx(tx, params) {
|
|
300
|
+
if (params.runnerIds) {
|
|
301
|
+
// The first pass retains the row locks. Recheck the full predicate in a fresh statement so
|
|
302
|
+
// reservation subqueries see the state that was current when the lock was acquired.
|
|
303
|
+
return await tx.select({
|
|
304
|
+
id: providerRunners.id,
|
|
305
|
+
launchKind: providerRunners.launchKind
|
|
306
|
+
}).from(providerRunners).where(and(inArray(providerRunners.id, params.runnerIds), isBindableRunner(tx, params))).orderBy(asc(providerRunners.createdAt), asc(providerRunners.id)).limit(params.count);
|
|
307
|
+
}
|
|
308
|
+
// Select the oldest candidates first, but leave row locking to a second pass. The cleanup
|
|
309
|
+
// path locks runner rows by id, so the locking pass must use that same order. A skipped row
|
|
310
|
+
// rolls back only this nested savepoint and retries the bounded candidate scan, allowing the
|
|
311
|
+
// next-oldest eligible runner to refill the grant without retaining a partial lock set.
|
|
312
|
+
const retrySelection = Symbol('retry bindable runner selection');
|
|
313
|
+
const idleRunners = await (async ()=>{
|
|
314
|
+
while(true){
|
|
315
|
+
try {
|
|
316
|
+
return await tx.transaction(async (lockTx)=>{
|
|
317
|
+
const candidateRunners = await lockTx.select({
|
|
318
|
+
id: providerRunners.id,
|
|
319
|
+
launchKind: providerRunners.launchKind
|
|
320
|
+
}).from(providerRunners).where(isBindableRunner(lockTx, params)).orderBy(asc(providerRunners.createdAt), asc(providerRunners.id)).limit(params.count);
|
|
321
|
+
if (candidateRunners.length === 0) return candidateRunners;
|
|
322
|
+
// PostgreSQL can acquire row locks before sorting a multi-row FOR UPDATE result. Lock
|
|
323
|
+
// each selected candidate individually so both polling and cleanup use id order.
|
|
324
|
+
const lockedRunners = [];
|
|
325
|
+
for (const candidate of [
|
|
326
|
+
...candidateRunners
|
|
327
|
+
].sort(compareRunnerIds)){
|
|
328
|
+
const [runner] = await lockTx.select({
|
|
329
|
+
id: providerRunners.id,
|
|
330
|
+
launchKind: providerRunners.launchKind
|
|
331
|
+
}).from(providerRunners).where(and(eq(providerRunners.id, candidate.id), isBindableRunner(lockTx, params))).for('update');
|
|
332
|
+
if (runner) lockedRunners.push(runner);
|
|
333
|
+
}
|
|
334
|
+
if (lockedRunners.length !== candidateRunners.length) throw retrySelection;
|
|
335
|
+
return lockedRunners;
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error !== retrySelection) throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
return idleRunners;
|
|
343
|
+
}
|
|
344
|
+
async function bindIdleRunnerInstancesTx(tx, params) {
|
|
345
|
+
const idleRunnerIds = params.idleRunners.map((runner)=>runner.id);
|
|
180
346
|
await tx.update(runnerActivationTokens).set({
|
|
181
347
|
revokedAt: sql`now()`
|
|
182
|
-
}).where(and(inArray(runnerActivationTokens.runnerInstanceId,
|
|
183
|
-
|
|
348
|
+
}).where(and(inArray(runnerActivationTokens.runnerInstanceId, idleRunnerIds), isNull(runnerActivationTokens.consumedAt), isNull(runnerActivationTokens.revokedAt)));
|
|
349
|
+
// listIdleRunnerInstancesTx re-checks the full eligibility predicate in a fresh statement
|
|
350
|
+
// after acquiring row locks. Those locks prevent the validated set from changing before this
|
|
351
|
+
// update runs.
|
|
352
|
+
const boundRunners = await tx.update(providerRunners).set({
|
|
184
353
|
workspaceId: params.workspaceId,
|
|
185
354
|
reservationId: params.reservationId,
|
|
186
355
|
intendedReservationId: null,
|
|
187
356
|
assignedAt: sql`now()`,
|
|
188
357
|
updatedAt: sql`now()`
|
|
189
|
-
}).where(and(inArray(providerRunners.id,
|
|
358
|
+
}).where(and(inArray(providerRunners.id, idleRunnerIds), isBindableRunner(tx, params))).returning({
|
|
359
|
+
id: providerRunners.id,
|
|
360
|
+
launchKind: providerRunners.launchKind
|
|
361
|
+
});
|
|
362
|
+
if (boundRunners.length !== params.idleRunners.length) throw new Error('Locked idle runner set changed before binding');
|
|
363
|
+
const reboundCount = boundRunners.filter((runner)=>runner.launchKind === 'demand').length;
|
|
364
|
+
if (reboundCount > 0) recordProviderRunnerActivationOutcome({
|
|
365
|
+
outcome: 'rebound',
|
|
366
|
+
count: reboundCount
|
|
367
|
+
});
|
|
190
368
|
}
|
|
191
369
|
async function listInstallationDemandWorkspaceIds(eligibleWorkspaceIds) {
|
|
192
370
|
if (eligibleWorkspaceIds.size === 0) return [];
|
|
@@ -228,13 +406,27 @@ export async function deleteReservationsByIds(ids) {
|
|
|
228
406
|
}
|
|
229
407
|
async function deleteReservationsWithCleanupTx(tx, ids, params = {}) {
|
|
230
408
|
if (ids.length === 0) return 0;
|
|
231
|
-
const
|
|
409
|
+
const isAffectedRunner = ()=>or(and(inArray(providerRunners.reservationId, ids), isNull(providerRunners.runnerSessionId), isNull(providerRunners.reservationReleasedAt)), inArray(providerRunners.intendedReservationId, ids));
|
|
410
|
+
const candidateRunners = await tx.select({
|
|
232
411
|
id: providerRunners.id,
|
|
233
412
|
reservationId: providerRunners.reservationId,
|
|
234
413
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
235
414
|
runnerSessionId: providerRunners.runnerSessionId,
|
|
236
415
|
reservationReleasedAt: providerRunners.reservationReleasedAt
|
|
237
|
-
}).from(providerRunners).where(
|
|
416
|
+
}).from(providerRunners).where(isAffectedRunner()).orderBy(asc(providerRunners.id));
|
|
417
|
+
const affectedRunners = [];
|
|
418
|
+
for (const candidate of [
|
|
419
|
+
...candidateRunners
|
|
420
|
+
].sort(compareRunnerIds)){
|
|
421
|
+
const [runner] = await tx.select({
|
|
422
|
+
id: providerRunners.id,
|
|
423
|
+
reservationId: providerRunners.reservationId,
|
|
424
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
425
|
+
runnerSessionId: providerRunners.runnerSessionId,
|
|
426
|
+
reservationReleasedAt: providerRunners.reservationReleasedAt
|
|
427
|
+
}).from(providerRunners).where(and(eq(providerRunners.id, candidate.id), isAffectedRunner())).for('update');
|
|
428
|
+
if (runner) affectedRunners.push(runner);
|
|
429
|
+
}
|
|
238
430
|
const reservationRows = await tx.select({
|
|
239
431
|
id: reservations.id
|
|
240
432
|
}).from(reservations).where(and(inArray(reservations.id, ids), params.expiredOnly ? lt(reservations.expiresAt, sql`now()`) : undefined)).for('update');
|
|
@@ -290,6 +482,9 @@ function deductProvisionerReservations(templates, activeReservations) {
|
|
|
290
482
|
drawSlots(satisfyingTemplates, reservation.reserved);
|
|
291
483
|
}
|
|
292
484
|
}
|
|
485
|
+
function compareRunnerIds(left, right) {
|
|
486
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
487
|
+
}
|
|
293
488
|
function sortDemandRows(rows) {
|
|
294
489
|
return [
|
|
295
490
|
...rows
|