c8ctl-plugin-nano 1.18.0 → 1.18.1
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 -0
- package/c8ctl-plugin.js +58 -4
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -207,6 +207,16 @@ the job with a decremented retry count, and a job that outlives `--job-timeout`
|
|
|
207
207
|
is killed. Profiles are stored in the plugin's `config.json` (see `c8ctl nano
|
|
208
208
|
config`).
|
|
209
209
|
|
|
210
|
+
> **Activation lock vs. kill deadline.** `--job-timeout` is the harness *kill*
|
|
211
|
+
> deadline. The broker's job-activation lock is derived as `--job-timeout +
|
|
212
|
+
> --lock-grace` (grace defaults to `120000`ms) so it strictly outlasts the kill:
|
|
213
|
+
> the worker always reports the outcome (complete/fail) before the lock lapses.
|
|
214
|
+
> Without that gap a lock expiring exactly as the harness dies lets the broker
|
|
215
|
+
> re-activate the still-retryable job (a second agent starts) and the stale
|
|
216
|
+
> `fail` is rejected with a 409 "job cannot be failed in the current state".
|
|
217
|
+
> Raising `--job-timeout` alone does **not** fix this — it moves both coupled
|
|
218
|
+
> deadlines together; widen `--lock-grace` (or keep the default) instead.
|
|
219
|
+
|
|
210
220
|
> **Trust boundary.** The profile `command` is run through a shell so you can
|
|
211
221
|
> write a full invocation (args, pipes, multi-word commands). It is
|
|
212
222
|
> **operator-authored** — only what you put in your own `config.json` is
|
package/c8ctl-plugin.js
CHANGED
|
@@ -1478,6 +1478,46 @@ function parseJobTypeFlags(input) {
|
|
|
1478
1478
|
return { jobTypes, errors };
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
+
/**
|
|
1482
|
+
* The broker job-activation lock must strictly outlast the harness kill
|
|
1483
|
+
* deadline: the worker has to report the outcome (complete/fail) before the lock
|
|
1484
|
+
* lapses, or the broker re-activates the still-retryable job (a second agent
|
|
1485
|
+
* starts) and the stale `fail` is rejected 409 "job cannot be failed in the
|
|
1486
|
+
* current state". So lock = kill + grace. Non-finite or non-positive inputs
|
|
1487
|
+
* fall back to fixed defaults (5m kill / 2m grace). All inputs are coerced to
|
|
1488
|
+
* safe positive integers (positive fractional values floor to at least 1) and
|
|
1489
|
+
* grace is capped so a positive kill always fits and `kill + grace` stays within
|
|
1490
|
+
* the safe-integer range, so the invariant lock > kill holds strictly for every
|
|
1491
|
+
* accepted input — including values at or beyond 2^53 where float addition
|
|
1492
|
+
* would otherwise round `kill + grace` back down to `kill`.
|
|
1493
|
+
*
|
|
1494
|
+
* Returns BOTH derived deadlines from this single computation so the caller
|
|
1495
|
+
* never re-derives (and drifts): `killMs` is the *clamped* harness kill deadline
|
|
1496
|
+
* the caller must actually enforce, and `lockMs` is the broker activation lock.
|
|
1497
|
+
* The caller must use `killMs` — not the raw input — for the harness timeout, or
|
|
1498
|
+
* the lock > kill invariant breaks for large inputs (the raw input can exceed
|
|
1499
|
+
* the clamped `killMs`, and thus reach or exceed `lockMs`).
|
|
1500
|
+
*
|
|
1501
|
+
* @returns {{ killMs: number, lockMs: number }}
|
|
1502
|
+
*/
|
|
1503
|
+
function deriveJobLockMs(jobTimeoutMs, lockGraceMs) {
|
|
1504
|
+
const MAX = Number.MAX_SAFE_INTEGER;
|
|
1505
|
+
const toSafeMs = (value, fallback) => {
|
|
1506
|
+
// Floor positive fractional values to at least 1 so a sub-millisecond input
|
|
1507
|
+
// (e.g. 0.5) never collapses to a non-positive value.
|
|
1508
|
+
const n = Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
|
|
1509
|
+
return Math.min(n, MAX);
|
|
1510
|
+
};
|
|
1511
|
+
// Cap grace to MAX - 1 so there is always room for a positive kill while
|
|
1512
|
+
// keeping kill + grace within the safe-integer range.
|
|
1513
|
+
const grace = Math.min(toSafeMs(lockGraceMs, 2 * 60_000), MAX - 1);
|
|
1514
|
+
// Cap kill so kill + grace stays a safe integer and floor it at 1 so the
|
|
1515
|
+
// internal kill is always positive; the sum is then exact and strictly
|
|
1516
|
+
// greater than kill (never equal to it via float rounding).
|
|
1517
|
+
const killMs = Math.max(1, Math.min(toSafeMs(jobTimeoutMs, 5 * 60_000), MAX - grace));
|
|
1518
|
+
return { killMs, lockMs: killMs + grace };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1481
1521
|
/** A profile name must be a safe, filesystem/token-friendly slug. */
|
|
1482
1522
|
function isValidProfileName(name) {
|
|
1483
1523
|
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
|
|
@@ -2679,6 +2719,17 @@ async function workAgent(req, flags) {
|
|
|
2679
2719
|
};
|
|
2680
2720
|
const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
|
|
2681
2721
|
const jobTimeoutMs = intFlag(flags?.['job-timeout'], 5 * 60_000);
|
|
2722
|
+
// The broker's job-activation lock MUST outlast the harness kill deadline: the
|
|
2723
|
+
// worker has to report the outcome (complete/fail) before the lock lapses,
|
|
2724
|
+
// otherwise the broker re-activates the still-retryable job (a second agent
|
|
2725
|
+
// starts) and the stale `fail` is rejected with a 409 "job cannot be failed in
|
|
2726
|
+
// the current state". So lock = harness kill + grace. Raising --job-timeout
|
|
2727
|
+
// alone does not help — it moves both coupled deadlines together.
|
|
2728
|
+
const lockGraceMs = intFlag(flags?.['lock-grace'], 2 * 60_000);
|
|
2729
|
+
// deriveJobLockMs is the single source of truth for both deadlines: the harness
|
|
2730
|
+
// MUST enforce the clamped `jobKillMs` (not the raw --job-timeout), or a very
|
|
2731
|
+
// large --job-timeout would outlive the broker lock and re-break the invariant.
|
|
2732
|
+
const { killMs: jobKillMs, lockMs: jobLockMs } = deriveJobLockMs(jobTimeoutMs, lockGraceMs);
|
|
2682
2733
|
|
|
2683
2734
|
// Sandbox: flag overrides the stored profile default. `none` runs on the host
|
|
2684
2735
|
// (legacy); `docker`/`podman` run each job in a throwaway labelled container.
|
|
@@ -2786,7 +2837,7 @@ async function workAgent(req, flags) {
|
|
|
2786
2837
|
if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
|
|
2787
2838
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
2788
2839
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
2789
|
-
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${
|
|
2840
|
+
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms`);
|
|
2790
2841
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
2791
2842
|
|
|
2792
2843
|
const workers = jobTypes.map((jobType) =>
|
|
@@ -2794,7 +2845,7 @@ async function workAgent(req, flags) {
|
|
|
2794
2845
|
jobType,
|
|
2795
2846
|
workerName: `${name}:${jobType}`,
|
|
2796
2847
|
maxParallelJobs,
|
|
2797
|
-
jobTimeoutMs,
|
|
2848
|
+
jobTimeoutMs: jobLockMs,
|
|
2798
2849
|
jobHandler: async (job) => {
|
|
2799
2850
|
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
2800
2851
|
|
|
@@ -2877,7 +2928,7 @@ async function workAgent(req, flags) {
|
|
|
2877
2928
|
} catch { resultDir = null; resultFile = null; }
|
|
2878
2929
|
|
|
2879
2930
|
result = await runAgentJob(profile, job, {
|
|
2880
|
-
timeoutMs:
|
|
2931
|
+
timeoutMs: jobKillMs,
|
|
2881
2932
|
envelope,
|
|
2882
2933
|
sandbox,
|
|
2883
2934
|
image,
|
|
@@ -4358,6 +4409,7 @@ export {
|
|
|
4358
4409
|
normalizeStoredProfile,
|
|
4359
4410
|
jobTypeMatrix,
|
|
4360
4411
|
parseJobTypeFlags,
|
|
4412
|
+
deriveJobLockMs,
|
|
4361
4413
|
AGENT_TASK_NS,
|
|
4362
4414
|
AGENT_RESULT_KEY,
|
|
4363
4415
|
RESULT_SENTINEL,
|
|
@@ -4462,6 +4514,7 @@ export const commands = {
|
|
|
4462
4514
|
list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
|
|
4463
4515
|
'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
|
|
4464
4516
|
'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
|
|
4517
|
+
'lock-grace': { type: 'string', description: 'work: extra ms added to --job-timeout to derive the broker activation lock, so the worker reports before the lock lapses (default 120000)' },
|
|
4465
4518
|
'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
|
|
4466
4519
|
},
|
|
4467
4520
|
handler: async (args, flags) => {
|
|
@@ -4606,7 +4659,7 @@ function printUsage() {
|
|
|
4606
4659
|
console.log(' c8ctl nano config');
|
|
4607
4660
|
console.log(' c8ctl nano update [--check]');
|
|
4608
4661
|
console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--arg <switch> ...] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
|
|
4609
|
-
console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--job-timeout <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
|
|
4662
|
+
console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--job-timeout <ms>] [--lock-grace <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
|
|
4610
4663
|
console.log('');
|
|
4611
4664
|
console.log('Subcommands:');
|
|
4612
4665
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -4649,6 +4702,7 @@ function printUsage() {
|
|
|
4649
4702
|
console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
|
|
4650
4703
|
console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
|
|
4651
4704
|
console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
|
|
4705
|
+
console.log(' --lock-grace <ms> work: extra ms over --job-timeout for the broker activation lock (default 120000)');
|
|
4652
4706
|
console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
|
|
4653
4707
|
console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
|
|
4654
4708
|
console.log(' --reap-interval <ms> work: how often to sweep finished agent containers/workspaces (default 300000)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"semantic-release": "^25.0.3"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.18.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.18.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.18.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.18.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.18.
|
|
57
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.18.
|
|
58
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.18.
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.18.1",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.18.1",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.18.1",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.18.1",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.18.1",
|
|
57
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.18.1",
|
|
58
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.18.1"
|
|
59
59
|
}
|
|
60
60
|
}
|