@treeseed/core 0.6.38 → 0.6.40
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/README.md +3 -3
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +2 -0
- package/dist/agents/spec-normalizer.js +71 -1
- package/dist/api/agent-routes.js +1 -11
- package/dist/scripts/build-dist.js +15 -0
- package/dist/services/common.d.ts +13 -0
- package/dist/services/common.js +33 -5
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +2 -0
- package/dist/services/manager.d.ts +24 -4
- package/dist/services/manager.js +184 -18
- package/dist/services/workday-manager.d.ts +279 -0
- package/dist/services/workday-manager.js +163 -0
- package/dist/services/workday-report.d.ts +23 -3
- package/dist/services/workday-start.d.ts +23 -3
- package/dist/services/worker-pool-scaler.d.ts +3 -3
- package/dist/services/worker-pool-scaler.js +69 -51
- package/dist/services/worker.d.ts +6 -0
- package/dist/services/worker.js +247 -13
- package/package.json +12 -6
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createServiceSdk } from "./common.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveManagerServiceConfig,
|
|
6
|
+
runManagerAction,
|
|
7
|
+
runManagerCycle
|
|
8
|
+
} from "./manager.js";
|
|
9
|
+
function readDate(value) {
|
|
10
|
+
if (typeof value !== "string") return null;
|
|
11
|
+
const parsed = new Date(value);
|
|
12
|
+
return Number.isFinite(parsed.valueOf()) ? parsed : null;
|
|
13
|
+
}
|
|
14
|
+
function workdayCloseDeadline(workDay, durationMinutes, now) {
|
|
15
|
+
const startedAt = readDate(workDay?.startedAt) ?? readDate(workDay?.started_at) ?? now;
|
|
16
|
+
return new Date(startedAt.valueOf() + durationMinutes * 60 * 1e3);
|
|
17
|
+
}
|
|
18
|
+
async function sleep(ms) {
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
async function acquireLease(sdk, config, workDayId) {
|
|
22
|
+
if (typeof sdk.claimWorkdayManagerLease !== "function") {
|
|
23
|
+
return { payload: { id: `local:${config.managerId}`, managerId: config.managerId } };
|
|
24
|
+
}
|
|
25
|
+
return sdk.claimWorkdayManagerLease({
|
|
26
|
+
projectId: config.projectId,
|
|
27
|
+
environment: config.environment,
|
|
28
|
+
workDayId,
|
|
29
|
+
managerId: config.managerId,
|
|
30
|
+
ttlSeconds: Math.max(60, Math.ceil(config.pollIntervalMs / 1e3) * 4),
|
|
31
|
+
staleAfterSeconds: Math.max(120, Math.ceil(config.pollIntervalMs / 1e3) * 8),
|
|
32
|
+
metadata: {
|
|
33
|
+
service: "workdayManager"
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function heartbeatLease(sdk, config, leaseId, workDayId) {
|
|
38
|
+
if (typeof sdk.claimWorkdayManagerLease !== "function") return;
|
|
39
|
+
await sdk.claimWorkdayManagerLease({
|
|
40
|
+
id: leaseId,
|
|
41
|
+
projectId: config.projectId,
|
|
42
|
+
environment: config.environment,
|
|
43
|
+
workDayId,
|
|
44
|
+
managerId: config.managerId,
|
|
45
|
+
ttlSeconds: Math.max(60, Math.ceil(config.pollIntervalMs / 1e3) * 4),
|
|
46
|
+
metadata: {
|
|
47
|
+
service: "workdayManager",
|
|
48
|
+
heartbeat: true
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function releaseLease(sdk, managerId, leaseId) {
|
|
53
|
+
if (!leaseId || typeof sdk.releaseWorkdayManagerLease !== "function") return;
|
|
54
|
+
await sdk.releaseWorkdayManagerLease({ id: leaseId, managerId }).catch(() => null);
|
|
55
|
+
}
|
|
56
|
+
async function recordRunnerCloseDecision(sdk, config, workDayId, action, reason) {
|
|
57
|
+
if (typeof sdk.recordRunnerScaleDecision !== "function") return;
|
|
58
|
+
const runners = typeof sdk.listWorkerRunners === "function" ? (await sdk.listWorkerRunners(config.projectId, config.environment).catch(() => ({ payload: [] }))).payload ?? [] : [];
|
|
59
|
+
if (runners.length === 0) {
|
|
60
|
+
await sdk.recordRunnerScaleDecision({
|
|
61
|
+
projectId: config.projectId,
|
|
62
|
+
environment: config.environment,
|
|
63
|
+
workDayId,
|
|
64
|
+
action: "noop",
|
|
65
|
+
reason: `${reason}:no_runners`,
|
|
66
|
+
metadata: { service: "workdayManager" }
|
|
67
|
+
}).catch(() => null);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const runner of runners) {
|
|
71
|
+
await sdk.recordRunnerScaleDecision({
|
|
72
|
+
projectId: config.projectId,
|
|
73
|
+
environment: config.environment,
|
|
74
|
+
workDayId,
|
|
75
|
+
runnerId: typeof runner.runnerId === "string" ? runner.runnerId : null,
|
|
76
|
+
runnerServiceName: typeof runner.runnerServiceName === "string" ? runner.runnerServiceName : null,
|
|
77
|
+
action,
|
|
78
|
+
reason,
|
|
79
|
+
metadata: { service: "workdayManager" }
|
|
80
|
+
}).catch(() => null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function runScheduledWorkdayManager(options = {}) {
|
|
84
|
+
const sdk = options.sdk ?? createServiceSdk();
|
|
85
|
+
const config = options.config ?? {
|
|
86
|
+
...resolveManagerServiceConfig(),
|
|
87
|
+
mode: "reconcile"
|
|
88
|
+
};
|
|
89
|
+
let now = options.now ?? /* @__PURE__ */ new Date();
|
|
90
|
+
const policyEnvelope = await sdk.getWorkPolicy(config.projectId, config.environment);
|
|
91
|
+
const policy = policyEnvelope.payload ?? await sdk.upsertWorkPolicy({
|
|
92
|
+
projectId: config.projectId,
|
|
93
|
+
environment: config.environment,
|
|
94
|
+
schedule: config.defaultSchedule,
|
|
95
|
+
enabled: true,
|
|
96
|
+
startCron: process.env.TREESEED_WORKDAY_START_CRON?.trim() || "0 9 * * 1-5",
|
|
97
|
+
durationMinutes: Number(process.env.TREESEED_WORKDAY_DURATION_MINUTES ?? 480),
|
|
98
|
+
maxRunners: config.autoscale.maxWorkers,
|
|
99
|
+
maxWorkersPerRunner: Number(process.env.TREESEED_RUNNER_MAX_LOCAL_WORKERS ?? 4),
|
|
100
|
+
dailyCreditBudget: config.dailyTaskCreditBudget,
|
|
101
|
+
closeoutGraceMinutes: Number(process.env.TREESEED_WORKDAY_CLOSEOUT_GRACE_MINUTES ?? 15),
|
|
102
|
+
dailyTaskCreditBudget: config.dailyTaskCreditBudget,
|
|
103
|
+
maxQueuedTasks: config.maxQueuedTasks,
|
|
104
|
+
maxQueuedCredits: config.maxQueuedCredits,
|
|
105
|
+
autoscale: config.autoscale,
|
|
106
|
+
creditWeights: config.creditWeights,
|
|
107
|
+
metadata: { managedBy: "workdayManager" }
|
|
108
|
+
}).then((created) => created.payload);
|
|
109
|
+
if (!policy?.enabled) {
|
|
110
|
+
return { ok: true, skipped: true, reason: "workday_policy_disabled" };
|
|
111
|
+
}
|
|
112
|
+
const requests = typeof sdk.listWorkdayRequests === "function" ? (await sdk.listWorkdayRequests(config.projectId, config.environment, "pending").catch(() => ({ payload: [] }))).payload ?? [] : [];
|
|
113
|
+
const oneOffRunRequested = requests.some((entry) => entry.type === "one_off_run");
|
|
114
|
+
const initial = await runManagerCycle({ sdk, config, now });
|
|
115
|
+
const workDay = initial.workDay;
|
|
116
|
+
if (!workDay && !oneOffRunRequested) {
|
|
117
|
+
return { ok: true, skipped: true, reason: "no_workday_started", initial };
|
|
118
|
+
}
|
|
119
|
+
const lease = await acquireLease(sdk, config, workDay ? String(workDay.id ?? "") : null);
|
|
120
|
+
if (!lease.payload) {
|
|
121
|
+
return { ok: true, skipped: true, reason: "healthy_manager_lease_exists" };
|
|
122
|
+
}
|
|
123
|
+
const leaseId = String(lease.payload.id ?? "");
|
|
124
|
+
let latest = initial;
|
|
125
|
+
try {
|
|
126
|
+
let activeWorkDay = workDay;
|
|
127
|
+
let closeDeadline = workdayCloseDeadline(activeWorkDay, Number(policy.durationMinutes ?? 480), now);
|
|
128
|
+
while (Date.now() < closeDeadline.valueOf()) {
|
|
129
|
+
await heartbeatLease(sdk, config, leaseId, activeWorkDay ? String(activeWorkDay.id ?? "") : null);
|
|
130
|
+
await sleep(config.pollIntervalMs);
|
|
131
|
+
now = /* @__PURE__ */ new Date();
|
|
132
|
+
latest = await runManagerCycle({ sdk, config, now });
|
|
133
|
+
activeWorkDay = latest.workDay;
|
|
134
|
+
closeDeadline = workdayCloseDeadline(activeWorkDay, Number(policy.durationMinutes ?? 480), now);
|
|
135
|
+
}
|
|
136
|
+
const workDayId = activeWorkDay ? String(activeWorkDay.id ?? "") : null;
|
|
137
|
+
await recordRunnerCloseDecision(sdk, config, workDayId, "drain", "workday_closeout");
|
|
138
|
+
const graceMs = Number(policy.closeoutGraceMinutes ?? 15) * 60 * 1e3;
|
|
139
|
+
const graceEnd = Date.now() + graceMs;
|
|
140
|
+
while (Date.now() < graceEnd) {
|
|
141
|
+
const result = await runManagerCycle({ sdk, config, now: /* @__PURE__ */ new Date() });
|
|
142
|
+
latest = result;
|
|
143
|
+
if (Number(result.queuedCount ?? 0) === 0 && Number(result.activeLeases ?? 0) === 0) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
await sleep(config.pollIntervalMs);
|
|
147
|
+
}
|
|
148
|
+
const closed = await runManagerAction({ sdk, config, mode: "close-workday" });
|
|
149
|
+
await recordRunnerCloseDecision(sdk, config, workDayId, "sleep", "workday_closed");
|
|
150
|
+
return { ok: true, initial, latest, closed };
|
|
151
|
+
} finally {
|
|
152
|
+
await releaseLease(sdk, config.managerId, leaseId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
156
|
+
const entryFile = process.argv[1] ?? "";
|
|
157
|
+
if (entryFile === currentFile) {
|
|
158
|
+
process.stdout.write(`${JSON.stringify(await runScheduledWorkdayManager(), null, 2)}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
runScheduledWorkdayManager
|
|
163
|
+
};
|
|
@@ -4,7 +4,7 @@ export declare function runWorkdayReport(): Promise<{
|
|
|
4
4
|
mode: "reconcile";
|
|
5
5
|
managerId: string;
|
|
6
6
|
projectId: string;
|
|
7
|
-
environment: "local" | "
|
|
7
|
+
environment: "local" | "prod" | "staging";
|
|
8
8
|
insideWorkWindow: boolean;
|
|
9
9
|
workPolicy: import("@treeseed/sdk").WorkdayPolicy;
|
|
10
10
|
workDay: Record<string, unknown>;
|
|
@@ -55,8 +55,18 @@ export declare function runWorkdayReport(): Promise<{
|
|
|
55
55
|
reportVersion: string;
|
|
56
56
|
title: string;
|
|
57
57
|
};
|
|
58
|
+
capacity: {
|
|
59
|
+
providerSplit: any;
|
|
60
|
+
grantedDailyCredits: number;
|
|
61
|
+
reservedCredits: number;
|
|
62
|
+
consumedCredits: number;
|
|
63
|
+
remainingDailyCredits: number | null;
|
|
64
|
+
providerCount: number;
|
|
65
|
+
laneCount: number;
|
|
66
|
+
grantCount: number;
|
|
67
|
+
};
|
|
58
68
|
projectId: string;
|
|
59
|
-
environment: "local" | "
|
|
69
|
+
environment: "local" | "prod" | "staging";
|
|
60
70
|
workDayId: string;
|
|
61
71
|
state: string;
|
|
62
72
|
totalTasks: number;
|
|
@@ -122,8 +132,18 @@ export declare function runWorkdayReport(): Promise<{
|
|
|
122
132
|
reportVersion: string;
|
|
123
133
|
title: string;
|
|
124
134
|
};
|
|
135
|
+
capacity: {
|
|
136
|
+
providerSplit: any;
|
|
137
|
+
grantedDailyCredits: number;
|
|
138
|
+
reservedCredits: number;
|
|
139
|
+
consumedCredits: number;
|
|
140
|
+
remainingDailyCredits: number | null;
|
|
141
|
+
providerCount: number;
|
|
142
|
+
laneCount: number;
|
|
143
|
+
grantCount: number;
|
|
144
|
+
};
|
|
125
145
|
projectId: string;
|
|
126
|
-
environment: "local" | "
|
|
146
|
+
environment: "local" | "prod" | "staging";
|
|
127
147
|
workDayId: string;
|
|
128
148
|
state: string;
|
|
129
149
|
totalTasks: number;
|
|
@@ -4,7 +4,7 @@ export declare function runWorkdayStart(): Promise<{
|
|
|
4
4
|
mode: "reconcile";
|
|
5
5
|
managerId: string;
|
|
6
6
|
projectId: string;
|
|
7
|
-
environment: "local" | "
|
|
7
|
+
environment: "local" | "prod" | "staging";
|
|
8
8
|
insideWorkWindow: boolean;
|
|
9
9
|
workPolicy: import("@treeseed/sdk").WorkdayPolicy;
|
|
10
10
|
workDay: Record<string, unknown>;
|
|
@@ -55,8 +55,18 @@ export declare function runWorkdayStart(): Promise<{
|
|
|
55
55
|
reportVersion: string;
|
|
56
56
|
title: string;
|
|
57
57
|
};
|
|
58
|
+
capacity: {
|
|
59
|
+
providerSplit: any;
|
|
60
|
+
grantedDailyCredits: number;
|
|
61
|
+
reservedCredits: number;
|
|
62
|
+
consumedCredits: number;
|
|
63
|
+
remainingDailyCredits: number | null;
|
|
64
|
+
providerCount: number;
|
|
65
|
+
laneCount: number;
|
|
66
|
+
grantCount: number;
|
|
67
|
+
};
|
|
58
68
|
projectId: string;
|
|
59
|
-
environment: "local" | "
|
|
69
|
+
environment: "local" | "prod" | "staging";
|
|
60
70
|
workDayId: string;
|
|
61
71
|
state: string;
|
|
62
72
|
totalTasks: number;
|
|
@@ -122,8 +132,18 @@ export declare function runWorkdayStart(): Promise<{
|
|
|
122
132
|
reportVersion: string;
|
|
123
133
|
title: string;
|
|
124
134
|
};
|
|
135
|
+
capacity: {
|
|
136
|
+
providerSplit: any;
|
|
137
|
+
grantedDailyCredits: number;
|
|
138
|
+
reservedCredits: number;
|
|
139
|
+
consumedCredits: number;
|
|
140
|
+
remainingDailyCredits: number | null;
|
|
141
|
+
providerCount: number;
|
|
142
|
+
laneCount: number;
|
|
143
|
+
grantCount: number;
|
|
144
|
+
};
|
|
125
145
|
projectId: string;
|
|
126
|
-
environment: "local" | "
|
|
146
|
+
environment: "local" | "prod" | "staging";
|
|
127
147
|
workDayId: string;
|
|
128
148
|
state: string;
|
|
129
149
|
totalTasks: number;
|
|
@@ -7,7 +7,6 @@ export interface RailwayWorkerPoolScalerOptions {
|
|
|
7
7
|
environmentId?: string | null;
|
|
8
8
|
projectId?: string | null;
|
|
9
9
|
fetchImpl?: typeof fetch;
|
|
10
|
-
mutation?: string | null;
|
|
11
10
|
}
|
|
12
11
|
export declare class NoopWorkerPoolScaler implements WorkerPoolScaler {
|
|
13
12
|
scale(decision: ScaleDecision): Promise<WorkerPoolScaleResult>;
|
|
@@ -19,9 +18,10 @@ export declare class RailwayWorkerPoolScaler implements WorkerPoolScaler {
|
|
|
19
18
|
private readonly environmentId;
|
|
20
19
|
private readonly projectId;
|
|
21
20
|
private readonly fetchImpl;
|
|
22
|
-
private readonly mutation;
|
|
23
21
|
constructor(options?: RailwayWorkerPoolScalerOptions);
|
|
24
|
-
private
|
|
22
|
+
private railwayMutation;
|
|
23
|
+
private wakeRunner;
|
|
24
|
+
private sleepRunner;
|
|
25
25
|
scale(decision: ScaleDecision): Promise<WorkerPoolScaleResult>;
|
|
26
26
|
}
|
|
27
27
|
export declare function createWorkerPoolScaler(kind?: WorkerPoolScalerKind | null, options?: RailwayWorkerPoolScalerOptions): WorkerPoolScaler;
|
|
@@ -2,18 +2,6 @@ function envValue(name) {
|
|
|
2
2
|
const value = process.env[name]?.trim();
|
|
3
3
|
return value ? value : "";
|
|
4
4
|
}
|
|
5
|
-
const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
6
|
-
const DEFAULT_SCALE_MUTATION = `
|
|
7
|
-
mutation TreeseedScaleService($serviceId: String!, $environmentId: String!, $replicas: Int!) {
|
|
8
|
-
serviceInstanceUpdate(
|
|
9
|
-
serviceId: $serviceId
|
|
10
|
-
environmentId: $environmentId
|
|
11
|
-
input: { numReplicas: $replicas }
|
|
12
|
-
) {
|
|
13
|
-
id
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
`.trim();
|
|
17
5
|
class NoopWorkerPoolScaler {
|
|
18
6
|
async scale(decision) {
|
|
19
7
|
return {
|
|
@@ -33,69 +21,99 @@ class RailwayWorkerPoolScaler {
|
|
|
33
21
|
environmentId;
|
|
34
22
|
projectId;
|
|
35
23
|
fetchImpl;
|
|
36
|
-
mutation;
|
|
37
24
|
constructor(options = {}) {
|
|
38
25
|
this.apiToken = options.apiToken?.trim() || envValue("RAILWAY_API_TOKEN") || null;
|
|
39
|
-
this.apiUrl = options.apiUrl?.trim() || envValue("TREESEED_RAILWAY_API_URL") ||
|
|
26
|
+
this.apiUrl = options.apiUrl?.trim() || envValue("TREESEED_RAILWAY_API_URL") || "https://backboard.railway.com/graphql/v2";
|
|
40
27
|
this.serviceId = options.serviceId?.trim() || envValue("TREESEED_RAILWAY_WORKER_SERVICE_ID") || envValue("TREESEED_WORKER_SERVICE_ID") || null;
|
|
41
28
|
this.environmentId = options.environmentId?.trim() || envValue("TREESEED_RAILWAY_ENVIRONMENT_ID") || null;
|
|
42
29
|
this.projectId = options.projectId?.trim() || envValue("TREESEED_RAILWAY_PROJECT_ID") || null;
|
|
43
30
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
44
|
-
this.mutation = options.mutation?.trim() || envValue("TREESEED_RAILWAY_SCALE_MUTATION") || DEFAULT_SCALE_MUTATION;
|
|
45
31
|
}
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
async railwayMutation(query, variables) {
|
|
33
|
+
if (!this.apiToken) {
|
|
34
|
+
throw new Error("Configure RAILWAY_API_TOKEN before waking Railway worker runners.");
|
|
35
|
+
}
|
|
36
|
+
const response = await this.fetchImpl(this.apiUrl, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${this.apiToken}`,
|
|
40
|
+
"content-type": "application/json"
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ query, variables })
|
|
43
|
+
});
|
|
44
|
+
const payload = await response.json().catch(() => ({}));
|
|
45
|
+
if (!response.ok || Array.isArray(payload.errors) && payload.errors.length > 0) {
|
|
46
|
+
throw new Error(payload.errors?.[0]?.message ?? `Railway runner request failed with ${response.status}.`);
|
|
47
|
+
}
|
|
48
|
+
return payload.data;
|
|
49
|
+
}
|
|
50
|
+
async wakeRunner() {
|
|
51
|
+
if (!this.serviceId || !this.environmentId) {
|
|
52
|
+
throw new Error("Railway runner wake requires serviceId and environmentId.");
|
|
53
|
+
}
|
|
54
|
+
const mutation = envValue("TREESEED_RAILWAY_RUNNER_WAKE_MUTATION") || `
|
|
55
|
+
mutation TreeseedRailwayRunnerWake($serviceId: String!, $environmentId: String!) {
|
|
56
|
+
serviceInstanceRedeploy(serviceId: $serviceId, environmentId: $environmentId)
|
|
57
|
+
}
|
|
58
|
+
`.trim();
|
|
59
|
+
return await this.railwayMutation(mutation, {
|
|
60
|
+
serviceId: this.serviceId,
|
|
61
|
+
environmentId: this.environmentId,
|
|
62
|
+
projectId: this.projectId
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async sleepRunner() {
|
|
66
|
+
if (!this.serviceId || !this.environmentId) {
|
|
67
|
+
throw new Error("Railway runner sleep requires serviceId and environmentId.");
|
|
68
|
+
}
|
|
69
|
+
const mutation = envValue("TREESEED_RAILWAY_RUNNER_SLEEP_MUTATION") || `
|
|
70
|
+
mutation TreeseedRailwayRunnerSleep($serviceId: String!, $environmentId: String!) {
|
|
71
|
+
deploymentRemove(serviceId: $serviceId, environmentId: $environmentId)
|
|
72
|
+
}
|
|
73
|
+
`.trim();
|
|
74
|
+
return await this.railwayMutation(mutation, {
|
|
75
|
+
serviceId: this.serviceId,
|
|
76
|
+
environmentId: this.environmentId,
|
|
77
|
+
projectId: this.projectId
|
|
78
|
+
});
|
|
48
79
|
}
|
|
49
80
|
async scale(decision) {
|
|
50
|
-
|
|
81
|
+
const desiredWorkers = Math.max(0, Number(decision.desiredWorkers ?? 0));
|
|
82
|
+
const action = desiredWorkers > 0 ? "wake" : "sleep";
|
|
83
|
+
try {
|
|
84
|
+
const result = action === "wake" ? await this.wakeRunner() : await this.sleepRunner();
|
|
51
85
|
return {
|
|
52
|
-
applied:
|
|
86
|
+
applied: true,
|
|
53
87
|
provider: "railway",
|
|
54
|
-
desiredWorkers
|
|
88
|
+
desiredWorkers,
|
|
55
89
|
metadata: {
|
|
56
|
-
|
|
90
|
+
action,
|
|
57
91
|
projectId: this.projectId,
|
|
58
92
|
serviceId: this.serviceId,
|
|
59
|
-
environmentId: this.environmentId
|
|
93
|
+
environmentId: this.environmentId,
|
|
94
|
+
result
|
|
60
95
|
}
|
|
61
96
|
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
variables: {
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
applied: false,
|
|
100
|
+
provider: "railway",
|
|
101
|
+
desiredWorkers,
|
|
102
|
+
metadata: {
|
|
103
|
+
action,
|
|
104
|
+
reason: "named_runner_action_failed",
|
|
105
|
+
projectId: this.projectId,
|
|
72
106
|
serviceId: this.serviceId,
|
|
73
107
|
environmentId: this.environmentId,
|
|
74
|
-
|
|
108
|
+
error: error instanceof Error ? error.message : String(error)
|
|
75
109
|
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
const payload = await response.json().catch(() => ({}));
|
|
79
|
-
if (!response.ok || Array.isArray(payload.errors) && payload.errors.length > 0) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
payload.errors?.[0]?.message ?? `Railway worker scale request failed with ${response.status}.`
|
|
82
|
-
);
|
|
110
|
+
};
|
|
83
111
|
}
|
|
84
|
-
return {
|
|
85
|
-
applied: true,
|
|
86
|
-
provider: "railway",
|
|
87
|
-
desiredWorkers: decision.desiredWorkers,
|
|
88
|
-
metadata: {
|
|
89
|
-
projectId: this.projectId,
|
|
90
|
-
serviceId: this.serviceId,
|
|
91
|
-
environmentId: this.environmentId
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
112
|
}
|
|
95
113
|
}
|
|
96
114
|
function createWorkerPoolScaler(kind, options = {}) {
|
|
97
115
|
const configuredKind = envValue("TREESEED_WORKER_POOL_SCALER") || null;
|
|
98
|
-
const inferredKind = envValue("RAILWAY_API_TOKEN") && (envValue("TREESEED_RAILWAY_WORKER_SERVICE_ID") || envValue("TREESEED_WORKER_SERVICE_ID")) ? "railway" : "noop";
|
|
116
|
+
const inferredKind = envValue("RAILWAY_API_TOKEN") && (envValue("TREESEED_RAILWAY_WORKER_SERVICE_ID") || envValue("TREESEED_WORKER_SERVICE_ID")) && envValue("TREESEED_RAILWAY_ENVIRONMENT_ID") ? "railway" : "noop";
|
|
99
117
|
const resolvedKind = kind ?? configuredKind ?? inferredKind;
|
|
100
118
|
if (resolvedKind === "railway") {
|
|
101
119
|
return new RailwayWorkerPoolScaler(options);
|
|
@@ -10,4 +10,10 @@ export declare function runWorkerCycle(): Promise<{
|
|
|
10
10
|
idle?: undefined;
|
|
11
11
|
reason?: undefined;
|
|
12
12
|
}>;
|
|
13
|
+
export declare function shouldExitWorkerLoopAfterIdle(options: {
|
|
14
|
+
idleExitMs?: number | null;
|
|
15
|
+
idleSince: number | null;
|
|
16
|
+
now: number;
|
|
17
|
+
processed: number;
|
|
18
|
+
}): boolean;
|
|
13
19
|
export declare function startWorkerLoop(): Promise<void>;
|