@shipfox/api-runners 6.0.0 → 7.0.2
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 +26 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/entities/runner-instance.d.ts +1 -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/errors.d.ts +6 -6
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +8 -8
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/runner-assignments.d.ts +6 -0
- package/dist/core/runner-assignments.d.ts.map +1 -0
- package/dist/core/runner-assignments.js +6 -0
- package/dist/core/runner-assignments.js.map +1 -0
- package/dist/core/runner-instances.d.ts +2 -9
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js +3 -6
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/db/db.d.ts +34 -218
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +0 -2
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -2
- package/dist/db/index.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +7 -0
- package/dist/db/runner-assignments.d.ts.map +1 -0
- package/dist/db/runner-assignments.js +55 -0
- package/dist/db/runner-assignments.js.map +1 -0
- package/dist/db/runner-instances.d.ts +2 -9
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +2 -16
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/schema/runner-instances.d.ts +17 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +5 -1
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/index.d.ts +0 -1
- package/dist/presentation/dto/index.d.ts.map +1 -1
- package/dist/presentation/dto/index.js +0 -1
- package/dist/presentation/dto/index.js.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.d.ts +2 -0
- package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -0
- package/dist/presentation/routes/assign-runner-instances.js +40 -0
- package/dist/presentation/routes/assign-runner-instances.js.map +1 -0
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +4 -8
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +1 -0
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +27 -0
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0000_initial.sql +1 -12
- package/drizzle/meta/0000_snapshot.json +6 -97
- package/package.json +8 -29
- package/src/config.ts +1 -1
- package/src/core/entities/runner-instance.ts +1 -0
- package/src/core/errors.ts +8 -8
- package/src/core/index.ts +2 -9
- package/src/core/runner-assignments.ts +9 -0
- package/src/core/runner-instances.ts +4 -13
- package/src/db/db.ts +0 -2
- package/src/db/index.ts +2 -3
- package/src/db/runner-assignments.test.ts +163 -0
- package/src/db/runner-assignments.ts +115 -0
- package/src/db/runner-instances.test.ts +36 -23
- package/src/db/runner-instances.ts +3 -23
- package/src/db/schema/runner-instances.ts +3 -1
- package/src/index.ts +0 -9
- package/src/presentation/dto/index.ts +0 -4
- package/src/presentation/routes/assign-runner-instances.ts +49 -0
- package/src/presentation/routes/index.ts +4 -7
- package/src/presentation/routes/runner-enrollment.test.ts +60 -0
- package/src/presentation/routes/runner-enrollment.ts +22 -0
- package/test/globalSetup.ts +0 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/core/capacity-assignments.d.ts +0 -7
- package/dist/core/capacity-assignments.d.ts.map +0 -1
- package/dist/core/capacity-assignments.js +0 -6
- package/dist/core/capacity-assignments.js.map +0 -1
- package/dist/core/entities/capacity-assignment.d.ts +0 -9
- package/dist/core/entities/capacity-assignment.d.ts.map +0 -1
- package/dist/core/entities/capacity-assignment.js +0 -3
- package/dist/core/entities/capacity-assignment.js.map +0 -1
- package/dist/db/capacity-assignments.d.ts +0 -7
- package/dist/db/capacity-assignments.d.ts.map +0 -1
- package/dist/db/capacity-assignments.js +0 -72
- package/dist/db/capacity-assignments.js.map +0 -1
- package/dist/db/schema/capacity-assignments.d.ts +0 -113
- package/dist/db/schema/capacity-assignments.d.ts.map +0 -1
- package/dist/db/schema/capacity-assignments.js +0 -29
- package/dist/db/schema/capacity-assignments.js.map +0 -1
- package/dist/presentation/dto/mint-registration-tokens.d.ts +0 -7
- package/dist/presentation/dto/mint-registration-tokens.d.ts.map +0 -1
- package/dist/presentation/dto/mint-registration-tokens.js +0 -19
- package/dist/presentation/dto/mint-registration-tokens.js.map +0 -1
- package/dist/presentation/routes/assign-capacity.d.ts +0 -2
- package/dist/presentation/routes/assign-capacity.d.ts.map +0 -1
- package/dist/presentation/routes/assign-capacity.js +0 -44
- package/dist/presentation/routes/assign-capacity.js.map +0 -1
- package/dist/presentation/routes/mint-registration-tokens.d.ts +0 -2
- package/dist/presentation/routes/mint-registration-tokens.d.ts.map +0 -1
- package/dist/presentation/routes/mint-registration-tokens.js +0 -74
- package/dist/presentation/routes/mint-registration-tokens.js.map +0 -1
- package/dist/presentation/routes/planned-capacity.d.ts +0 -3
- package/dist/presentation/routes/planned-capacity.d.ts.map +0 -1
- package/dist/presentation/routes/planned-capacity.js +0 -54
- package/dist/presentation/routes/planned-capacity.js.map +0 -1
- package/src/core/capacity-assignments.ts +0 -10
- package/src/core/entities/capacity-assignment.ts +0 -8
- package/src/db/capacity-assignments.test.ts +0 -159
- package/src/db/capacity-assignments.ts +0 -116
- package/src/db/schema/capacity-assignments.ts +0 -34
- package/src/presentation/dto/mint-registration-tokens.ts +0 -28
- package/src/presentation/routes/assign-capacity.ts +0 -48
- package/src/presentation/routes/mint-registration-tokens.test.ts +0 -453
- package/src/presentation/routes/mint-registration-tokens.ts +0 -80
- package/src/presentation/routes/planned-capacity.ts +0 -49
package/drizzle/0000_initial.sql
CHANGED
|
@@ -2,18 +2,6 @@ CREATE TYPE "public"."runners_provider_runner_state" AS ENUM('starting', 'runnin
|
|
|
2
2
|
CREATE TYPE "public"."runners_provisioner_scope" AS ENUM('workspace', 'installation');--> statement-breakpoint
|
|
3
3
|
CREATE TYPE "public"."runners_runner_session_registration_token_kind" AS ENUM('manual', 'ephemeral', 'activation');--> statement-breakpoint
|
|
4
4
|
CREATE TYPE "public"."runners_runner_session_scope" AS ENUM('workspace');--> statement-breakpoint
|
|
5
|
-
CREATE TABLE "runners_capacity_assignments" (
|
|
6
|
-
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
7
|
-
"capacity_id" uuid NOT NULL,
|
|
8
|
-
"reservation_id" uuid NOT NULL,
|
|
9
|
-
"workspace_id" uuid NOT NULL,
|
|
10
|
-
"provisioner_id" uuid NOT NULL,
|
|
11
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
12
|
-
);
|
|
13
|
-
--> statement-breakpoint
|
|
14
|
-
CREATE UNIQUE INDEX "runners_capacity_assignments_capacity_unique" ON "runners_capacity_assignments" USING btree ("capacity_id");--> statement-breakpoint
|
|
15
|
-
CREATE INDEX "runners_capacity_assignments_reservation_idx" ON "runners_capacity_assignments" USING btree ("reservation_id");--> statement-breakpoint
|
|
16
|
-
CREATE INDEX "runners_capacity_assignments_workspace_idx" ON "runners_capacity_assignments" USING btree ("workspace_id");--> statement-breakpoint
|
|
17
5
|
CREATE TABLE "runners_ephemeral_registration_tokens" (
|
|
18
6
|
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
19
7
|
"workspace_id" uuid,
|
|
@@ -73,6 +61,7 @@ CREATE TABLE "runners_runner_instances" (
|
|
|
73
61
|
"provisioner_id" uuid NOT NULL,
|
|
74
62
|
"provider_runner_id" text,
|
|
75
63
|
"reservation_id" uuid,
|
|
64
|
+
"assigned_at" timestamp with time zone,
|
|
76
65
|
"template_key" text,
|
|
77
66
|
"labels" text[] DEFAULT '{}' NOT NULL,
|
|
78
67
|
"state" "runners_provider_runner_state" NOT NULL,
|
|
@@ -4,103 +4,6 @@
|
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
6
6
|
"tables": {
|
|
7
|
-
"public.runners_capacity_assignments": {
|
|
8
|
-
"name": "runners_capacity_assignments",
|
|
9
|
-
"schema": "",
|
|
10
|
-
"columns": {
|
|
11
|
-
"id": {
|
|
12
|
-
"name": "id",
|
|
13
|
-
"type": "uuid",
|
|
14
|
-
"primaryKey": true,
|
|
15
|
-
"notNull": true,
|
|
16
|
-
"default": "uuidv7()"
|
|
17
|
-
},
|
|
18
|
-
"capacity_id": {
|
|
19
|
-
"name": "capacity_id",
|
|
20
|
-
"type": "uuid",
|
|
21
|
-
"primaryKey": false,
|
|
22
|
-
"notNull": true
|
|
23
|
-
},
|
|
24
|
-
"reservation_id": {
|
|
25
|
-
"name": "reservation_id",
|
|
26
|
-
"type": "uuid",
|
|
27
|
-
"primaryKey": false,
|
|
28
|
-
"notNull": true
|
|
29
|
-
},
|
|
30
|
-
"workspace_id": {
|
|
31
|
-
"name": "workspace_id",
|
|
32
|
-
"type": "uuid",
|
|
33
|
-
"primaryKey": false,
|
|
34
|
-
"notNull": true
|
|
35
|
-
},
|
|
36
|
-
"provisioner_id": {
|
|
37
|
-
"name": "provisioner_id",
|
|
38
|
-
"type": "uuid",
|
|
39
|
-
"primaryKey": false,
|
|
40
|
-
"notNull": true
|
|
41
|
-
},
|
|
42
|
-
"created_at": {
|
|
43
|
-
"name": "created_at",
|
|
44
|
-
"type": "timestamp with time zone",
|
|
45
|
-
"primaryKey": false,
|
|
46
|
-
"notNull": true,
|
|
47
|
-
"default": "now()"
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
"indexes": {
|
|
51
|
-
"runners_capacity_assignments_capacity_unique": {
|
|
52
|
-
"name": "runners_capacity_assignments_capacity_unique",
|
|
53
|
-
"columns": [
|
|
54
|
-
{
|
|
55
|
-
"expression": "capacity_id",
|
|
56
|
-
"isExpression": false,
|
|
57
|
-
"asc": true,
|
|
58
|
-
"nulls": "last"
|
|
59
|
-
}
|
|
60
|
-
],
|
|
61
|
-
"isUnique": true,
|
|
62
|
-
"concurrently": false,
|
|
63
|
-
"method": "btree",
|
|
64
|
-
"with": {}
|
|
65
|
-
},
|
|
66
|
-
"runners_capacity_assignments_reservation_idx": {
|
|
67
|
-
"name": "runners_capacity_assignments_reservation_idx",
|
|
68
|
-
"columns": [
|
|
69
|
-
{
|
|
70
|
-
"expression": "reservation_id",
|
|
71
|
-
"isExpression": false,
|
|
72
|
-
"asc": true,
|
|
73
|
-
"nulls": "last"
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"isUnique": false,
|
|
77
|
-
"concurrently": false,
|
|
78
|
-
"method": "btree",
|
|
79
|
-
"with": {}
|
|
80
|
-
},
|
|
81
|
-
"runners_capacity_assignments_workspace_idx": {
|
|
82
|
-
"name": "runners_capacity_assignments_workspace_idx",
|
|
83
|
-
"columns": [
|
|
84
|
-
{
|
|
85
|
-
"expression": "workspace_id",
|
|
86
|
-
"isExpression": false,
|
|
87
|
-
"asc": true,
|
|
88
|
-
"nulls": "last"
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
"isUnique": false,
|
|
92
|
-
"concurrently": false,
|
|
93
|
-
"method": "btree",
|
|
94
|
-
"with": {}
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
"foreignKeys": {},
|
|
98
|
-
"compositePrimaryKeys": {},
|
|
99
|
-
"uniqueConstraints": {},
|
|
100
|
-
"policies": {},
|
|
101
|
-
"checkConstraints": {},
|
|
102
|
-
"isRLSEnabled": false
|
|
103
|
-
},
|
|
104
7
|
"public.runners_ephemeral_registration_tokens": {
|
|
105
8
|
"name": "runners_ephemeral_registration_tokens",
|
|
106
9
|
"schema": "",
|
|
@@ -852,6 +755,12 @@
|
|
|
852
755
|
"primaryKey": false,
|
|
853
756
|
"notNull": false
|
|
854
757
|
},
|
|
758
|
+
"assigned_at": {
|
|
759
|
+
"name": "assigned_at",
|
|
760
|
+
"type": "timestamp with time zone",
|
|
761
|
+
"primaryKey": false,
|
|
762
|
+
"notNull": false
|
|
763
|
+
},
|
|
855
764
|
"template_key": {
|
|
856
765
|
"name": "template_key",
|
|
857
766
|
"type": "text",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-runners",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "7.0.2",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,42 +27,21 @@
|
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
28
|
"@shipfox/api-auth-context": "6.0.0",
|
|
29
29
|
"@shipfox/api-auth-dto": "6.0.0",
|
|
30
|
-
"@shipfox/api-runners-dto": "
|
|
30
|
+
"@shipfox/api-runners-dto": "7.0.1",
|
|
31
31
|
"@shipfox/api-workflows-dto": "6.0.0",
|
|
32
32
|
"@shipfox/config": "1.2.2",
|
|
33
|
+
"@shipfox/inter-module": "0.2.0",
|
|
33
34
|
"@shipfox/node-drizzle": "0.3.2",
|
|
34
|
-
"@shipfox/node-auth-root-key": "0.2.
|
|
35
|
+
"@shipfox/node-auth-root-key": "0.2.1",
|
|
35
36
|
"@shipfox/node-fastify": "0.2.4",
|
|
37
|
+
"@shipfox/node-module": "0.4.0",
|
|
36
38
|
"@shipfox/node-opentelemetry": "0.5.2",
|
|
37
39
|
"@shipfox/node-outbox": "0.2.4",
|
|
38
|
-
"@shipfox/inter-module": "0.2.0",
|
|
39
|
-
"@shipfox/node-module": "0.4.0",
|
|
40
40
|
"@shipfox/node-postgres": "0.4.2",
|
|
41
|
-
"@shipfox/node-
|
|
41
|
+
"@shipfox/node-rate-limit": "0.3.0",
|
|
42
42
|
"@shipfox/node-temporal": "0.3.2",
|
|
43
|
-
"@shipfox/
|
|
44
|
-
"@shipfox/
|
|
45
|
-
},
|
|
46
|
-
"devDependencies": {
|
|
47
|
-
"@temporalio/activity": "1.18.1",
|
|
48
|
-
"@temporalio/client": "1.18.1",
|
|
49
|
-
"@temporalio/common": "1.18.1",
|
|
50
|
-
"@temporalio/testing": "1.18.1",
|
|
51
|
-
"@temporalio/worker": "1.18.1",
|
|
52
|
-
"@types/pg": "^8.15.5",
|
|
53
|
-
"drizzle-kit": "^0.31.10",
|
|
54
|
-
"fastify": "^5.3.3",
|
|
55
|
-
"fastify-type-provider-zod": "^6.0.0",
|
|
56
|
-
"fishery": "^2.4.0",
|
|
57
|
-
"@shipfox/api-auth": "6.0.0",
|
|
58
|
-
"@shipfox/api-auth-dto": "6.0.0",
|
|
59
|
-
"@shipfox/biome": "1.8.2",
|
|
60
|
-
"@shipfox/depcruise": "1.0.2",
|
|
61
|
-
"@shipfox/node-jwt": "0.3.0",
|
|
62
|
-
"@shipfox/swc": "1.2.6",
|
|
63
|
-
"@shipfox/typescript": "1.1.7",
|
|
64
|
-
"@shipfox/vitest": "1.2.3",
|
|
65
|
-
"@shipfox/ts-config": "1.3.8"
|
|
43
|
+
"@shipfox/node-tokens": "0.3.0",
|
|
44
|
+
"@shipfox/runner-labels": "0.1.1"
|
|
66
45
|
},
|
|
67
46
|
"scripts": {
|
|
68
47
|
"build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {REGISTRATION_TOKEN_BATCH_HARD_MAX} from '@shipfox/api-runners-dto';
|
|
2
1
|
import {bool, createConfig, num} from '@shipfox/config';
|
|
3
2
|
import {STUCK_JOB_THRESHOLD_SECONDS} from '#core/maintenance-policy.js';
|
|
4
3
|
|
|
5
4
|
const EPHEMERAL_REGISTRATION_TOKEN_TTL_HARD_MAX_SECONDS = 3600;
|
|
5
|
+
const REGISTRATION_TOKEN_BATCH_HARD_MAX = 1000;
|
|
6
6
|
const RUNNER_CONTROL_PLANE_TOKEN_TTL_HARD_MAX_SECONDS = 3600;
|
|
7
7
|
|
|
8
8
|
export const config = createConfig({
|
package/src/core/errors.ts
CHANGED
|
@@ -86,16 +86,16 @@ export class ReservationExpiredError extends Error {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export class
|
|
90
|
-
constructor(public readonly
|
|
91
|
-
super(`
|
|
92
|
-
this.name = '
|
|
89
|
+
export class RunnerInstanceNotAssignableError extends Error {
|
|
90
|
+
constructor(public readonly runnerInstanceId: string) {
|
|
91
|
+
super(`Runner instance cannot be assigned: ${runnerInstanceId}`);
|
|
92
|
+
this.name = 'RunnerInstanceNotAssignableError';
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
export class
|
|
96
|
-
constructor(public readonly
|
|
97
|
-
super(`
|
|
98
|
-
this.name = '
|
|
95
|
+
export class RunnerInstanceAlreadyAssignedError extends Error {
|
|
96
|
+
constructor(public readonly runnerInstanceId: string) {
|
|
97
|
+
super(`Runner instance is already assigned: ${runnerInstanceId}`);
|
|
98
|
+
this.name = 'RunnerInstanceAlreadyAssignedError';
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
export class ReservationAlreadyAssignedError extends Error {
|
package/src/core/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export {assignCapacityBatch} from './capacity-assignments.js';
|
|
2
|
-
export type {CapacityAssignment} from './entities/capacity-assignment.js';
|
|
3
1
|
export type {EphemeralRegistrationToken} from './entities/ephemeral-registration-token.js';
|
|
4
2
|
export type {ManualRegistrationToken} from './entities/manual-registration-token.js';
|
|
5
3
|
export type {ProvisionerCapabilitySnapshot} from './entities/provisioner-capability-snapshot.js';
|
|
@@ -13,11 +11,6 @@ export type {
|
|
|
13
11
|
RunnerInstanceState,
|
|
14
12
|
} from './entities/runner-instance.js';
|
|
15
13
|
export type {RunnerSession} from './entities/runner-session.js';
|
|
16
|
-
export {
|
|
17
|
-
type MintEphemeralRegistrationTokenParams,
|
|
18
|
-
type MintEphemeralRegistrationTokenResult,
|
|
19
|
-
mintEphemeralRegistrationToken,
|
|
20
|
-
} from './ephemeral-registration-tokens.js';
|
|
21
14
|
export {
|
|
22
15
|
EmptyRunnerLabelsError,
|
|
23
16
|
ManualRegistrationTokenNotFoundError,
|
|
@@ -43,6 +36,7 @@ export {
|
|
|
43
36
|
revokeWorkspaceProvisionerToken,
|
|
44
37
|
} from './provisioner-tokens.js';
|
|
45
38
|
export {getRunnerAssignment, issueRunnerActivationToken} from './runner-activation.js';
|
|
39
|
+
export {assignRunnerInstances} from './runner-assignments.js';
|
|
46
40
|
export {
|
|
47
41
|
attachRunnerControlProviderId,
|
|
48
42
|
createRunnerInstancesWithBootstrapTokens,
|
|
@@ -54,8 +48,7 @@ export {
|
|
|
54
48
|
} from './runner-control-sessions.js';
|
|
55
49
|
export {
|
|
56
50
|
type ActiveRunner,
|
|
57
|
-
|
|
58
|
-
createPlannedProvisionedCapacity,
|
|
51
|
+
attachRunnerInstanceProviderId,
|
|
59
52
|
listActiveRunners,
|
|
60
53
|
type ReconcileDesiredIntent,
|
|
61
54
|
type ReconciledBoundJobExecution,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {assignRunnerInstances as assignRunnerInstancesDb} from '#db/runner-assignments.js';
|
|
2
|
+
|
|
3
|
+
export function assignRunnerInstances(params: {
|
|
4
|
+
provisionerId: string;
|
|
5
|
+
reservationId: string;
|
|
6
|
+
runnerInstanceIds: string[];
|
|
7
|
+
}): Promise<string[]> {
|
|
8
|
+
return assignRunnerInstancesDb(params);
|
|
9
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {RunnerInstance, RunnerInstanceState} from '#core/entities/runner-instance.js';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
createPlannedProvisionedCapacity as createPlannedProvisionedCapacityDb,
|
|
3
|
+
attachRunnerInstanceProviderId as attachRunnerInstanceProviderIdDb,
|
|
5
4
|
isTerminalState,
|
|
6
5
|
listActiveRunnerInstances,
|
|
7
6
|
listActiveRunningJobExecutions,
|
|
@@ -29,20 +28,12 @@ export interface ReportRunnerInstancesParams {
|
|
|
29
28
|
events: RunnerInstanceReportEvent[];
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
providerKind: string | null;
|
|
35
|
-
templateKey: string | null;
|
|
36
|
-
}): Promise<{capacityId: string}> {
|
|
37
|
-
return createPlannedProvisionedCapacityDb(params);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function attachProviderRunnerId(params: {
|
|
41
|
-
capacityId: string;
|
|
31
|
+
export function attachRunnerInstanceProviderId(params: {
|
|
32
|
+
runnerInstanceId: string;
|
|
42
33
|
provisionerId: string;
|
|
43
34
|
providerRunnerId: string;
|
|
44
35
|
}): Promise<boolean> {
|
|
45
|
-
return
|
|
36
|
+
return attachRunnerInstanceProviderIdDb(params);
|
|
46
37
|
}
|
|
47
38
|
|
|
48
39
|
export interface ReportRunnerInstancesResult {
|
package/src/db/db.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
|
|
2
2
|
import {pgClient} from '@shipfox/node-postgres';
|
|
3
|
-
import {capacityAssignments} from './schema/capacity-assignments.js';
|
|
4
3
|
import {ephemeralRegistrationTokens} from './schema/ephemeral-registration-tokens.js';
|
|
5
4
|
import {manualRegistrationTokens} from './schema/manual-registration-tokens.js';
|
|
6
5
|
import {runnersOutbox} from './schema/outbox.js';
|
|
@@ -16,7 +15,6 @@ import {runnerSessions} from './schema/runner-sessions.js';
|
|
|
16
15
|
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
17
16
|
|
|
18
17
|
export const schema = {
|
|
19
|
-
capacityAssignments,
|
|
20
18
|
ephemeralRegistrationTokens,
|
|
21
19
|
pendingJobExecutions,
|
|
22
20
|
providerRunners,
|
package/src/db/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {dirname, resolve} from 'node:path';
|
|
2
2
|
import {fileURLToPath} from 'node:url';
|
|
3
3
|
|
|
4
|
-
export {assignCapacityBatch} from './capacity-assignments.js';
|
|
5
4
|
export {closeDb, db, schema} from './db.js';
|
|
6
5
|
export type {
|
|
7
6
|
CreateEphemeralRegistrationTokenParams,
|
|
@@ -67,6 +66,7 @@ export {
|
|
|
67
66
|
pollDemandAndReserve,
|
|
68
67
|
releaseReservationUnits,
|
|
69
68
|
} from './reservations.js';
|
|
69
|
+
export {assignRunnerInstances} from './runner-assignments.js';
|
|
70
70
|
export type {
|
|
71
71
|
ActiveRunnerInstanceTemplateCount,
|
|
72
72
|
ReapStaleRunnerInstancesResult,
|
|
@@ -78,8 +78,7 @@ export type {
|
|
|
78
78
|
RunnerInstanceTerminateIntentReason,
|
|
79
79
|
} from './runner-instances.js';
|
|
80
80
|
export {
|
|
81
|
-
|
|
82
|
-
createPlannedProvisionedCapacity,
|
|
81
|
+
attachRunnerInstanceProviderId,
|
|
83
82
|
isTerminalState,
|
|
84
83
|
listActiveRunnerInstanceCountsByTemplateTx,
|
|
85
84
|
listActiveRunnerInstances,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {eq} from 'drizzle-orm';
|
|
2
|
+
import {
|
|
3
|
+
ReservationExpiredError,
|
|
4
|
+
RunnerInstanceAlreadyAssignedError,
|
|
5
|
+
RunnerInstanceNotAssignableError,
|
|
6
|
+
} from '#core/errors.js';
|
|
7
|
+
import {db} from '#db/db.js';
|
|
8
|
+
import {assignRunnerInstances} from '#db/runner-assignments.js';
|
|
9
|
+
import {reservations} from '#db/schema/reservations.js';
|
|
10
|
+
import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
|
|
11
|
+
import {providerRunners} from '#db/schema/runner-instances.js';
|
|
12
|
+
|
|
13
|
+
describe('assignRunnerInstances', () => {
|
|
14
|
+
let workspaceId: string;
|
|
15
|
+
let provisionerId: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
workspaceId = crypto.randomUUID();
|
|
19
|
+
provisionerId = crypto.randomUUID();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('writes the reservation and workspace after enrollment', async () => {
|
|
23
|
+
const reservation = await createReservation();
|
|
24
|
+
const runner = await createEnrolledRunner();
|
|
25
|
+
|
|
26
|
+
const assigned = await assignRunnerInstances({
|
|
27
|
+
provisionerId,
|
|
28
|
+
reservationId: reservation.id,
|
|
29
|
+
runnerInstanceIds: [runner.id],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(assigned).toEqual([runner.id]);
|
|
33
|
+
const [stored] = await db()
|
|
34
|
+
.select()
|
|
35
|
+
.from(providerRunners)
|
|
36
|
+
.where(eq(providerRunners.id, runner.id));
|
|
37
|
+
expect(stored).toMatchObject({
|
|
38
|
+
workspaceId,
|
|
39
|
+
reservationId: reservation.id,
|
|
40
|
+
assignedAt: expect.any(Date),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is idempotent for concurrent retries of the same assignment', async () => {
|
|
45
|
+
const reservation = await createReservation();
|
|
46
|
+
const runner = await createEnrolledRunner();
|
|
47
|
+
|
|
48
|
+
const results = await Promise.all([
|
|
49
|
+
assignRunnerInstances({
|
|
50
|
+
provisionerId,
|
|
51
|
+
reservationId: reservation.id,
|
|
52
|
+
runnerInstanceIds: [runner.id],
|
|
53
|
+
}),
|
|
54
|
+
assignRunnerInstances({
|
|
55
|
+
provisionerId,
|
|
56
|
+
reservationId: reservation.id,
|
|
57
|
+
runnerInstanceIds: [runner.id],
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
expect(results).toEqual([[runner.id], [runner.id]]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects expired reservations', async () => {
|
|
65
|
+
const reservation = await createReservation({expiresAt: new Date(Date.now() - 1_000)});
|
|
66
|
+
const runner = await createEnrolledRunner();
|
|
67
|
+
|
|
68
|
+
const assignment = assignRunnerInstances({
|
|
69
|
+
provisionerId,
|
|
70
|
+
reservationId: reservation.id,
|
|
71
|
+
runnerInstanceIds: [runner.id],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await expect(assignment).rejects.toThrow(ReservationExpiredError);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects unenrolled or incompatible runners', async () => {
|
|
78
|
+
const reservation = await createReservation({requiredLabels: ['linux', 'gpu']});
|
|
79
|
+
const runner = await createEnrolledRunner({labels: ['linux']});
|
|
80
|
+
|
|
81
|
+
const assignment = assignRunnerInstances({
|
|
82
|
+
provisionerId,
|
|
83
|
+
reservationId: reservation.id,
|
|
84
|
+
runnerInstanceIds: [runner.id],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects a runner assigned to a different reservation', async () => {
|
|
91
|
+
const firstReservation = await createReservation();
|
|
92
|
+
const secondReservation = await createReservation();
|
|
93
|
+
const runner = await createEnrolledRunner();
|
|
94
|
+
await assignRunnerInstances({
|
|
95
|
+
provisionerId,
|
|
96
|
+
reservationId: firstReservation.id,
|
|
97
|
+
runnerInstanceIds: [runner.id],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const assignment = assignRunnerInstances({
|
|
101
|
+
provisionerId,
|
|
102
|
+
reservationId: secondReservation.id,
|
|
103
|
+
runnerInstanceIds: [runner.id],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await expect(assignment).rejects.toThrow(RunnerInstanceAlreadyAssignedError);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('rejects assignments that exceed reservation capacity', async () => {
|
|
110
|
+
const reservation = await createReservation();
|
|
111
|
+
const firstRunner = await createEnrolledRunner();
|
|
112
|
+
const secondRunner = await createEnrolledRunner();
|
|
113
|
+
|
|
114
|
+
const assignment = assignRunnerInstances({
|
|
115
|
+
provisionerId,
|
|
116
|
+
reservationId: reservation.id,
|
|
117
|
+
runnerInstanceIds: [firstRunner.id, secondRunner.id],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
async function createReservation(
|
|
124
|
+
overrides: Partial<{expiresAt: Date; requiredLabels: string[]}> = {},
|
|
125
|
+
) {
|
|
126
|
+
const [reservation] = await db()
|
|
127
|
+
.insert(reservations)
|
|
128
|
+
.values({
|
|
129
|
+
workspaceId,
|
|
130
|
+
provisionerId,
|
|
131
|
+
requiredLabels: overrides.requiredLabels ?? ['linux'],
|
|
132
|
+
count: 1,
|
|
133
|
+
expiresAt: overrides.expiresAt ?? new Date(Date.now() + 60_000),
|
|
134
|
+
})
|
|
135
|
+
.returning();
|
|
136
|
+
if (!reservation) throw new Error('Reservation insert returned no row');
|
|
137
|
+
return reservation;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function createEnrolledRunner(overrides: Partial<{labels: string[]}> = {}) {
|
|
141
|
+
const [runner] = await db()
|
|
142
|
+
.insert(providerRunners)
|
|
143
|
+
.values({
|
|
144
|
+
provisionerId,
|
|
145
|
+
providerRunnerId: crypto.randomUUID(),
|
|
146
|
+
labels: overrides.labels ?? ['linux'],
|
|
147
|
+
state: 'running',
|
|
148
|
+
reportedAt: new Date(),
|
|
149
|
+
})
|
|
150
|
+
.returning();
|
|
151
|
+
if (!runner) throw new Error('Runner instance insert returned no row');
|
|
152
|
+
await db()
|
|
153
|
+
.insert(runnerControlSessions)
|
|
154
|
+
.values({
|
|
155
|
+
runnerInstanceId: runner.id,
|
|
156
|
+
provisionerId,
|
|
157
|
+
hashedToken: crypto.randomUUID(),
|
|
158
|
+
prefix: 'test',
|
|
159
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
160
|
+
});
|
|
161
|
+
return runner;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {and, eq, inArray, isNull, sql} from 'drizzle-orm';
|
|
2
|
+
import {
|
|
3
|
+
ReservationExpiredError,
|
|
4
|
+
ReservationNotFoundError,
|
|
5
|
+
RunnerInstanceAlreadyAssignedError,
|
|
6
|
+
RunnerInstanceNotAssignableError,
|
|
7
|
+
} from '#core/errors.js';
|
|
8
|
+
import {db} from './db.js';
|
|
9
|
+
import {reservations} from './schema/reservations.js';
|
|
10
|
+
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
11
|
+
import {providerRunners} from './schema/runner-instances.js';
|
|
12
|
+
|
|
13
|
+
/** Atomically consumes reservation units by writing the immutable assignment on each runner. */
|
|
14
|
+
export async function assignRunnerInstances(params: {
|
|
15
|
+
provisionerId: string;
|
|
16
|
+
reservationId: string;
|
|
17
|
+
runnerInstanceIds: string[];
|
|
18
|
+
}): Promise<string[]> {
|
|
19
|
+
const runnerInstanceIds = [...params.runnerInstanceIds].sort();
|
|
20
|
+
return await db().transaction(async (tx) => {
|
|
21
|
+
await tx.execute(
|
|
22
|
+
sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`,
|
|
23
|
+
);
|
|
24
|
+
const [reservation] = await tx
|
|
25
|
+
.select()
|
|
26
|
+
.from(reservations)
|
|
27
|
+
.where(
|
|
28
|
+
and(
|
|
29
|
+
eq(reservations.id, params.reservationId),
|
|
30
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
.limit(1)
|
|
34
|
+
.for('update');
|
|
35
|
+
if (!reservation) throw new ReservationNotFoundError(params.reservationId);
|
|
36
|
+
if (reservation.expiresAt <= new Date())
|
|
37
|
+
throw new ReservationExpiredError(params.reservationId);
|
|
38
|
+
|
|
39
|
+
const runnerRows = await tx
|
|
40
|
+
.select({
|
|
41
|
+
id: providerRunners.id,
|
|
42
|
+
reservationId: providerRunners.reservationId,
|
|
43
|
+
workspaceId: providerRunners.workspaceId,
|
|
44
|
+
providerRunnerId: providerRunners.providerRunnerId,
|
|
45
|
+
labels: providerRunners.labels,
|
|
46
|
+
state: providerRunners.state,
|
|
47
|
+
})
|
|
48
|
+
.from(providerRunners)
|
|
49
|
+
.where(
|
|
50
|
+
and(
|
|
51
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
52
|
+
inArray(providerRunners.id, runnerInstanceIds),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
.for('update');
|
|
56
|
+
const activeControlSessions = await tx
|
|
57
|
+
.select({runnerInstanceId: runnerControlSessions.runnerInstanceId})
|
|
58
|
+
.from(runnerControlSessions)
|
|
59
|
+
.where(
|
|
60
|
+
and(
|
|
61
|
+
inArray(
|
|
62
|
+
runnerControlSessions.runnerInstanceId,
|
|
63
|
+
runnerRows.map((runner) => runner.id),
|
|
64
|
+
),
|
|
65
|
+
isNull(runnerControlSessions.closedAt),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
const runnerInstanceIdsWithControlSession = new Set(
|
|
69
|
+
activeControlSessions.map((session) => session.runnerInstanceId),
|
|
70
|
+
);
|
|
71
|
+
const runners = runnerRows.map((runner) => ({
|
|
72
|
+
...runner,
|
|
73
|
+
controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,
|
|
74
|
+
}));
|
|
75
|
+
if (runners.length !== runnerInstanceIds.length)
|
|
76
|
+
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
|
|
77
|
+
|
|
78
|
+
const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
|
|
79
|
+
if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))
|
|
80
|
+
throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
|
|
81
|
+
const newRunners = runners.filter((runner) => runner.reservationId === null);
|
|
82
|
+
const assignedCount = await tx
|
|
83
|
+
.select({count: sql<number>`count(*)::int`})
|
|
84
|
+
.from(providerRunners)
|
|
85
|
+
.where(eq(providerRunners.reservationId, reservation.id));
|
|
86
|
+
if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)
|
|
87
|
+
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
|
|
88
|
+
for (const runner of newRunners) {
|
|
89
|
+
if (
|
|
90
|
+
runner.state !== 'running' ||
|
|
91
|
+
!runner.providerRunnerId ||
|
|
92
|
+
!runner.controlSessionId ||
|
|
93
|
+
!reservation.requiredLabels.every((label) => runner.labels.includes(label))
|
|
94
|
+
)
|
|
95
|
+
throw new RunnerInstanceNotAssignableError(runner.id);
|
|
96
|
+
}
|
|
97
|
+
if (newRunners.length > 0) {
|
|
98
|
+
await tx
|
|
99
|
+
.update(providerRunners)
|
|
100
|
+
.set({
|
|
101
|
+
workspaceId: reservation.workspaceId,
|
|
102
|
+
reservationId: reservation.id,
|
|
103
|
+
assignedAt: sql`now()`,
|
|
104
|
+
updatedAt: sql`now()`,
|
|
105
|
+
})
|
|
106
|
+
.where(
|
|
107
|
+
inArray(
|
|
108
|
+
providerRunners.id,
|
|
109
|
+
newRunners.map((runner) => runner.id),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return params.runnerInstanceIds;
|
|
114
|
+
});
|
|
115
|
+
}
|