hammerkit 1.27.0-next.8 → 1.27.0-next.9
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/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +4 -9
- package/dist/kubernetes/ensure-persistent-data.js +2 -5
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +9 -6
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +14 -7
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +2 -2
- package/dist/planner/work-runtime-kubernetes.js +19 -15
- package/dist/server/kubernetes-backend.js +70 -29
- package/dist/server/runs.js +10 -3
- package/package.json +1 -1
package/dist/kubernetes/apply.js
CHANGED
|
@@ -36,28 +36,39 @@ exports.apply = apply;
|
|
|
36
36
|
* of a policy).
|
|
37
37
|
*/
|
|
38
38
|
async function replace(instance, spec) {
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
for (let attempt = 1;; attempt++) {
|
|
40
|
+
let current;
|
|
41
|
+
try {
|
|
42
|
+
current = await instance.objectApi.read(spec);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (statusCodeOf(e) !== 404) {
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return await instance.objectApi.create(spec);
|
|
50
|
+
}
|
|
51
|
+
catch (createError) {
|
|
52
|
+
if (statusCodeOf(createError) !== 409) {
|
|
53
|
+
throw createError;
|
|
54
|
+
}
|
|
55
|
+
current = await instance.objectApi.read(spec);
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
try {
|
|
48
|
-
return await instance.objectApi.
|
|
59
|
+
return await instance.objectApi.replace({
|
|
60
|
+
...spec,
|
|
61
|
+
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
|
-
catch (
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
catch (e) {
|
|
65
|
+
// replaced by someone else since it was read (the server sets up a caller's
|
|
66
|
+
// namespace for a run and for a sandbox at the same time): read it again
|
|
67
|
+
if (statusCodeOf(e) !== 409 || attempt >= 3) {
|
|
68
|
+
throw e;
|
|
53
69
|
}
|
|
54
|
-
current = await instance.objectApi.read(spec);
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
|
-
return await instance.objectApi.replace({
|
|
58
|
-
...spec,
|
|
59
|
-
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
60
|
-
});
|
|
61
72
|
}
|
|
62
73
|
exports.replace = replace;
|
|
63
74
|
//# sourceMappingURL=apply.js.map
|
|
@@ -36,7 +36,7 @@ async function ensureHttpRoute(instance, env, route, service) {
|
|
|
36
36
|
],
|
|
37
37
|
backendRefs: [
|
|
38
38
|
{
|
|
39
|
-
name: (0, resources_1.getResourceName)(service),
|
|
39
|
+
name: (0, resources_1.getResourceName)(service, env),
|
|
40
40
|
port: route.servicePort ?? service.data.ports[0].containerPort,
|
|
41
41
|
},
|
|
42
42
|
],
|
|
@@ -21,7 +21,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
21
21
|
// the needed services by name, and the proxy of a limited network
|
|
22
22
|
network) {
|
|
23
23
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
24
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
24
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
25
25
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
26
26
|
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
27
27
|
const probe = healthcheckCommand
|
|
@@ -47,25 +47,21 @@ network) {
|
|
|
47
47
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
48
48
|
},
|
|
49
49
|
labels: {
|
|
50
|
-
|
|
50
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
51
51
|
'hammerkit.dev/state': stateKey,
|
|
52
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
53
52
|
[exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
|
|
54
53
|
},
|
|
55
54
|
},
|
|
56
55
|
spec: {
|
|
57
56
|
selector: {
|
|
58
|
-
matchLabels:
|
|
59
|
-
'hammerkit.dev/id': service.id(),
|
|
60
|
-
},
|
|
57
|
+
matchLabels: (0, resources_1.getItemLabels)(service, env),
|
|
61
58
|
},
|
|
62
59
|
replicas: 1,
|
|
63
60
|
template: {
|
|
64
61
|
metadata: {
|
|
65
62
|
labels: {
|
|
66
|
-
|
|
63
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
67
64
|
'hammerkit.dev/state': stateKey,
|
|
68
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
69
65
|
...workspace.labels,
|
|
70
66
|
...(0, network_policy_1.getNetworkLabels)(env, service),
|
|
71
67
|
},
|
|
@@ -127,7 +123,7 @@ network) {
|
|
|
127
123
|
return;
|
|
128
124
|
}
|
|
129
125
|
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
130
|
-
podSelector:
|
|
126
|
+
podSelector: (0, resources_1.getItemSelector)(service, env),
|
|
131
127
|
timeout: service.data.healthcheck?.timeout ?? null,
|
|
132
128
|
timeoutMessage: service.data.healthcheck
|
|
133
129
|
? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
|
|
@@ -4,11 +4,10 @@ exports.ensureKubernetesServiceExists = void 0;
|
|
|
4
4
|
const apply_1 = require("./apply");
|
|
5
5
|
const resources_1 = require("./resources");
|
|
6
6
|
const version_1 = require("../version");
|
|
7
|
-
const job_placement_1 = require("./job-placement");
|
|
8
7
|
// A service without ports gets a headless Service: a ClusterIP one needs a port,
|
|
9
8
|
// and its tasks reach every port of the pod, as on docker.
|
|
10
9
|
async function ensureKubernetesServiceExists(instance, env, service) {
|
|
11
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
10
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
12
11
|
const headless = service.data.ports.length === 0;
|
|
13
12
|
const svc = {
|
|
14
13
|
kind: 'Service',
|
|
@@ -19,17 +18,13 @@ async function ensureKubernetesServiceExists(instance, env, service) {
|
|
|
19
18
|
annotations: {
|
|
20
19
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
21
20
|
},
|
|
22
|
-
labels:
|
|
23
|
-
'hammerkit.dev/id': service.id(),
|
|
24
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
25
|
-
},
|
|
21
|
+
labels: (0, resources_1.getItemLabels)(service, env),
|
|
26
22
|
},
|
|
27
23
|
spec: {
|
|
28
24
|
type: 'ClusterIP',
|
|
29
25
|
...(headless ? { clusterIP: 'None' } : {}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
26
|
+
// the pods of the service of this run, on a server where runs share the namespace
|
|
27
|
+
selector: (0, resources_1.getItemLabels)(service, env),
|
|
33
28
|
...(headless
|
|
34
29
|
? {}
|
|
35
30
|
: {
|
|
@@ -67,17 +67,14 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
67
67
|
for (const volume of persistence.volumes) {
|
|
68
68
|
await (0, ensure_kubernetes_persistent_volume_claim_exists_1.ensureKubernetesPersistentVolumeClaimExists)(instance, env, volume, service, persistence.sizes[volume.name], persistence.kept.includes(volume.name));
|
|
69
69
|
}
|
|
70
|
-
const name = (0, resources_1.getResourceName)(service, type === 'write' ? '-upload' : '-download');
|
|
70
|
+
const name = (0, resources_1.getResourceName)(service, env, type === 'write' ? '-upload' : '-download');
|
|
71
71
|
const podSpec = {
|
|
72
72
|
kind: 'Pod',
|
|
73
73
|
apiVersion: 'v1',
|
|
74
74
|
metadata: {
|
|
75
75
|
namespace: env.namespace,
|
|
76
76
|
name,
|
|
77
|
-
labels:
|
|
78
|
-
'hammerkit.dev/id': service.id(),
|
|
79
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
80
|
-
},
|
|
77
|
+
labels: (0, resources_1.getItemLabels)(service, env),
|
|
81
78
|
annotations: {
|
|
82
79
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
83
80
|
},
|
|
@@ -5,7 +5,7 @@ const resources_1 = require("./resources");
|
|
|
5
5
|
// The address a task reaches the service on: its clusterIP, or for a headless
|
|
6
6
|
// Service (a service without ports) the address of its ready pod.
|
|
7
7
|
async function getServiceIp(instance, env, workService) {
|
|
8
|
-
const name = (0, resources_1.getResourceName)(workService);
|
|
8
|
+
const name = (0, resources_1.getResourceName)(workService, env);
|
|
9
9
|
const svc = await instance.coreApi.readNamespacedService({ name, namespace: env.namespace });
|
|
10
10
|
const clusterIP = svc.spec?.clusterIP ?? null;
|
|
11
11
|
if (clusterIP !== 'None') {
|
|
@@ -13,7 +13,7 @@ async function getServiceIp(instance, env, workService) {
|
|
|
13
13
|
}
|
|
14
14
|
const pods = await instance.coreApi.listNamespacedPod({
|
|
15
15
|
namespace: env.namespace,
|
|
16
|
-
labelSelector:
|
|
16
|
+
labelSelector: (0, resources_1.getItemSelector)(workService, env),
|
|
17
17
|
});
|
|
18
18
|
const ready = pods.items.find((pod) => !pod.metadata?.deletionTimestamp &&
|
|
19
19
|
pod.status?.podIP &&
|
|
@@ -65,8 +65,8 @@ exports.getSecretHosts = getSecretHosts;
|
|
|
65
65
|
function accountsOf(secrets) {
|
|
66
66
|
return [...new Set(secrets.map((s) => s.source.account))];
|
|
67
67
|
}
|
|
68
|
-
function getCredentialsName(item) {
|
|
69
|
-
return (0, resources_1.getResourceName)(item, '-credentials');
|
|
68
|
+
function getCredentialsName(item, kubernetes) {
|
|
69
|
+
return (0, resources_1.getResourceName)(item, kubernetes, '-credentials');
|
|
70
70
|
}
|
|
71
71
|
exports.getCredentialsName = getCredentialsName;
|
|
72
72
|
// The keys of the credentials Secret: `<account>.token` and `<account>.file.<key>`
|
|
@@ -136,9 +136,9 @@ async function ensureJobCredentials(instance, kubernetes, item, secrets, admitte
|
|
|
136
136
|
apiVersion: 'v1',
|
|
137
137
|
metadata: {
|
|
138
138
|
namespace: kubernetes.namespace,
|
|
139
|
-
name: getCredentialsName(item),
|
|
139
|
+
name: getCredentialsName(item, kubernetes),
|
|
140
140
|
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
141
|
-
labels:
|
|
141
|
+
labels: (0, resources_1.getItemLabels)(item, kubernetes),
|
|
142
142
|
},
|
|
143
143
|
type: 'Opaque',
|
|
144
144
|
data,
|
|
@@ -169,7 +169,10 @@ async function removeJobCredentials(instance, kubernetes, item) {
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
try {
|
|
172
|
-
await instance.coreApi.deleteNamespacedSecret({
|
|
172
|
+
await instance.coreApi.deleteNamespacedSecret({
|
|
173
|
+
name: getCredentialsName(item, kubernetes),
|
|
174
|
+
namespace: kubernetes.namespace,
|
|
175
|
+
});
|
|
173
176
|
}
|
|
174
177
|
catch (e) {
|
|
175
178
|
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
@@ -213,7 +216,7 @@ proxyEnv = []) {
|
|
|
213
216
|
}
|
|
214
217
|
const name = `hammerkit-account-${index}`;
|
|
215
218
|
accountVolume.set(account, name);
|
|
216
|
-
volumes.push({ name, secret: { secretName: getCredentialsName(item), items, defaultMode: 0o444 } });
|
|
219
|
+
volumes.push({ name, secret: { secretName: getCredentialsName(item, { server }), items, defaultMode: 0o444 } });
|
|
217
220
|
});
|
|
218
221
|
const initContainers = secrets.map(({ index, secret, source }) => {
|
|
219
222
|
const label = `secret ${(0, work_secret_1.getSecretName)(secret)} (${(0, work_secret_1.describeSecretSource)(secret)})`;
|
|
@@ -5,6 +5,7 @@ const crypto_1 = require("crypto");
|
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const sleep_1 = require("../utils/sleep");
|
|
7
7
|
const job_placement_1 = require("./job-placement");
|
|
8
|
+
const resources_1 = require("./resources");
|
|
8
9
|
// On a server with `backend.keepVolumes`, the claim of a task stays after the run, so the
|
|
9
10
|
// next run of the same source finds the outputs in it and mounts them without restoring
|
|
10
11
|
// them from the cache. A kept claim has no run label (the end of the run removes what has
|
|
@@ -18,11 +19,15 @@ exports.STATE_ANNOTATION = 'hammerkit.dev/state';
|
|
|
18
19
|
// when it goes, set by the end of the run that mounted it last
|
|
19
20
|
exports.EXPIRES_ANNOTATION = 'hammerkit.dev/expires';
|
|
20
21
|
// The claim of a work item: a task's on a server that keeps them carries the scope, so a
|
|
21
|
-
// run of another source never mounts it
|
|
22
|
+
// run of another source never mounts it, and the runs of one scope go one at a time
|
|
23
|
+
// (KubernetesExecutor.exclusive). Any other claim on a server is the run's own, as the
|
|
24
|
+
// runs of a caller share their namespace. 63 characters at most, a pod's volume name.
|
|
22
25
|
function getClaimName(work, kubernetes) {
|
|
23
|
-
const scope = kubernetes
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const scope = kubernetes.server?.volumeScope;
|
|
27
|
+
if (scope && isTask(work)) {
|
|
28
|
+
return `hammerkit-${work.id()}-${(0, crypto_1.createHash)('sha256').update(scope).digest('hex').slice(0, 12)}`;
|
|
29
|
+
}
|
|
30
|
+
return 'hammerkit-' + (0, resources_1.getItemKey)(work, kubernetes);
|
|
26
31
|
}
|
|
27
32
|
exports.getClaimName = getClaimName;
|
|
28
33
|
// whether the claim of the work item stays after the run
|
|
@@ -156,14 +161,17 @@ async function expireKeptClaims(instance, namespace, runLabel, expires) {
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
exports.expireKeptClaims = expireKeptClaims;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// the others expires.
|
|
164
|
+
// The kept claims that expired, and those a run left without an expiry because the server
|
|
165
|
+
// stopped before its end (all of them when the server keeps none any more), but those of
|
|
166
|
+
// the scopes `inUse`: a run of the scope executes and mounts them, or may in a moment.
|
|
167
|
+
// The names of those removed, and when the next of the others expires.
|
|
163
168
|
async function sweepKeptClaims(instance, namespace, options) {
|
|
164
169
|
const expired = [];
|
|
165
170
|
let next = null;
|
|
166
171
|
for (const claim of await listKeptClaims(instance, namespace)) {
|
|
172
|
+
if (options.inUse?.has(claim.metadata?.labels?.[exports.VOLUME_SCOPE_LABEL] ?? '')) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
167
175
|
const expires = Date.parse(claim.metadata?.annotations?.[exports.EXPIRES_ANNOTATION] ?? '');
|
|
168
176
|
if (!options.keep || !(expires > options.now.getTime())) {
|
|
169
177
|
expired.push(claim);
|
|
@@ -178,14 +186,18 @@ async function sweepKeptClaims(instance, namespace, options) {
|
|
|
178
186
|
exports.sweepKeptClaims = sweepKeptClaims;
|
|
179
187
|
// When a run needs room under the quota of its namespace: the kept claims it does not use
|
|
180
188
|
// go, those of the other sources and those of its own a task of this run did not take (one
|
|
181
|
-
// renamed or changed since).
|
|
189
|
+
// renamed or changed since). Of another scope, only one its last run released, which has an
|
|
190
|
+
// expiry: one without is mounted by a run of the caller that executes now (or was left by
|
|
191
|
+
// a server that stopped, which the next sweep removes). The names of those removed.
|
|
182
192
|
async function evictKeptClaims(instance, kubernetes, timeout) {
|
|
183
193
|
const server = kubernetes.server;
|
|
184
194
|
if (!server?.volumeScope) {
|
|
185
195
|
return [];
|
|
186
196
|
}
|
|
187
197
|
const unused = await listKeptClaims(instance, kubernetes.namespace, `${exports.VOLUME_SCOPE_LABEL},${exports.KEPT_BY_LABEL}!=${(0, job_placement_1.getRunLabelValue)(server.run)}`);
|
|
188
|
-
|
|
198
|
+
const released = unused.filter((claim) => claim.metadata?.labels?.[exports.VOLUME_SCOPE_LABEL] === server.volumeScope ||
|
|
199
|
+
!!claim.metadata?.annotations?.[exports.EXPIRES_ANNOTATION]);
|
|
200
|
+
return removeClaims(instance, kubernetes.namespace, released, timeout);
|
|
189
201
|
}
|
|
190
202
|
exports.evictKeptClaims = evictKeptClaims;
|
|
191
203
|
//# sourceMappingURL=kept-claims.js.map
|
|
@@ -17,7 +17,7 @@ const sleep_1 = require("../utils/sleep");
|
|
|
17
17
|
const network_gate_1 = require("./network-gate");
|
|
18
18
|
const job_placement_1 = require("./job-placement");
|
|
19
19
|
Object.defineProperty(exports, "RUN_LABEL", { enumerable: true, get: function () { return job_placement_1.RUN_LABEL; } });
|
|
20
|
-
const
|
|
20
|
+
const resources_1 = require("./resources");
|
|
21
21
|
const PROXY_LABEL = 'hammerkit.dev/proxy-for';
|
|
22
22
|
const NETWORK_OF_LABEL = 'hammerkit.dev/network-of';
|
|
23
23
|
// on the proxy of a task on a sandbox: the sandbox, whose pod of files it stays with
|
|
@@ -86,7 +86,7 @@ function getNetworkPolicyName(item, scope) {
|
|
|
86
86
|
exports.getNetworkPolicyName = getNetworkPolicyName;
|
|
87
87
|
// the pods of the item, of the run when the scope has one
|
|
88
88
|
function getItemPodLabels(item, scope) {
|
|
89
|
-
return { [ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
89
|
+
return { [resources_1.ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
90
90
|
}
|
|
91
91
|
function toSelector(labels) {
|
|
92
92
|
return Object.entries(labels)
|
|
@@ -107,7 +107,12 @@ function getIngress(env, item) {
|
|
|
107
107
|
if (ids.length === 0) {
|
|
108
108
|
return [];
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// on a server, of this run: the runs of a caller share their namespace
|
|
111
|
+
const run = (0, job_placement_1.getRunLabels)(env);
|
|
112
|
+
const from = {
|
|
113
|
+
matchExpressions: [{ key: resources_1.ID_LABEL, operator: 'In', values: ids }],
|
|
114
|
+
...(Object.keys(run).length > 0 ? { matchLabels: run } : {}),
|
|
115
|
+
};
|
|
111
116
|
// the client's name for `from`, a reserved word
|
|
112
117
|
return [{ _from: [{ podSelector: from }] }];
|
|
113
118
|
}
|
|
@@ -132,7 +137,7 @@ function buildNetworkPolicy(env, item, network, scope, proxy = getRunProxy(item,
|
|
|
132
137
|
ingress: getIngress(env, item),
|
|
133
138
|
egress: [
|
|
134
139
|
...(needs.length > 0
|
|
135
|
-
? [{ to: needs.map((need) => ({ podSelector: { matchLabels:
|
|
140
|
+
? [{ to: needs.map((need) => ({ podSelector: { matchLabels: (0, resources_1.getItemLabels)(need.service, env) } })) }]
|
|
136
141
|
: []),
|
|
137
142
|
...(proxied
|
|
138
143
|
? [
|
|
@@ -306,12 +311,14 @@ async function releaseKubernetesItemNetwork(instance, env, item, scope) {
|
|
|
306
311
|
}
|
|
307
312
|
exports.releaseKubernetesItemNetwork = releaseKubernetesItemNetwork;
|
|
308
313
|
// Every policy and proxy of the item, of any run, once none of its pods runs:
|
|
309
|
-
// what `down` and `clean` remove.
|
|
314
|
+
// what `down` and `clean` remove. On a server, those of the server's run only: the
|
|
315
|
+
// runs of a caller share their namespace, and the pods of another run of the same item
|
|
316
|
+
// would run unlimited without their policy.
|
|
310
317
|
async function removeAllKubernetesItemNetworks(instance, env, item) {
|
|
311
|
-
if (!(await awaitPodsEnded(instance, env,
|
|
318
|
+
if (!(await awaitPodsEnded(instance, env, (0, resources_1.getItemLabels)(item, env)))) {
|
|
312
319
|
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
313
320
|
}
|
|
314
|
-
const labelSelector =
|
|
321
|
+
const labelSelector = toSelector({ [NETWORK_OF_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) });
|
|
315
322
|
let policies;
|
|
316
323
|
try {
|
|
317
324
|
const list = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', env.namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.removePersistentData = void 0;
|
|
4
|
+
const resources_1 = require("./resources");
|
|
4
5
|
async function removePersistentData(instance, kubernetes, task) {
|
|
5
|
-
|
|
6
|
+
// on a server, the run's: a claim it kept has no run label and goes once it expired
|
|
7
|
+
const labelSelector = (0, resources_1.getItemSelector)(task, kubernetes);
|
|
6
8
|
const configmaps = await instance.coreApi.listNamespacedConfigMap({ namespace: kubernetes.namespace, labelSelector });
|
|
7
9
|
for (const configMap of configmaps.items) {
|
|
8
10
|
if (configMap.metadata?.name) {
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.removeInvalidCharacters = exports.getResourceName = void 0;
|
|
3
|
+
exports.removeInvalidCharacters = exports.getResourceName = exports.getItemSelector = exports.getItemLabels = exports.getItemKey = exports.ID_LABEL = void 0;
|
|
4
|
+
const job_placement_1 = require("./job-placement");
|
|
4
5
|
// An object name is a DNS label: lowercase letters, digits and `-`, at most 63
|
|
5
6
|
// characters, starting and ending with a letter or digit. The id makes a name
|
|
6
7
|
// unique; the item's name is there for people and is cut to fit.
|
|
7
8
|
const MAX_NAME = 63;
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
exports.ID_LABEL = 'hammerkit.dev/id';
|
|
10
|
+
// What tells the objects of an item apart in its namespace: its id, and on a server the
|
|
11
|
+
// run's label too. The runs of a caller execute side by side in their namespace, and two
|
|
12
|
+
// of them often run the same task (the same command on two sandboxes of a repository),
|
|
13
|
+
// so each has objects of its own, which nothing of the other run finds or removes. The
|
|
14
|
+
// first 8 digits of the run label tell runs apart, as they do for a run's proxies.
|
|
15
|
+
function getItemKey(item, env) {
|
|
16
|
+
return env.server ? `${item.id().substring(0, 32)}-${(0, job_placement_1.getRunLabelValue)(env.server.run).substring(0, 8)}` : item.id();
|
|
17
|
+
}
|
|
18
|
+
exports.getItemKey = getItemKey;
|
|
19
|
+
// The labels the objects of an item carry, by which they are found again
|
|
20
|
+
function getItemLabels(item, env) {
|
|
21
|
+
return { [exports.ID_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) };
|
|
22
|
+
}
|
|
23
|
+
exports.getItemLabels = getItemLabels;
|
|
24
|
+
function getItemSelector(item, env) {
|
|
25
|
+
return Object.entries(getItemLabels(item, env))
|
|
26
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
27
|
+
.join(',');
|
|
28
|
+
}
|
|
29
|
+
exports.getItemSelector = getItemSelector;
|
|
30
|
+
function getResourceName(item, env, suffix) {
|
|
31
|
+
const tail = `-${getItemKey(item, env)}${suffix ?? ''}`;
|
|
10
32
|
const name = removeInvalidCharacters(item.name)
|
|
11
33
|
.slice(0, Math.max(0, MAX_NAME - tail.length))
|
|
12
34
|
.replace(/-+$/, '');
|
|
@@ -5,19 +5,18 @@ const work_secret_1 = require("../planner/work-secret");
|
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const resources_1 = require("./resources");
|
|
7
7
|
const version_1 = require("../version");
|
|
8
|
-
const job_placement_1 = require("./job-placement");
|
|
9
8
|
const SECRET_VOLUME_NAME = 'hammerkit-secrets';
|
|
10
|
-
function getSecretResourceName(item) {
|
|
11
|
-
return (0, resources_1.getResourceName)(item, '-secrets');
|
|
9
|
+
function getSecretResourceName(item, kubernetes) {
|
|
10
|
+
return (0, resources_1.getResourceName)(item, kubernetes, '-secrets');
|
|
12
11
|
}
|
|
13
12
|
exports.getSecretResourceName = getSecretResourceName;
|
|
14
13
|
// The values go into a Secret of the item; the pod only references it, so no
|
|
15
14
|
// value is part of a Job or Deployment spec.
|
|
16
|
-
function getKubernetesSecretRefs(item, secrets) {
|
|
15
|
+
function getKubernetesSecretRefs(item, kubernetes, secrets) {
|
|
17
16
|
if (secrets.length === 0) {
|
|
18
17
|
return { env: [], mounts: [], volumes: [] };
|
|
19
18
|
}
|
|
20
|
-
const secretName = getSecretResourceName(item);
|
|
19
|
+
const secretName = getSecretResourceName(item, kubernetes);
|
|
21
20
|
const refs = {
|
|
22
21
|
env: [],
|
|
23
22
|
mounts: [],
|
|
@@ -50,14 +49,11 @@ async function ensureKubernetesSecret(instance, kubernetes, item, secrets, envir
|
|
|
50
49
|
apiVersion: 'v1',
|
|
51
50
|
metadata: {
|
|
52
51
|
namespace: kubernetes.namespace,
|
|
53
|
-
name: getSecretResourceName(item),
|
|
52
|
+
name: getSecretResourceName(item, kubernetes),
|
|
54
53
|
annotations: {
|
|
55
54
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
56
55
|
},
|
|
57
|
-
labels:
|
|
58
|
-
'hammerkit.dev/id': item.id(),
|
|
59
|
-
...(0, job_placement_1.getRunLabels)(kubernetes),
|
|
60
|
-
},
|
|
56
|
+
labels: (0, resources_1.getItemLabels)(item, kubernetes),
|
|
61
57
|
},
|
|
62
58
|
type: 'Opaque',
|
|
63
59
|
stringData,
|
|
@@ -68,7 +64,7 @@ exports.ensureKubernetesSecret = ensureKubernetesSecret;
|
|
|
68
64
|
async function removeKubernetesSecret(instance, kubernetes, item) {
|
|
69
65
|
try {
|
|
70
66
|
await instance.coreApi.deleteNamespacedSecret({
|
|
71
|
-
name: getSecretResourceName(item),
|
|
67
|
+
name: getSecretResourceName(item, kubernetes),
|
|
72
68
|
namespace: kubernetes.namespace,
|
|
73
69
|
});
|
|
74
70
|
}
|
|
@@ -51,7 +51,7 @@ async function getKubernetesPersistence(work, kubernetes) {
|
|
|
51
51
|
mounts: [],
|
|
52
52
|
initCommands: [],
|
|
53
53
|
};
|
|
54
|
-
const workspace = kubernetes
|
|
54
|
+
const workspace = kubernetes.server?.workspace;
|
|
55
55
|
for (const mount of work.data.mounts) {
|
|
56
56
|
if (workspace &&
|
|
57
57
|
appendWorkspaceMount(workspace, persistence, mount.containerPath, mount.localPath, mount.isFile, mount.readOnly)) {
|
|
@@ -94,7 +94,7 @@ function appendVolume(sourceWork, persistence, containerPath, localPath, isFile,
|
|
|
94
94
|
},
|
|
95
95
|
});
|
|
96
96
|
persistence.sizes[volumeName] = 'volumeSize' in sourceWork.data ? sourceWork.data.volumeSize : null;
|
|
97
|
-
if (
|
|
97
|
+
if ((0, kept_claims_1.isKeptClaim)(sourceWork, kubernetes)) {
|
|
98
98
|
persistence.kept.push(volumeName);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -38,7 +38,7 @@ const work_secret_1 = require("./work-secret");
|
|
|
38
38
|
async function listJobNames(instance, kubernetes, task) {
|
|
39
39
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
40
40
|
namespace: kubernetes.namespace,
|
|
41
|
-
labelSelector:
|
|
41
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
42
42
|
});
|
|
43
43
|
return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
|
|
44
44
|
}
|
|
@@ -201,7 +201,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
201
201
|
}
|
|
202
202
|
const server = kubernetes.server;
|
|
203
203
|
const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
|
|
204
|
-
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, delivered);
|
|
204
|
+
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, kubernetes, delivered);
|
|
205
205
|
const admitted = environment.hosted?.accounts;
|
|
206
206
|
let secretHosts;
|
|
207
207
|
try {
|
|
@@ -230,7 +230,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
230
230
|
: {}),
|
|
231
231
|
};
|
|
232
232
|
const labels = {
|
|
233
|
-
|
|
233
|
+
...(0, resources_1.getItemLabels)(task, kubernetes),
|
|
234
234
|
'hammerkit.dev/state': options.stateKey,
|
|
235
235
|
...workspace.labels,
|
|
236
236
|
[network_policy_1.RUN_LABEL]: scope.run,
|
|
@@ -258,7 +258,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
try {
|
|
261
|
-
const podName = (0, resources_1.getResourceName)(task, '-job');
|
|
261
|
+
const podName = (0, resources_1.getResourceName)(task, kubernetes, '-job');
|
|
262
262
|
let i = 0;
|
|
263
263
|
for (const cmd of task.data.cmds) {
|
|
264
264
|
const container = `cmd-${++i}`;
|
|
@@ -387,7 +387,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
387
387
|
async stop() {
|
|
388
388
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
389
389
|
namespace: kubernetes.namespace,
|
|
390
|
-
labelSelector:
|
|
390
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
391
391
|
});
|
|
392
392
|
for (const job of jobs.items) {
|
|
393
393
|
if (job.metadata?.name) {
|
|
@@ -396,7 +396,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
396
396
|
}
|
|
397
397
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
398
398
|
namespace: kubernetes.namespace,
|
|
399
|
-
labelSelector:
|
|
399
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
400
400
|
});
|
|
401
401
|
for (const deploy of deployments.items) {
|
|
402
402
|
if (deploy.metadata?.name) {
|
|
@@ -408,7 +408,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
408
408
|
}
|
|
409
409
|
const pods = await instance.coreApi.listNamespacedPod({
|
|
410
410
|
namespace: kubernetes.namespace,
|
|
411
|
-
labelSelector:
|
|
411
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
412
412
|
});
|
|
413
413
|
for (const pod of pods.items) {
|
|
414
414
|
if (pod.metadata?.name) {
|
|
@@ -430,7 +430,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
430
430
|
}
|
|
431
431
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
432
432
|
namespace: kubernetes.namespace,
|
|
433
|
-
labelSelector:
|
|
433
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
434
434
|
});
|
|
435
435
|
const completedStates = jobs.items
|
|
436
436
|
.filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
|
|
@@ -467,7 +467,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
467
467
|
async initialize(state) {
|
|
468
468
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
469
469
|
namespace: kubernetes.namespace,
|
|
470
|
-
labelSelector:
|
|
470
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
471
471
|
});
|
|
472
472
|
const deployment = deployments.items[0];
|
|
473
473
|
if (!deployment?.metadata?.name) {
|
|
@@ -494,7 +494,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
494
494
|
async stop() {
|
|
495
495
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
496
496
|
namespace: kubernetes.namespace,
|
|
497
|
-
labelSelector:
|
|
497
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
498
498
|
});
|
|
499
499
|
for (const deployment of deployments.items) {
|
|
500
500
|
if (deployment.metadata?.name) {
|
|
@@ -514,9 +514,13 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
514
514
|
await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
|
|
515
515
|
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
|
|
516
516
|
await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
|
|
517
|
-
|
|
518
|
-
await (0,
|
|
519
|
-
|
|
517
|
+
// on a server, the run's: its runs share the namespace, and the service goes with the run
|
|
518
|
+
const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
|
|
519
|
+
followLog: false,
|
|
520
|
+
run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
|
|
521
|
+
});
|
|
522
|
+
await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network);
|
|
523
|
+
const name = (0, resources_1.getResourceName)(service, kubernetes);
|
|
520
524
|
options.state.set({
|
|
521
525
|
type: 'running',
|
|
522
526
|
dns: { containerId: name },
|
|
@@ -548,7 +552,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
548
552
|
}
|
|
549
553
|
const services = await instance.coreApi.listNamespacedService({
|
|
550
554
|
namespace: kubernetes.namespace,
|
|
551
|
-
labelSelector:
|
|
555
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
552
556
|
});
|
|
553
557
|
for (const service of services.items) {
|
|
554
558
|
if (service.metadata?.name) {
|
|
@@ -564,7 +568,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
564
568
|
async currentStateKey() {
|
|
565
569
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
566
570
|
namespace: kubernetes.namespace,
|
|
567
|
-
labelSelector:
|
|
571
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
568
572
|
});
|
|
569
573
|
const deployment = deployments.items[0];
|
|
570
574
|
return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
|
|
@@ -167,18 +167,21 @@ exports.getRunRoleBinding = getRunRoleBinding;
|
|
|
167
167
|
* process of the server as on a host (see HostExecutor); what changes is where its
|
|
168
168
|
* container tasks and services go: into the caller's namespace, on the nodes of the
|
|
169
169
|
* operator's pools, with the secrets of providers read in the Job itself. The runs
|
|
170
|
-
* of
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
170
|
+
* of a caller execute side by side in their namespace, up to `runs.limits.running`:
|
|
171
|
+
* the objects of a run are named and found by its run label (getItemKey), so two runs
|
|
172
|
+
* of the same task have their own. What two runs share are the claims the server keeps
|
|
173
|
+
* (`backend.keepVolumes`) of a scope (getVolumeScope): the runs of one scope go one at
|
|
174
|
+
* a time (exclusive). When a run ends, everything made for it is removed, but for the
|
|
175
|
+
* kept claims, which go once they expired.
|
|
174
176
|
*/
|
|
175
177
|
class KubernetesExecutor {
|
|
176
178
|
constructor(config, processEnvs, options) {
|
|
177
179
|
this.config = config;
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
// the run that executes, or the last one waiting, per caller namespace
|
|
180
|
+
// per caller namespace, what changes objects of the whole namespace: its setup, the
|
|
181
|
+
// removal of what a stopped server left, the sweeps of kept claims. The last one waiting.
|
|
181
182
|
this.queues = new Map();
|
|
183
|
+
// per caller namespace, the runs that execute in it: by run label, their volume scope
|
|
184
|
+
this.active = new Map();
|
|
182
185
|
// per caller namespace, when its kept claims are next looked at
|
|
183
186
|
this.sweeps = new Map();
|
|
184
187
|
// whether the kept claims a server left when it stopped were looked for
|
|
@@ -203,17 +206,45 @@ class KubernetesExecutor {
|
|
|
203
206
|
this.log = options.log;
|
|
204
207
|
this.host = new executor_1.HostExecutor({ ...options, placement: this.placement() });
|
|
205
208
|
}
|
|
206
|
-
execute(run, context) {
|
|
209
|
+
async execute(run, context) {
|
|
207
210
|
const namespace = getCallerNamespace(this.config.backend.namespacePrefix, run.owner);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
212
|
+
let runs = this.active.get(namespace);
|
|
213
|
+
if (!runs) {
|
|
214
|
+
runs = new Map();
|
|
215
|
+
this.active.set(namespace, runs);
|
|
216
|
+
}
|
|
217
|
+
runs.set(label, getVolumeScope(run));
|
|
218
|
+
try {
|
|
219
|
+
await this.host.execute(run, context);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
runs.delete(label);
|
|
223
|
+
if (runs.size === 0 && this.active.get(namespace) === runs) {
|
|
224
|
+
this.active.delete(namespace);
|
|
211
225
|
}
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// The runs of a scope share its kept claims, which one of them may change at a time:
|
|
229
|
+
// what a task finds in one is what the last run left there. Without kept claims, runs
|
|
230
|
+
// share no object and nothing is held.
|
|
231
|
+
exclusive(run) {
|
|
232
|
+
if (this.keep === 0) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
return `${getCallerNamespace(this.config.backend.namespacePrefix, run.owner)}/${getVolumeScope(run)}`;
|
|
214
236
|
}
|
|
215
|
-
//
|
|
216
|
-
//
|
|
237
|
+
// The scopes of the runs that execute in the namespace, but `except`'s: their kept claims
|
|
238
|
+
// are theirs to mount and stay. A server that keeps none mounts none.
|
|
239
|
+
scopesInUse(namespace, except) {
|
|
240
|
+
if (this.keep === 0) {
|
|
241
|
+
return new Set();
|
|
242
|
+
}
|
|
243
|
+
return new Set([...(this.active.get(namespace) ?? new Map()).entries()]
|
|
244
|
+
.filter(([label]) => label !== except)
|
|
245
|
+
.map(([, scope]) => scope));
|
|
246
|
+
}
|
|
247
|
+
// What changes objects of a whole namespace happens one at a time, not what its runs do
|
|
217
248
|
enqueue(namespace, fn) {
|
|
218
249
|
const previous = this.queues.get(namespace) ?? Promise.resolve();
|
|
219
250
|
const execution = previous.then(fn);
|
|
@@ -231,19 +262,28 @@ class KubernetesExecutor {
|
|
|
231
262
|
return {
|
|
232
263
|
prepare: async (run, context, work) => {
|
|
233
264
|
const namespace = getCallerNamespace(backend.namespacePrefix, run.owner);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
266
|
+
await this.enqueue(namespace, async () => {
|
|
267
|
+
if (context.signal.aborted) {
|
|
268
|
+
throw new Error('the run was cancelled');
|
|
269
|
+
}
|
|
270
|
+
await this.namespaces.prepare(namespace);
|
|
271
|
+
// what an earlier run left when the server stopped before its end: the objects of
|
|
272
|
+
// a run that does not execute. One that starts after this waits for it here
|
|
273
|
+
// before it makes any.
|
|
274
|
+
const running = [...new Set([label, ...(this.active.get(namespace)?.keys() ?? [])])];
|
|
275
|
+
await removeRunObjects(this.instance, namespace, `${job_placement_1.RUN_LABEL},${job_placement_1.RUN_LABEL} notin (${running.join(',')})`, RELEASE_TIMEOUT);
|
|
276
|
+
// the claims earlier runs kept that expired; the others this run may mount
|
|
277
|
+
const { next } = await (0, kept_claims_1.sweepKeptClaims)(this.instance, namespace, {
|
|
278
|
+
now: new Date(),
|
|
279
|
+
keep: this.keep > 0,
|
|
280
|
+
timeout: RELEASE_TIMEOUT,
|
|
281
|
+
inUse: this.scopesInUse(namespace, label),
|
|
282
|
+
});
|
|
283
|
+
if (next) {
|
|
284
|
+
this.sweepLater(namespace, Math.max(0, next.getTime() - Date.now()));
|
|
285
|
+
}
|
|
243
286
|
});
|
|
244
|
-
if (next) {
|
|
245
|
-
this.sweepLater(namespace, Math.max(0, next.getTime() - Date.now()));
|
|
246
|
-
}
|
|
247
287
|
this.sweepLeftClaims();
|
|
248
288
|
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `runs in the namespace ${namespace}` });
|
|
249
289
|
const kubernetes = {
|
|
@@ -300,8 +340,8 @@ class KubernetesExecutor {
|
|
|
300
340
|
},
|
|
301
341
|
};
|
|
302
342
|
}
|
|
303
|
-
// The kept claims of a namespace that expired go
|
|
304
|
-
//
|
|
343
|
+
// The kept claims of a namespace that expired go at the soonest expiry the server knows
|
|
344
|
+
// of, but those of a scope a run of the caller executes in
|
|
305
345
|
sweepLater(namespace, delay) {
|
|
306
346
|
const at = Date.now() + delay + 1000;
|
|
307
347
|
const pending = this.sweeps.get(namespace);
|
|
@@ -321,6 +361,7 @@ class KubernetesExecutor {
|
|
|
321
361
|
now: new Date(),
|
|
322
362
|
keep: this.keep > 0,
|
|
323
363
|
timeout: RELEASE_TIMEOUT,
|
|
364
|
+
inUse: this.scopesInUse(namespace),
|
|
324
365
|
});
|
|
325
366
|
if (removed.length > 0) {
|
|
326
367
|
this.log?.(`removed ${removed.length} kept claims in ${namespace}`);
|
package/dist/server/runs.js
CHANGED
|
@@ -424,13 +424,20 @@ class RunRegistry {
|
|
|
424
424
|
return (source.type === 'sandbox' &&
|
|
425
425
|
[...this.entries.values()].some((entry) => entry.run.state === 'running' && onSandbox(entry.run, source.id)));
|
|
426
426
|
}
|
|
427
|
+
// Whether a run that executes holds what the run would (RunExecutor.exclusive)
|
|
428
|
+
held(run) {
|
|
429
|
+
const exclusive = this.options.executor.exclusive?.bind(this.options.executor);
|
|
430
|
+
const key = exclusive?.(run);
|
|
431
|
+
return (key !== undefined &&
|
|
432
|
+
[...this.entries.values()].some((entry) => entry.run.state === 'running' && exclusive?.(entry.run) === key));
|
|
433
|
+
}
|
|
427
434
|
// The first run in the queue whose caller has a place left: one caller with many runs
|
|
428
435
|
// does not keep the others from the machine. A run on a sandbox another run works on
|
|
429
|
-
// waits for it.
|
|
436
|
+
// waits for it, as does one that needs what an executing run holds.
|
|
430
437
|
dispatch() {
|
|
431
438
|
while (this.running < this.maxConcurrent) {
|
|
432
|
-
const perCaller =
|
|
433
|
-
const index = this.queue.findIndex((entry) => this.runningOf(entry.run.owner) < perCaller && !this.sandboxBusy(entry.run));
|
|
439
|
+
const perCaller = this.options.limits?.running ?? Infinity;
|
|
440
|
+
const index = this.queue.findIndex((entry) => this.runningOf(entry.run.owner) < perCaller && !this.sandboxBusy(entry.run) && !this.held(entry.run));
|
|
434
441
|
if (index < 0) {
|
|
435
442
|
return;
|
|
436
443
|
}
|
package/package.json
CHANGED