c8ctl-plugin-nano 1.44.7 → 1.44.8
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/c8ctl-plugin.js +77 -1
- package/package.json +8 -8
package/c8ctl-plugin.js
CHANGED
|
@@ -135,6 +135,11 @@ const READINESS_TIMEOUT_MS = 60_000;
|
|
|
135
135
|
const READINESS_POLL_MS = 500;
|
|
136
136
|
const HEALTH_TIMEOUT_MS = 1_500;
|
|
137
137
|
const STOP_GRACE_MS = 8_000;
|
|
138
|
+
// Backoff applied when a poller fails a lease fast because the worker is already
|
|
139
|
+
// running another job (issue #142 single-flight). Long enough that the deferred
|
|
140
|
+
// job doesn't tight-loop re-activating while the first runs, short enough that it
|
|
141
|
+
// is picked up promptly once the worker frees up.
|
|
142
|
+
const WORKER_BUSY_RETRY_BACKOFF_MS = 5_000;
|
|
138
143
|
// Upper bound on one `--auto` engine-read reconcile (enumerate deployed
|
|
139
144
|
// definitions + fetch each BPMN). A read that stalls past this is treated as a
|
|
140
145
|
// transient failure so the running poller set is KEPT and, crucially, shutdown
|
|
@@ -5164,6 +5169,44 @@ function baseAgentEnv(profile, job) {
|
|
|
5164
5169
|
};
|
|
5165
5170
|
}
|
|
5166
5171
|
|
|
5172
|
+
/**
|
|
5173
|
+
* Process-wide single-flight guard (issue #142).
|
|
5174
|
+
*
|
|
5175
|
+
* `maxParallelJobs = 1` only caps concurrency WITHIN one job-type poller, but a
|
|
5176
|
+
* single `work` process runs one poller per job type (rank×capability matrix, or
|
|
5177
|
+
* every deployed agent type under `--auto`). Without a shared gate a worker
|
|
5178
|
+
* serving N job types could lease and run up to N jobs at once — each holding its
|
|
5179
|
+
* own PTY + git workspace + broker lock-extender — the exact failure the
|
|
5180
|
+
* "one job per worker" invariant exists to prevent.
|
|
5181
|
+
*
|
|
5182
|
+
* This is a capacity-1, non-blocking mutex shared by EVERY per-type poller: the
|
|
5183
|
+
* first poller to `tryAcquire()` runs its job to completion (releasing in a
|
|
5184
|
+
* `finally`); any other poller that finds the permit already held must NOT begin
|
|
5185
|
+
* a second job (the caller fails the lease fast so the broker re-queues it rather
|
|
5186
|
+
* than leaving it "claimed but idle"). `tryAcquire`/`release` are synchronous
|
|
5187
|
+
* check-and-set, so the single-threaded event loop makes them race-free across
|
|
5188
|
+
* the concurrently-invoked async job handlers.
|
|
5189
|
+
*/
|
|
5190
|
+
function createSingleFlight() {
|
|
5191
|
+
let held = false;
|
|
5192
|
+
return {
|
|
5193
|
+
/** Take the permit if free; returns false when a job is already in flight. */
|
|
5194
|
+
tryAcquire() {
|
|
5195
|
+
if (held) return false;
|
|
5196
|
+
held = true;
|
|
5197
|
+
return true;
|
|
5198
|
+
},
|
|
5199
|
+
/** Release the permit. Idempotent: redundant calls are safe no-ops, though the normal path releases once per acquire (in a `finally`). */
|
|
5200
|
+
release() {
|
|
5201
|
+
held = false;
|
|
5202
|
+
},
|
|
5203
|
+
/** True while a job holds the permit. */
|
|
5204
|
+
get busy() {
|
|
5205
|
+
return held;
|
|
5206
|
+
},
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5167
5210
|
/**
|
|
5168
5211
|
* Keep a leased job's broker activation lock ahead of *now* while the harness is
|
|
5169
5212
|
* running, so a long agent run never has its lock lapse and get re-activated (a
|
|
@@ -6266,6 +6309,13 @@ async function workAgent(req, flags) {
|
|
|
6266
6309
|
// SDK derives maxJobsToActivate = maxParallelJobs - activeJobs, so 1 means
|
|
6267
6310
|
// "activate one job, then stop polling until it completes".
|
|
6268
6311
|
const maxParallelJobs = 1;
|
|
6312
|
+
// Process-wide single-flight guard (issue #142). The SDK's maxParallelJobs=1
|
|
6313
|
+
// only serializes ONE job-type poller, but this process runs one poller per
|
|
6314
|
+
// job type, so nothing stops N pollers from each leasing + running a job
|
|
6315
|
+
// concurrently. This capacity-1 mutex, shared by every poller's jobHandler,
|
|
6316
|
+
// enforces the real "one job per worker" invariant: while any job is in flight
|
|
6317
|
+
// on any job type, no other poller starts a second one.
|
|
6318
|
+
const singleFlight = createSingleFlight();
|
|
6269
6319
|
// The broker job-activation lock is NOT hardcoded up front. A fixed timeout is
|
|
6270
6320
|
// impossible to size for an agent: too short reclaims a still-working job (a
|
|
6271
6321
|
// second agent starts + the stale complete/fail is rejected 409), too long
|
|
@@ -6465,7 +6515,7 @@ async function workAgent(req, flags) {
|
|
|
6465
6515
|
}
|
|
6466
6516
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
6467
6517
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
6468
|
-
logger.info(` one job per worker; recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
|
|
6518
|
+
logger.info(` one job per worker (single-flight across all ${jobTypes.length} job type(s)); recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
|
|
6469
6519
|
// Warm the gh-token cache now, off the job-handling path: githubCloneToken()
|
|
6470
6520
|
// may consult `gh auth token` (a synchronous spawn, up to 10s) as its default
|
|
6471
6521
|
// credential fallback, and doing that inside a job handler would block the
|
|
@@ -6753,6 +6803,27 @@ async function workAgent(req, flags) {
|
|
|
6753
6803
|
jobTimeoutMs: recoveryWindowMs,
|
|
6754
6804
|
pollTimeoutMs,
|
|
6755
6805
|
jobHandler: async (job) => {
|
|
6806
|
+
// Process-wide single-flight (issue #142): if another job is already
|
|
6807
|
+
// running on ANY poller, do not start a second harness. Fail this lease
|
|
6808
|
+
// FAST — before recording it active, extending its lock, or provisioning
|
|
6809
|
+
// anything — so the broker re-queues it (retries preserved) instead of it
|
|
6810
|
+
// sitting "claimed but idle" while the first job runs. Gating here, at the
|
|
6811
|
+
// point activation surfaces as a handler call, is the cross-poller gate
|
|
6812
|
+
// the per-type maxParallelJobs cannot provide.
|
|
6813
|
+
if (!singleFlight.tryAcquire()) {
|
|
6814
|
+
// Not a failure — preserve the broker-provided retries verbatim so
|
|
6815
|
+
// re-dispatch doesn't decrement (or resurrect) the job. Keep a real 0
|
|
6816
|
+
// as 0 (an already-incidentable job must stay that way); only default
|
|
6817
|
+
// to 1 when the count is missing/invalid.
|
|
6818
|
+
const rawRetries = Number(job.retries);
|
|
6819
|
+
const retries = Number.isInteger(rawRetries) && rawRetries >= 0 ? rawRetries : 1;
|
|
6820
|
+
logger.info(`[${jobType}] job ${job.jobKey} deferred — worker already running another job; releasing lease for re-dispatch.`);
|
|
6821
|
+
return job.fail({
|
|
6822
|
+
errorMessage: 'worker busy: one job per worker (single-flight across all job types)',
|
|
6823
|
+
retries,
|
|
6824
|
+
retryBackOff: WORKER_BUSY_RETRY_BACKOFF_MS,
|
|
6825
|
+
});
|
|
6826
|
+
}
|
|
6756
6827
|
recordJobStart(job, jobType);
|
|
6757
6828
|
// Auto-extend the broker lock for the whole life of this job (harness run
|
|
6758
6829
|
// + git finalize + complete/fail), stopped in the outer finally. The lock
|
|
@@ -7091,6 +7162,10 @@ async function workAgent(req, flags) {
|
|
|
7091
7162
|
} finally {
|
|
7092
7163
|
stopLockExtender();
|
|
7093
7164
|
recordJobEnd(job);
|
|
7165
|
+
// Release the process-wide single-flight permit LAST, once this job's
|
|
7166
|
+
// lock-extender is stopped and its bookkeeping cleared, so another
|
|
7167
|
+
// poller can only begin after this job is fully settled.
|
|
7168
|
+
singleFlight.release();
|
|
7094
7169
|
}
|
|
7095
7170
|
},
|
|
7096
7171
|
});
|
|
@@ -11731,6 +11806,7 @@ export {
|
|
|
11731
11806
|
parsePsTime,
|
|
11732
11807
|
ensureAcpFlag,
|
|
11733
11808
|
startLockExtender,
|
|
11809
|
+
createSingleFlight,
|
|
11734
11810
|
provisionRepo,
|
|
11735
11811
|
finalizeGit,
|
|
11736
11812
|
describeGitFailure,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.44.
|
|
3
|
+
"version": "1.44.8",
|
|
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",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"node-pty": "^1.0.0",
|
|
60
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.8",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.8",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.8",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.8",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.8",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.8",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.8"
|
|
67
67
|
}
|
|
68
68
|
}
|