@genroc/eval-node 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/realm.js +4 -1
- package/dist/worker.js +22 -9
- package/package.json +1 -1
- package/realm.ts +4 -1
- package/worker.ts +23 -9
package/README.md
CHANGED
|
@@ -78,9 +78,9 @@ export default function (input: Input): Output {
|
|
|
78
78
|
## Running the worker
|
|
79
79
|
|
|
80
80
|
```sh
|
|
81
|
-
docker run -e GENROC_SERVER=http://host:8448
|
|
81
|
+
docker run -e GENROC_SERVER=http://host:8448 ghcr.io/genroc/eval-node:latest
|
|
82
82
|
# or
|
|
83
|
-
GENROC_SERVER=http://localhost:8448
|
|
83
|
+
GENROC_SERVER=http://localhost:8448 npx genroc-eval-node
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
The worker connects to genroc, not the other way round, so it only needs outbound access.
|
|
@@ -89,7 +89,7 @@ The worker connects to genroc, not the other way round, so it only needs outboun
|
|
|
89
89
|
|---|---|---|
|
|
90
90
|
| `GENROC_SERVER` | `http://localhost:8448` | the genroc server |
|
|
91
91
|
| `GENROC_TOKEN` / `GENROC_TOKEN_FILE` | *(none)* | token, or a file holding it, when the server uses `--auth token` |
|
|
92
|
-
| `
|
|
92
|
+
| `PROCESS` / `TASK` | `script-node` / `eval_node` | claim only this process / task id; the defaults are script-node's. Set `PROCESS` for a renamed copy, or `""` to drop a filter; **with both `""`, the worker claims every external task.** |
|
|
93
93
|
| `CONCURRENCY` | `4` | scripts run at once |
|
|
94
94
|
| `LEASE_MS` | `30000` | claim lease, renewed every third of it |
|
|
95
95
|
| `POLL_MS` | `250` | idle poll interval |
|
|
@@ -109,19 +109,20 @@ match. Retrying does not help with any of them, because the same code fails the
|
|
|
109
109
|
| code | when |
|
|
110
110
|
|---|---|
|
|
111
111
|
| `threw` | the script threw or rejected |
|
|
112
|
-
| `timeout` | it ran past `
|
|
112
|
+
| `timeout` | it ran past the task's `timeout` (5000 ms when the task has none) |
|
|
113
113
|
| `compile_error` | the module does not parse, or has no default-export function |
|
|
114
114
|
| `nonserializable` | the return value cannot be serialized to JSON (a cycle, a `BigInt`) |
|
|
115
115
|
| `exited` | the script called `process.exit()` |
|
|
116
116
|
|
|
117
|
-
`error.data` is `{name, stack?}`.
|
|
118
|
-
|
|
117
|
+
`error.data` is `{name, stack?}`. `name` is the error's class name, so throwing your own
|
|
118
|
+
`class LimitExceeded extends Error {}` tells your failures apart; setting `e.name` overrides it,
|
|
119
|
+
and a thrown non-`Error` is `Thrown`. The stack is mapped back to your source lines.
|
|
119
120
|
|
|
120
121
|
If the worker itself faults, it releases the task for another worker and reports no error.
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
The task's `timeout` is the script's budget, waiting in the queue included. The worker stops
|
|
124
|
+
the script just before it and answers `timeout`, so `external.timeout` means no worker answered
|
|
125
|
+
at all. With `script-node`, set it through the `timeout_ms` input.
|
|
125
126
|
|
|
126
127
|
## Limits
|
|
127
128
|
|
package/dist/realm.js
CHANGED
|
@@ -36,7 +36,10 @@ function scriptStack(err) {
|
|
|
36
36
|
}
|
|
37
37
|
function describe(err, kind) {
|
|
38
38
|
if (err instanceof Error) {
|
|
39
|
-
|
|
39
|
+
// A subclass inherits `name` from Error.prototype, so `class LimitExceeded extends Error {}`
|
|
40
|
+
// would report "Error"; a name the script set still wins.
|
|
41
|
+
const name = err.name === "Error" && err.constructor.name ? err.constructor.name : err.name;
|
|
42
|
+
return { kind, name, message: err.message, stack: scriptStack(err) };
|
|
40
43
|
}
|
|
41
44
|
// A script may throw a non-Error (`throw {code: "x"}`), so name/message must not assume one.
|
|
42
45
|
return { kind, name: "Thrown", message: safeText(err) };
|
package/dist/worker.js
CHANGED
|
@@ -19,8 +19,13 @@ const POLL_MS = Number(process.env.POLL_MS ?? 250);
|
|
|
19
19
|
// should return its task quickly rather than holding it for the whole budget.
|
|
20
20
|
const LEASE_MS = Number(process.env.LEASE_MS ?? 30_000);
|
|
21
21
|
const RENEW_MS = Math.max(1_000, Math.floor(LEASE_MS / 3));
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// The answer has to land before the task's deadline fires, or a script that overran surfaces as
|
|
23
|
+
// external.timeout ("nobody answered") rather than as its own `timeout`.
|
|
24
|
+
const ANSWER_MARGIN_MS = 250;
|
|
25
|
+
// script-node's names. A renamed copy needs PROCESS set; an explicit "" drops that filter, and
|
|
26
|
+
// both "" claims every external task, other runtimes' included.
|
|
27
|
+
const PROCESS_FILTER = process.env.PROCESS ?? "script-node";
|
|
28
|
+
const TASK_FILTER = process.env.TASK ?? "eval_node";
|
|
24
29
|
// Large values arrive as refs; a bundle is one object per definition version, fetched once.
|
|
25
30
|
// Refs are content hashes, so the cache never invalidates.
|
|
26
31
|
const objectCache = new Map();
|
|
@@ -94,17 +99,13 @@ async function call(path, body) {
|
|
|
94
99
|
}
|
|
95
100
|
/** A task input that is not an EvalRequest is the definition's fault, reported as compile_error:
|
|
96
101
|
* the nearest permanent kind, since no retry fixes the definition. */
|
|
97
|
-
function asEvalRequest(input) {
|
|
102
|
+
function asEvalRequest(input, timeoutMs) {
|
|
98
103
|
if (typeof input !== "object" || input === null)
|
|
99
104
|
return "the task input is not an object";
|
|
100
105
|
const r = input;
|
|
101
106
|
if (typeof r.code !== "string")
|
|
102
107
|
return "the task input has no `code` string";
|
|
103
|
-
return {
|
|
104
|
-
code: r.code,
|
|
105
|
-
input: r.input,
|
|
106
|
-
timeout_ms: typeof r.timeout_ms === "number" ? r.timeout_ms : undefined,
|
|
107
|
-
};
|
|
108
|
+
return { code: r.code, input: r.input, timeout_ms: timeoutMs };
|
|
108
109
|
}
|
|
109
110
|
const inFlight = new Map();
|
|
110
111
|
let running = true;
|
|
@@ -161,6 +162,9 @@ async function answer(token, outcome) {
|
|
|
161
162
|
await release(token);
|
|
162
163
|
}
|
|
163
164
|
async function run(job, signal) {
|
|
165
|
+
// The task's own timeout is the script's budget. Taken on arrival, because fetching the
|
|
166
|
+
// objects below spends the same deadline.
|
|
167
|
+
const due = job.deadline_in_ms === undefined ? undefined : performance.now() + job.deadline_in_ms;
|
|
164
168
|
let resolved;
|
|
165
169
|
try {
|
|
166
170
|
resolved = await resolveObjects(job);
|
|
@@ -172,7 +176,16 @@ async function run(job, signal) {
|
|
|
172
176
|
await release(job.token);
|
|
173
177
|
return;
|
|
174
178
|
}
|
|
175
|
-
|
|
179
|
+
let timeoutMs;
|
|
180
|
+
if (due !== undefined) {
|
|
181
|
+
timeoutMs = Math.floor(due - performance.now() - ANSWER_MARGIN_MS);
|
|
182
|
+
if (timeoutMs <= 0) {
|
|
183
|
+
// Too late to run anything: the deadline fires as external.timeout, which is the truth.
|
|
184
|
+
await release(job.token);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const req = asEvalRequest(resolved, timeoutMs);
|
|
176
189
|
if (typeof req === "string") {
|
|
177
190
|
await answer(job.token, {
|
|
178
191
|
error: { code: "compile_error", message: req, data: { name: "BadTaskInput" } },
|
package/package.json
CHANGED
package/realm.ts
CHANGED
|
@@ -35,7 +35,10 @@ function scriptStack(err: unknown): string | undefined {
|
|
|
35
35
|
|
|
36
36
|
function describe(err: unknown, kind: FailureKind): EvalFailure {
|
|
37
37
|
if (err instanceof Error) {
|
|
38
|
-
|
|
38
|
+
// A subclass inherits `name` from Error.prototype, so `class LimitExceeded extends Error {}`
|
|
39
|
+
// would report "Error"; a name the script set still wins.
|
|
40
|
+
const name = err.name === "Error" && err.constructor.name ? err.constructor.name : err.name;
|
|
41
|
+
return { kind, name, message: err.message, stack: scriptStack(err) };
|
|
39
42
|
}
|
|
40
43
|
// A script may throw a non-Error (`throw {code: "x"}`), so name/message must not assume one.
|
|
41
44
|
return { kind, name: "Thrown", message: safeText(err) };
|
package/worker.ts
CHANGED
|
@@ -22,8 +22,13 @@ const POLL_MS = Number(process.env.POLL_MS ?? 250);
|
|
|
22
22
|
// should return its task quickly rather than holding it for the whole budget.
|
|
23
23
|
const LEASE_MS = Number(process.env.LEASE_MS ?? 30_000);
|
|
24
24
|
const RENEW_MS = Math.max(1_000, Math.floor(LEASE_MS / 3));
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// The answer has to land before the task's deadline fires, or a script that overran surfaces as
|
|
26
|
+
// external.timeout ("nobody answered") rather than as its own `timeout`.
|
|
27
|
+
const ANSWER_MARGIN_MS = 250;
|
|
28
|
+
// script-node's names. A renamed copy needs PROCESS set; an explicit "" drops that filter, and
|
|
29
|
+
// both "" claims every external task, other runtimes' included.
|
|
30
|
+
const PROCESS_FILTER = process.env.PROCESS ?? "script-node";
|
|
31
|
+
const TASK_FILTER = process.env.TASK ?? "eval_node";
|
|
27
32
|
|
|
28
33
|
type ObjectEntry = { path: (string | number)[]; ref: string; size: number };
|
|
29
34
|
|
|
@@ -34,6 +39,7 @@ type QueueTask = {
|
|
|
34
39
|
external_input: unknown;
|
|
35
40
|
objects?: ObjectEntry[];
|
|
36
41
|
raises?: Record<string, unknown>;
|
|
42
|
+
deadline_in_ms?: number;
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
// Large values arrive as refs; a bundle is one object per definition version, fetched once.
|
|
@@ -106,15 +112,11 @@ async function call(path: string, body: unknown): Promise<{ ok: boolean; status:
|
|
|
106
112
|
|
|
107
113
|
/** A task input that is not an EvalRequest is the definition's fault, reported as compile_error:
|
|
108
114
|
* the nearest permanent kind, since no retry fixes the definition. */
|
|
109
|
-
function asEvalRequest(input: unknown): EvalRequest | string {
|
|
115
|
+
function asEvalRequest(input: unknown, timeoutMs: number | undefined): EvalRequest | string {
|
|
110
116
|
if (typeof input !== "object" || input === null) return "the task input is not an object";
|
|
111
117
|
const r = input as Record<string, unknown>;
|
|
112
118
|
if (typeof r.code !== "string") return "the task input has no `code` string";
|
|
113
|
-
return {
|
|
114
|
-
code: r.code,
|
|
115
|
-
input: r.input,
|
|
116
|
-
timeout_ms: typeof r.timeout_ms === "number" ? r.timeout_ms : undefined,
|
|
117
|
-
};
|
|
119
|
+
return { code: r.code, input: r.input, timeout_ms: timeoutMs };
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/** genroc cannot call us, so a cancellation arrives on our own heartbeat and reaches the
|
|
@@ -183,6 +185,9 @@ async function answer(token: string, outcome: Record<string, unknown>): Promise<
|
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
async function run(job: QueueTask, signal: AbortSignal): Promise<void> {
|
|
188
|
+
// The task's own timeout is the script's budget. Taken on arrival, because fetching the
|
|
189
|
+
// objects below spends the same deadline.
|
|
190
|
+
const due = job.deadline_in_ms === undefined ? undefined : performance.now() + job.deadline_in_ms;
|
|
186
191
|
let resolved: unknown;
|
|
187
192
|
try {
|
|
188
193
|
resolved = await resolveObjects(job);
|
|
@@ -193,7 +198,16 @@ async function run(job: QueueTask, signal: AbortSignal): Promise<void> {
|
|
|
193
198
|
await release(job.token);
|
|
194
199
|
return;
|
|
195
200
|
}
|
|
196
|
-
|
|
201
|
+
let timeoutMs: number | undefined;
|
|
202
|
+
if (due !== undefined) {
|
|
203
|
+
timeoutMs = Math.floor(due - performance.now() - ANSWER_MARGIN_MS);
|
|
204
|
+
if (timeoutMs <= 0) {
|
|
205
|
+
// Too late to run anything: the deadline fires as external.timeout, which is the truth.
|
|
206
|
+
await release(job.token);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const req = asEvalRequest(resolved, timeoutMs);
|
|
197
211
|
if (typeof req === "string") {
|
|
198
212
|
await answer(job.token, {
|
|
199
213
|
error: { code: "compile_error", message: req, data: { name: "BadTaskInput" } },
|