@shipfox/api-runners 12.2.0 → 12.3.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 +12 -0
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +14 -2
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +22 -6
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +3 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +10 -1
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +22 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/config.test.ts +2 -2
- package/src/config.ts +3 -3
- package/src/core/runner-control-sessions.test.ts +169 -4
- package/src/core/runner-control-sessions.ts +22 -1
- package/src/db/index.ts +1 -0
- package/src/db/reservations.test.ts +369 -1
- package/src/db/reservations.ts +74 -9
- package/src/db/runner-instances.test.ts +231 -0
- package/src/db/runner-instances.ts +32 -1
- package/src/db/runner-sessions.ts +26 -15
- package/src/metrics/service.test.ts +99 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
- package/src/presentation/routes/poll-demand.test.ts +23 -6
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -31,30 +31,37 @@ export async function getRunnerSessionById(runnerSessionId) {
|
|
|
31
31
|
}
|
|
32
32
|
export async function createRunnerSessionConsumingActivationToken(params) {
|
|
33
33
|
return await db().transaction(async (tx)=>{
|
|
34
|
-
const [
|
|
35
|
-
|
|
36
|
-
runnerInstanceId:
|
|
34
|
+
const [runner] = await tx.select({
|
|
35
|
+
activationTokenId: runnerActivationTokens.id,
|
|
36
|
+
runnerInstanceId: providerRunners.id,
|
|
37
37
|
workspaceId: providerRunners.workspaceId,
|
|
38
38
|
provisionerId: providerRunners.provisionerId,
|
|
39
|
-
providerRunnerId: providerRunners.providerRunnerId
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
providerRunnerId: providerRunners.providerRunnerId,
|
|
40
|
+
runnerSessionId: providerRunners.runnerSessionId
|
|
41
|
+
}).from(runnerActivationTokens).innerJoin(providerRunners, eq(providerRunners.id, runnerActivationTokens.runnerInstanceId)).where(eq(runnerActivationTokens.id, params.activationTokenId)).limit(1).for('update', {
|
|
42
|
+
of: providerRunners
|
|
43
|
+
});
|
|
44
|
+
if (!runner?.workspaceId || !runner.providerRunnerId || runner.runnerSessionId) throw new Error('Runner activation token is invalid, expired, or has already been used');
|
|
45
|
+
const [activationToken] = await tx.select({
|
|
46
|
+
id: runnerActivationTokens.id
|
|
47
|
+
}).from(runnerActivationTokens).where(and(eq(runnerActivationTokens.id, runner.activationTokenId), eq(runnerActivationTokens.runnerInstanceId, runner.runnerInstanceId), isNull(runnerActivationTokens.consumedAt), isNull(runnerActivationTokens.revokedAt), gt(runnerActivationTokens.expiresAt, sql`now()`))).limit(1).for('update');
|
|
48
|
+
if (!activationToken) throw new Error('Runner activation token is invalid, expired, or has already been used');
|
|
42
49
|
const [provisioner] = await tx.select({
|
|
43
50
|
scope: provisionerTokens.scope
|
|
44
|
-
}).from(provisionerTokens).where(eq(provisionerTokens.id,
|
|
51
|
+
}).from(provisionerTokens).where(eq(provisionerTokens.id, runner.provisionerId)).limit(1);
|
|
45
52
|
const labels = sanitizeRunnerLabelsOrThrow(params.labels, {
|
|
46
53
|
scope: provisioner?.scope ?? 'workspace',
|
|
47
54
|
source: 'activation runner registration'
|
|
48
55
|
});
|
|
49
56
|
if (labels.length === 0) throw new EmptyRunnerLabelsError();
|
|
50
57
|
const [session] = await tx.insert(runnerSessions).values({
|
|
51
|
-
workspaceId:
|
|
58
|
+
workspaceId: runner.workspaceId,
|
|
52
59
|
scope: 'workspace',
|
|
53
|
-
registrationTokenId:
|
|
60
|
+
registrationTokenId: activationToken.id,
|
|
54
61
|
registrationTokenKind: 'activation',
|
|
55
|
-
runnerInstanceId:
|
|
56
|
-
provisionerId:
|
|
57
|
-
providerRunnerId:
|
|
62
|
+
runnerInstanceId: runner.runnerInstanceId,
|
|
63
|
+
provisionerId: runner.provisionerId,
|
|
64
|
+
providerRunnerId: runner.providerRunnerId,
|
|
58
65
|
labels,
|
|
59
66
|
toolCapabilities: params.toolCapabilities ?? null,
|
|
60
67
|
toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,
|
|
@@ -65,15 +72,15 @@ export async function createRunnerSessionConsumingActivationToken(params) {
|
|
|
65
72
|
await tx.update(runnerActivationTokens).set({
|
|
66
73
|
consumedAt: sql`now()`,
|
|
67
74
|
consumedSessionId: session.id
|
|
68
|
-
}).where(eq(runnerActivationTokens.id,
|
|
75
|
+
}).where(eq(runnerActivationTokens.id, activationToken.id));
|
|
69
76
|
await tx.update(providerRunners).set({
|
|
70
77
|
runnerSessionId: session.id,
|
|
71
78
|
updatedAt: sql`now()`
|
|
72
|
-
}).where(and(eq(providerRunners.id,
|
|
79
|
+
}).where(and(eq(providerRunners.id, runner.runnerInstanceId), isNull(providerRunners.runnerSessionId)));
|
|
73
80
|
await tx.update(runnerControlSessions).set({
|
|
74
81
|
closedAt: sql`now()`,
|
|
75
82
|
closeReason: 'activated'
|
|
76
|
-
}).where(and(eq(runnerControlSessions.runnerInstanceId,
|
|
83
|
+
}).where(and(eq(runnerControlSessions.runnerInstanceId, runner.runnerInstanceId), isNull(runnerControlSessions.closedAt)));
|
|
77
84
|
return toRunnerSession(session);
|
|
78
85
|
});
|
|
79
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/runner-sessions.ts"],"sourcesContent":["import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';\nimport {and, asc, eq, gt, inArray, isNull, lt, notExists, or, sql} from 'drizzle-orm';\nimport type {RunnerSession} from '#core/entities/runner-session.js';\nimport {EmptyRunnerLabelsError} from '#core/errors.js';\nimport {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';\nimport {db} from './db.js';\nimport {provisionerTokens} from './schema/provisioner-tokens.js';\nimport {runnerActivationTokens} from './schema/runner-activation-tokens.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\nimport {runnerSessions, toRunnerSession} from './schema/runner-sessions.js';\nimport {runningJobExecutions} from './schema/running-job-executions.js';\n\nexport interface CreateRunnerSessionParams {\n workspaceId: string;\n scope: 'workspace';\n registrationTokenId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n}\n\nexport async function createRunnerSession(\n params: CreateRunnerSessionParams,\n): Promise<RunnerSession> {\n const rows = await db()\n .insert(runnerSessions)\n .values({\n workspaceId: params.workspaceId,\n scope: params.scope,\n registrationTokenId: params.registrationTokenId,\n registrationTokenKind: 'manual',\n labels: params.labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: null,\n claimsUsed: 0,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRunnerSession(row);\n}\n\nexport async function getRunnerSessionById(runnerSessionId: string): Promise<RunnerSession | null> {\n const rows = await db()\n .select()\n .from(runnerSessions)\n .where(eq(runnerSessions.id, runnerSessionId))\n .limit(1);\n\n const row = rows[0];\n return row ? toRunnerSession(row) : null;\n}\n\nexport async function createRunnerSessionConsumingActivationToken(params: {\n activationTokenId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n}) {\n return await db().transaction(async (tx) => {\n const [token] = await tx\n .select({\n id: runnerActivationTokens.id,\n runnerInstanceId: runnerActivationTokens.runnerInstanceId,\n workspaceId: providerRunners.workspaceId,\n provisionerId: providerRunners.provisionerId,\n providerRunnerId: providerRunners.providerRunnerId,\n })\n .from(runnerActivationTokens)\n .innerJoin(providerRunners, eq(providerRunners.id, runnerActivationTokens.runnerInstanceId))\n .where(\n and(\n eq(runnerActivationTokens.id, params.activationTokenId),\n isNull(runnerActivationTokens.consumedAt),\n isNull(runnerActivationTokens.revokedAt),\n gt(runnerActivationTokens.expiresAt, sql`now()`),\n isNull(providerRunners.runnerSessionId),\n ),\n )\n .limit(1)\n .for('update');\n if (!token?.workspaceId || !token.providerRunnerId)\n throw new Error('Runner activation token is invalid, expired, or has already been used');\n const [provisioner] = await tx\n .select({scope: provisionerTokens.scope})\n .from(provisionerTokens)\n .where(eq(provisionerTokens.id, token.provisionerId))\n .limit(1);\n const labels = sanitizeRunnerLabelsOrThrow(params.labels, {\n scope: provisioner?.scope ?? 'workspace',\n source: 'activation runner registration',\n });\n if (labels.length === 0) throw new EmptyRunnerLabelsError();\n const [session] = await tx\n .insert(runnerSessions)\n .values({\n workspaceId: token.workspaceId,\n scope: 'workspace',\n registrationTokenId: token.id,\n registrationTokenKind: 'activation',\n runnerInstanceId: token.runnerInstanceId,\n provisionerId: token.provisionerId,\n providerRunnerId: token.providerRunnerId,\n labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: 1,\n claimsUsed: 0,\n })\n .returning();\n if (!session) throw new Error('Runner activation session insert returned no row');\n await tx\n .update(runnerActivationTokens)\n .set({consumedAt: sql`now()`, consumedSessionId: session.id})\n .where(eq(runnerActivationTokens.id, token.id));\n await tx\n .update(providerRunners)\n .set({runnerSessionId: session.id, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.id, token.runnerInstanceId),\n isNull(providerRunners.runnerSessionId),\n ),\n );\n await tx\n .update(runnerControlSessions)\n .set({closedAt: sql`now()`, closeReason: 'activated'})\n .where(\n and(\n eq(runnerControlSessions.runnerInstanceId, token.runnerInstanceId),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n return toRunnerSession(session);\n });\n}\n\nexport interface DeleteExpiredRunnerSessionsParams {\n manualRetentionDays: number;\n ephemeralRetentionDays: number;\n limit?: number;\n}\n\nexport async function deleteExpiredRunnerSessions(\n params: DeleteExpiredRunnerSessionsParams,\n): Promise<number> {\n const expiredIds = db()\n .select({id: runnerSessions.id})\n .from(runnerSessions)\n .where(\n and(\n or(\n and(\n eq(runnerSessions.registrationTokenKind, 'manual'),\n lt(\n runnerSessions.createdAt,\n sql`now() - (${params.manualRetentionDays} || ' days')::interval`,\n ),\n ),\n and(\n inArray(runnerSessions.registrationTokenKind, ['ephemeral', 'activation']),\n lt(\n runnerSessions.createdAt,\n sql`now() - (${params.ephemeralRetentionDays} || ' days')::interval`,\n ),\n ),\n ),\n notExists(\n db()\n .select({id: runningJobExecutions.id})\n .from(runningJobExecutions)\n .where(eq(runningJobExecutions.runnerSessionId, runnerSessions.id)),\n ),\n ),\n )\n .orderBy(asc(runnerSessions.createdAt), asc(runnerSessions.id))\n .limit(params.limit ?? 1000);\n\n const deleted = await db()\n .delete(runnerSessions)\n .where(inArray(runnerSessions.id, expiredIds))\n .returning({id: runnerSessions.id});\n\n return deleted.length;\n}\n"],"names":["and","asc","eq","gt","inArray","isNull","lt","notExists","or","sql","EmptyRunnerLabelsError","sanitizeRunnerLabelsOrThrow","db","provisionerTokens","runnerActivationTokens","runnerControlSessions","providerRunners","runnerSessions","toRunnerSession","runningJobExecutions","createRunnerSession","params","rows","insert","values","workspaceId","scope","registrationTokenId","registrationTokenKind","labels","toolCapabilities","toolCapabilitiesReportedAt","maxClaims","claimsUsed","returning","row","Error","getRunnerSessionById","runnerSessionId","select","from","where","id","limit","createRunnerSessionConsumingActivationToken","transaction","tx","token","runnerInstanceId","provisionerId","providerRunnerId","innerJoin","activationTokenId","consumedAt","revokedAt","expiresAt","for","provisioner","source","length","session","update","set","consumedSessionId","updatedAt","closedAt","closeReason","deleteExpiredRunnerSessions","expiredIds","createdAt","manualRetentionDays","ephemeralRetentionDays","orderBy","deleted","delete"],"mappings":"AACA,SAAQA,GAAG,EAAEC,GAAG,EAAEC,EAAE,EAAEC,EAAE,EAAEC,OAAO,EAAEC,MAAM,EAAEC,EAAE,EAAEC,SAAS,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AAEtF,SAAQC,sBAAsB,QAAO,kBAAkB;AACvD,SAAQC,2BAA2B,QAAO,yBAAyB;AACnE,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,iBAAiB,QAAO,iCAAiC;AACjE,SAAQC,sBAAsB,QAAO,uCAAuC;AAC5E,SAAQC,qBAAqB,QAAO,sCAAsC;AAC1E,SAAQC,eAAe,QAAO,+BAA+B;AAC7D,SAAQC,cAAc,EAAEC,eAAe,QAAO,8BAA8B;AAC5E,SAAQC,oBAAoB,QAAO,qCAAqC;AAUxE,OAAO,eAAeC,oBACpBC,MAAiC;IAEjC,MAAMC,OAAO,MAAMV,KAChBW,MAAM,CAACN,gBACPO,MAAM,CAAC;QACNC,aAAaJ,OAAOI,WAAW;QAC/BC,OAAOL,OAAOK,KAAK;QACnBC,qBAAqBN,OAAOM,mBAAmB;QAC/CC,uBAAuB;QACvBC,QAAQR,OAAOQ,MAAM;QACrBC,kBAAkBT,OAAOS,gBAAgB,IAAI;QAC7CC,4BAA4BV,OAAOS,gBAAgB,GAAGrB,GAAG,CAAC,KAAK,CAAC,GAAG;QACnEuB,WAAW;QACXC,YAAY;IACd,GACCC,SAAS;IAEZ,MAAMC,MAAMb,IAAI,CAAC,EAAE;IACnB,IAAI,CAACa,KAAK,MAAM,IAAIC,MAAM;IAC1B,OAAOlB,gBAAgBiB;AACzB;AAEA,OAAO,eAAeE,qBAAqBC,eAAuB;IAChE,MAAMhB,OAAO,MAAMV,KAChB2B,MAAM,GACNC,IAAI,CAACvB,gBACLwB,KAAK,CAACvC,GAAGe,eAAeyB,EAAE,EAAEJ,kBAC5BK,KAAK,CAAC;IAET,MAAMR,MAAMb,IAAI,CAAC,EAAE;IACnB,OAAOa,MAAMjB,gBAAgBiB,OAAO;AACtC;AAEA,OAAO,eAAeS,4CAA4CvB,MAIjE;IACC,OAAO,MAAMT,KAAKiC,WAAW,CAAC,OAAOC;QACnC,MAAM,CAACC,MAAM,GAAG,MAAMD,GACnBP,MAAM,CAAC;YACNG,IAAI5B,uBAAuB4B,EAAE;YAC7BM,kBAAkBlC,uBAAuBkC,gBAAgB;YACzDvB,aAAaT,gBAAgBS,WAAW;YACxCwB,eAAejC,gBAAgBiC,aAAa;YAC5CC,kBAAkBlC,gBAAgBkC,gBAAgB;QACpD,GACCV,IAAI,CAAC1B,wBACLqC,SAAS,CAACnC,iBAAiBd,GAAGc,gBAAgB0B,EAAE,EAAE5B,uBAAuBkC,gBAAgB,GACzFP,KAAK,CACJzC,IACEE,GAAGY,uBAAuB4B,EAAE,EAAErB,OAAO+B,iBAAiB,GACtD/C,OAAOS,uBAAuBuC,UAAU,GACxChD,OAAOS,uBAAuBwC,SAAS,GACvCnD,GAAGW,uBAAuByC,SAAS,EAAE9C,GAAG,CAAC,KAAK,CAAC,GAC/CJ,OAAOW,gBAAgBsB,eAAe,IAGzCK,KAAK,CAAC,GACNa,GAAG,CAAC;QACP,IAAI,CAACT,OAAOtB,eAAe,CAACsB,MAAMG,gBAAgB,EAChD,MAAM,IAAId,MAAM;QAClB,MAAM,CAACqB,YAAY,GAAG,MAAMX,GACzBP,MAAM,CAAC;YAACb,OAAOb,kBAAkBa,KAAK;QAAA,GACtCc,IAAI,CAAC3B,mBACL4B,KAAK,CAACvC,GAAGW,kBAAkB6B,EAAE,EAAEK,MAAME,aAAa,GAClDN,KAAK,CAAC;QACT,MAAMd,SAASlB,4BAA4BU,OAAOQ,MAAM,EAAE;YACxDH,OAAO+B,aAAa/B,SAAS;YAC7BgC,QAAQ;QACV;QACA,IAAI7B,OAAO8B,MAAM,KAAK,GAAG,MAAM,IAAIjD;QACnC,MAAM,CAACkD,QAAQ,GAAG,MAAMd,GACrBvB,MAAM,CAACN,gBACPO,MAAM,CAAC;YACNC,aAAasB,MAAMtB,WAAW;YAC9BC,OAAO;YACPC,qBAAqBoB,MAAML,EAAE;YAC7Bd,uBAAuB;YACvBoB,kBAAkBD,MAAMC,gBAAgB;YACxCC,eAAeF,MAAME,aAAa;YAClCC,kBAAkBH,MAAMG,gBAAgB;YACxCrB;YACAC,kBAAkBT,OAAOS,gBAAgB,IAAI;YAC7CC,4BAA4BV,OAAOS,gBAAgB,GAAGrB,GAAG,CAAC,KAAK,CAAC,GAAG;YACnEuB,WAAW;YACXC,YAAY;QACd,GACCC,SAAS;QACZ,IAAI,CAAC0B,SAAS,MAAM,IAAIxB,MAAM;QAC9B,MAAMU,GACHe,MAAM,CAAC/C,wBACPgD,GAAG,CAAC;YAACT,YAAY5C,GAAG,CAAC,KAAK,CAAC;YAAEsD,mBAAmBH,QAAQlB,EAAE;QAAA,GAC1DD,KAAK,CAACvC,GAAGY,uBAAuB4B,EAAE,EAAEK,MAAML,EAAE;QAC/C,MAAMI,GACHe,MAAM,CAAC7C,iBACP8C,GAAG,CAAC;YAACxB,iBAAiBsB,QAAQlB,EAAE;YAAEsB,WAAWvD,GAAG,CAAC,KAAK,CAAC;QAAA,GACvDgC,KAAK,CACJzC,IACEE,GAAGc,gBAAgB0B,EAAE,EAAEK,MAAMC,gBAAgB,GAC7C3C,OAAOW,gBAAgBsB,eAAe;QAG5C,MAAMQ,GACHe,MAAM,CAAC9C,uBACP+C,GAAG,CAAC;YAACG,UAAUxD,GAAG,CAAC,KAAK,CAAC;YAAEyD,aAAa;QAAW,GACnDzB,KAAK,CACJzC,IACEE,GAAGa,sBAAsBiC,gBAAgB,EAAED,MAAMC,gBAAgB,GACjE3C,OAAOU,sBAAsBkD,QAAQ;QAG3C,OAAO/C,gBAAgB0C;IACzB;AACF;AAQA,OAAO,eAAeO,4BACpB9C,MAAyC;IAEzC,MAAM+C,aAAaxD,KAChB2B,MAAM,CAAC;QAACG,IAAIzB,eAAeyB,EAAE;IAAA,GAC7BF,IAAI,CAACvB,gBACLwB,KAAK,CACJzC,IACEQ,GACER,IACEE,GAAGe,eAAeW,qBAAqB,EAAE,WACzCtB,GACEW,eAAeoD,SAAS,EACxB5D,GAAG,CAAC,SAAS,EAAEY,OAAOiD,mBAAmB,CAAC,sBAAsB,CAAC,IAGrEtE,IACEI,QAAQa,eAAeW,qBAAqB,EAAE;QAAC;QAAa;KAAa,GACzEtB,GACEW,eAAeoD,SAAS,EACxB5D,GAAG,CAAC,SAAS,EAAEY,OAAOkD,sBAAsB,CAAC,sBAAsB,CAAC,KAI1EhE,UACEK,KACG2B,MAAM,CAAC;QAACG,IAAIvB,qBAAqBuB,EAAE;IAAA,GACnCF,IAAI,CAACrB,sBACLsB,KAAK,CAACvC,GAAGiB,qBAAqBmB,eAAe,EAAErB,eAAeyB,EAAE,MAIxE8B,OAAO,CAACvE,IAAIgB,eAAeoD,SAAS,GAAGpE,IAAIgB,eAAeyB,EAAE,GAC5DC,KAAK,CAACtB,OAAOsB,KAAK,IAAI;IAEzB,MAAM8B,UAAU,MAAM7D,KACnB8D,MAAM,CAACzD,gBACPwB,KAAK,CAACrC,QAAQa,eAAeyB,EAAE,EAAE0B,aACjClC,SAAS,CAAC;QAACQ,IAAIzB,eAAeyB,EAAE;IAAA;IAEnC,OAAO+B,QAAQd,MAAM;AACvB"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/runner-sessions.ts"],"sourcesContent":["import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';\nimport {and, asc, eq, gt, inArray, isNull, lt, notExists, or, sql} from 'drizzle-orm';\nimport type {RunnerSession} from '#core/entities/runner-session.js';\nimport {EmptyRunnerLabelsError} from '#core/errors.js';\nimport {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';\nimport {db} from './db.js';\nimport {provisionerTokens} from './schema/provisioner-tokens.js';\nimport {runnerActivationTokens} from './schema/runner-activation-tokens.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\nimport {runnerSessions, toRunnerSession} from './schema/runner-sessions.js';\nimport {runningJobExecutions} from './schema/running-job-executions.js';\n\nexport interface CreateRunnerSessionParams {\n workspaceId: string;\n scope: 'workspace';\n registrationTokenId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n}\n\nexport async function createRunnerSession(\n params: CreateRunnerSessionParams,\n): Promise<RunnerSession> {\n const rows = await db()\n .insert(runnerSessions)\n .values({\n workspaceId: params.workspaceId,\n scope: params.scope,\n registrationTokenId: params.registrationTokenId,\n registrationTokenKind: 'manual',\n labels: params.labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: null,\n claimsUsed: 0,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRunnerSession(row);\n}\n\nexport async function getRunnerSessionById(runnerSessionId: string): Promise<RunnerSession | null> {\n const rows = await db()\n .select()\n .from(runnerSessions)\n .where(eq(runnerSessions.id, runnerSessionId))\n .limit(1);\n\n const row = rows[0];\n return row ? toRunnerSession(row) : null;\n}\n\nexport async function createRunnerSessionConsumingActivationToken(params: {\n activationTokenId: string;\n labels: string[];\n toolCapabilities?: RunnerToolCapabilitiesDto | null;\n}) {\n return await db().transaction(async (tx) => {\n const [runner] = await tx\n .select({\n activationTokenId: runnerActivationTokens.id,\n runnerInstanceId: providerRunners.id,\n workspaceId: providerRunners.workspaceId,\n provisionerId: providerRunners.provisionerId,\n providerRunnerId: providerRunners.providerRunnerId,\n runnerSessionId: providerRunners.runnerSessionId,\n })\n .from(runnerActivationTokens)\n .innerJoin(providerRunners, eq(providerRunners.id, runnerActivationTokens.runnerInstanceId))\n .where(eq(runnerActivationTokens.id, params.activationTokenId))\n .limit(1)\n .for('update', {of: providerRunners});\n if (!runner?.workspaceId || !runner.providerRunnerId || runner.runnerSessionId)\n throw new Error('Runner activation token is invalid, expired, or has already been used');\n\n const [activationToken] = await tx\n .select({id: runnerActivationTokens.id})\n .from(runnerActivationTokens)\n .where(\n and(\n eq(runnerActivationTokens.id, runner.activationTokenId),\n eq(runnerActivationTokens.runnerInstanceId, runner.runnerInstanceId),\n isNull(runnerActivationTokens.consumedAt),\n isNull(runnerActivationTokens.revokedAt),\n gt(runnerActivationTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1)\n .for('update');\n if (!activationToken)\n throw new Error('Runner activation token is invalid, expired, or has already been used');\n\n const [provisioner] = await tx\n .select({scope: provisionerTokens.scope})\n .from(provisionerTokens)\n .where(eq(provisionerTokens.id, runner.provisionerId))\n .limit(1);\n const labels = sanitizeRunnerLabelsOrThrow(params.labels, {\n scope: provisioner?.scope ?? 'workspace',\n source: 'activation runner registration',\n });\n if (labels.length === 0) throw new EmptyRunnerLabelsError();\n const [session] = await tx\n .insert(runnerSessions)\n .values({\n workspaceId: runner.workspaceId,\n scope: 'workspace',\n registrationTokenId: activationToken.id,\n registrationTokenKind: 'activation',\n runnerInstanceId: runner.runnerInstanceId,\n provisionerId: runner.provisionerId,\n providerRunnerId: runner.providerRunnerId,\n labels,\n toolCapabilities: params.toolCapabilities ?? null,\n toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,\n maxClaims: 1,\n claimsUsed: 0,\n })\n .returning();\n if (!session) throw new Error('Runner activation session insert returned no row');\n await tx\n .update(runnerActivationTokens)\n .set({consumedAt: sql`now()`, consumedSessionId: session.id})\n .where(eq(runnerActivationTokens.id, activationToken.id));\n await tx\n .update(providerRunners)\n .set({runnerSessionId: session.id, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.id, runner.runnerInstanceId),\n isNull(providerRunners.runnerSessionId),\n ),\n );\n await tx\n .update(runnerControlSessions)\n .set({closedAt: sql`now()`, closeReason: 'activated'})\n .where(\n and(\n eq(runnerControlSessions.runnerInstanceId, runner.runnerInstanceId),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n return toRunnerSession(session);\n });\n}\n\nexport interface DeleteExpiredRunnerSessionsParams {\n manualRetentionDays: number;\n ephemeralRetentionDays: number;\n limit?: number;\n}\n\nexport async function deleteExpiredRunnerSessions(\n params: DeleteExpiredRunnerSessionsParams,\n): Promise<number> {\n const expiredIds = db()\n .select({id: runnerSessions.id})\n .from(runnerSessions)\n .where(\n and(\n or(\n and(\n eq(runnerSessions.registrationTokenKind, 'manual'),\n lt(\n runnerSessions.createdAt,\n sql`now() - (${params.manualRetentionDays} || ' days')::interval`,\n ),\n ),\n and(\n inArray(runnerSessions.registrationTokenKind, ['ephemeral', 'activation']),\n lt(\n runnerSessions.createdAt,\n sql`now() - (${params.ephemeralRetentionDays} || ' days')::interval`,\n ),\n ),\n ),\n notExists(\n db()\n .select({id: runningJobExecutions.id})\n .from(runningJobExecutions)\n .where(eq(runningJobExecutions.runnerSessionId, runnerSessions.id)),\n ),\n ),\n )\n .orderBy(asc(runnerSessions.createdAt), asc(runnerSessions.id))\n .limit(params.limit ?? 1000);\n\n const deleted = await db()\n .delete(runnerSessions)\n .where(inArray(runnerSessions.id, expiredIds))\n .returning({id: runnerSessions.id});\n\n return deleted.length;\n}\n"],"names":["and","asc","eq","gt","inArray","isNull","lt","notExists","or","sql","EmptyRunnerLabelsError","sanitizeRunnerLabelsOrThrow","db","provisionerTokens","runnerActivationTokens","runnerControlSessions","providerRunners","runnerSessions","toRunnerSession","runningJobExecutions","createRunnerSession","params","rows","insert","values","workspaceId","scope","registrationTokenId","registrationTokenKind","labels","toolCapabilities","toolCapabilitiesReportedAt","maxClaims","claimsUsed","returning","row","Error","getRunnerSessionById","runnerSessionId","select","from","where","id","limit","createRunnerSessionConsumingActivationToken","transaction","tx","runner","activationTokenId","runnerInstanceId","provisionerId","providerRunnerId","innerJoin","for","of","activationToken","consumedAt","revokedAt","expiresAt","provisioner","source","length","session","update","set","consumedSessionId","updatedAt","closedAt","closeReason","deleteExpiredRunnerSessions","expiredIds","createdAt","manualRetentionDays","ephemeralRetentionDays","orderBy","deleted","delete"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/metrics/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/metrics/service.ts"],"names":[],"mappings":"AAKA,wBAAgB,6BAA6B,IAAI,IAAI,CAoCpD"}
|
package/dist/metrics/service.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getServiceMetricsProvider } from '@shipfox/node-opentelemetry';
|
|
2
|
+
import { config } from '#config.js';
|
|
2
3
|
import { getJobExecutionQueueDepth } from '#db/job-executions.js';
|
|
4
|
+
import { countStaleEnrolledRunnerInstances } from '#db/runner-instances.js';
|
|
3
5
|
export function registerRunnersServiceMetrics() {
|
|
4
6
|
const meter = getServiceMetricsProvider().getMeter('runners');
|
|
5
7
|
const pendingJobExecutions = meter.createObservableGauge('runners_pending_job_executions', {
|
|
@@ -8,13 +10,27 @@ export function registerRunnersServiceMetrics() {
|
|
|
8
10
|
const runningJobExecutions = meter.createObservableGauge('runners_running_job_executions', {
|
|
9
11
|
description: 'Job executions currently claimed by a runner and in progress'
|
|
10
12
|
});
|
|
13
|
+
const enrolledRunnersWithoutRecentReport = meter.createObservableGauge('runners_enrolled_without_recent_report', {
|
|
14
|
+
description: 'Running enrolled runners with a live control session, no workspace or runner session, and no recent provisioner report after the stale-runner grace window'
|
|
15
|
+
});
|
|
11
16
|
meter.addBatchObservableCallback(async (observer)=>{
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const [depthResult, staleEnrolledRunnerCountResult] = await Promise.allSettled([
|
|
18
|
+
getJobExecutionQueueDepth(),
|
|
19
|
+
countStaleEnrolledRunnerInstances({
|
|
20
|
+
graceSeconds: config.RUNNER_STALE_PROVISIONED_RUNNER_THRESHOLD_SECONDS
|
|
21
|
+
})
|
|
22
|
+
]);
|
|
23
|
+
if (depthResult.status === 'fulfilled') {
|
|
24
|
+
observer.observe(pendingJobExecutions, depthResult.value.pendingJobExecutions);
|
|
25
|
+
observer.observe(runningJobExecutions, depthResult.value.runningJobExecutions);
|
|
26
|
+
}
|
|
27
|
+
if (staleEnrolledRunnerCountResult.status === 'fulfilled') {
|
|
28
|
+
observer.observe(enrolledRunnersWithoutRecentReport, staleEnrolledRunnerCountResult.value);
|
|
29
|
+
}
|
|
15
30
|
}, [
|
|
16
31
|
pendingJobExecutions,
|
|
17
|
-
runningJobExecutions
|
|
32
|
+
runningJobExecutions,
|
|
33
|
+
enrolledRunnersWithoutRecentReport
|
|
18
34
|
]);
|
|
19
35
|
}
|
|
20
36
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/metrics/service.ts"],"sourcesContent":["import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';\nimport {getJobExecutionQueueDepth} from '#db/job-executions.js';\n\nexport function registerRunnersServiceMetrics(): void {\n const meter = getServiceMetricsProvider().getMeter('runners');\n\n const pendingJobExecutions = meter.createObservableGauge('runners_pending_job_executions', {\n description: 'Job executions currently waiting in the queue to be claimed',\n });\n const runningJobExecutions = meter.createObservableGauge('runners_running_job_executions', {\n description: 'Job executions currently claimed by a runner and in progress',\n });\n\n meter.addBatchObservableCallback(\n async (observer) => {\n const
|
|
1
|
+
{"version":3,"sources":["../../src/metrics/service.ts"],"sourcesContent":["import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';\nimport {config} from '#config.js';\nimport {getJobExecutionQueueDepth} from '#db/job-executions.js';\nimport {countStaleEnrolledRunnerInstances} from '#db/runner-instances.js';\n\nexport function registerRunnersServiceMetrics(): void {\n const meter = getServiceMetricsProvider().getMeter('runners');\n\n const pendingJobExecutions = meter.createObservableGauge('runners_pending_job_executions', {\n description: 'Job executions currently waiting in the queue to be claimed',\n });\n const runningJobExecutions = meter.createObservableGauge('runners_running_job_executions', {\n description: 'Job executions currently claimed by a runner and in progress',\n });\n const enrolledRunnersWithoutRecentReport = meter.createObservableGauge(\n 'runners_enrolled_without_recent_report',\n {\n description:\n 'Running enrolled runners with a live control session, no workspace or runner session, and no recent provisioner report after the stale-runner grace window',\n },\n );\n\n meter.addBatchObservableCallback(\n async (observer) => {\n const [depthResult, staleEnrolledRunnerCountResult] = await Promise.allSettled([\n getJobExecutionQueueDepth(),\n countStaleEnrolledRunnerInstances({\n graceSeconds: config.RUNNER_STALE_PROVISIONED_RUNNER_THRESHOLD_SECONDS,\n }),\n ]);\n\n if (depthResult.status === 'fulfilled') {\n observer.observe(pendingJobExecutions, depthResult.value.pendingJobExecutions);\n observer.observe(runningJobExecutions, depthResult.value.runningJobExecutions);\n }\n if (staleEnrolledRunnerCountResult.status === 'fulfilled') {\n observer.observe(enrolledRunnersWithoutRecentReport, staleEnrolledRunnerCountResult.value);\n }\n },\n [pendingJobExecutions, runningJobExecutions, enrolledRunnersWithoutRecentReport],\n );\n}\n"],"names":["getServiceMetricsProvider","config","getJobExecutionQueueDepth","countStaleEnrolledRunnerInstances","registerRunnersServiceMetrics","meter","getMeter","pendingJobExecutions","createObservableGauge","description","runningJobExecutions","enrolledRunnersWithoutRecentReport","addBatchObservableCallback","observer","depthResult","staleEnrolledRunnerCountResult","Promise","allSettled","graceSeconds","RUNNER_STALE_PROVISIONED_RUNNER_THRESHOLD_SECONDS","status","observe","value"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,8BAA8B;AACtE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,yBAAyB,QAAO,wBAAwB;AAChE,SAAQC,iCAAiC,QAAO,0BAA0B;AAE1E,OAAO,SAASC;IACd,MAAMC,QAAQL,4BAA4BM,QAAQ,CAAC;IAEnD,MAAMC,uBAAuBF,MAAMG,qBAAqB,CAAC,kCAAkC;QACzFC,aAAa;IACf;IACA,MAAMC,uBAAuBL,MAAMG,qBAAqB,CAAC,kCAAkC;QACzFC,aAAa;IACf;IACA,MAAME,qCAAqCN,MAAMG,qBAAqB,CACpE,0CACA;QACEC,aACE;IACJ;IAGFJ,MAAMO,0BAA0B,CAC9B,OAAOC;QACL,MAAM,CAACC,aAAaC,+BAA+B,GAAG,MAAMC,QAAQC,UAAU,CAAC;YAC7Ef;YACAC,kCAAkC;gBAChCe,cAAcjB,OAAOkB,iDAAiD;YACxE;SACD;QAED,IAAIL,YAAYM,MAAM,KAAK,aAAa;YACtCP,SAASQ,OAAO,CAACd,sBAAsBO,YAAYQ,KAAK,CAACf,oBAAoB;YAC7EM,SAASQ,OAAO,CAACX,sBAAsBI,YAAYQ,KAAK,CAACZ,oBAAoB;QAC/E;QACA,IAAIK,+BAA+BK,MAAM,KAAK,aAAa;YACzDP,SAASQ,OAAO,CAACV,oCAAoCI,+BAA+BO,KAAK;QAC3F;IACF,GACA;QAACf;QAAsBG;QAAsBC;KAAmC;AAEpF"}
|