@bastani/atomic 0.9.61-alpha.2 → 0.9.61-alpha.3
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/CHANGELOG.md +16 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +49 -9
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +61 -10
- package/dist/builtin/workflows/src/index.js +49 -9
- package/dist/builtin/workflows/src/local-worker-entry.mjs +61 -10
- package/dist/modes/interactive-engine/engine-render-service.d.ts +8 -0
- package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-render-service.js +28 -6
- package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
- package/dist/modes/interactive-engine/remote-renderer.d.ts +6 -0
- package/dist/modes/interactive-engine/remote-renderer.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-renderer.js +17 -3
- package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
- package/dist/modes/rpc/queued-writer.d.ts +2 -0
- package/dist/modes/rpc/queued-writer.d.ts.map +1 -1
- package/dist/modes/rpc/queued-writer.js +8 -0
- package/dist/modes/rpc/queued-writer.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +4 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/docs/workflows/operations.md +1 -1
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Local worker pools that hold up under faults and load, a lighter terminal under heavy workflow load, and workflows that can work in any repository's worktree.
|
|
8
8
|
|
|
9
|
-
- **One failing child no longer takes down a pool:** a child that fails to dispatch fails alone, a worker or tool process group that survives SIGKILL is a warning rather than a failure, and pool faults replay the child within its three attempts ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
|
|
9
|
+
- **One failing child no longer takes down a pool:** a child that fails to dispatch fails alone, a worker or tool process group that survives SIGKILL is a warning rather than a failure, and pool faults replay the child within its three attempts, and a replay blocked by those groups' quarantined permits fails after a bounded wait instead of waiting forever ([#4159](https://github.com/bastani-inc/atomic/pull/4159), [#4170](https://github.com/bastani-inc/atomic/pull/4170))
|
|
10
10
|
- **Local worker pools keep their leases:** a slow journal or root no longer gets every healthy worker killed at once, and a race in lease recovery no longer fails every child of a pool with `stale_work_lease` ([#4165](https://github.com/bastani-inc/atomic/pull/4165))
|
|
11
|
-
- **A lighter terminal under heavy load:** a workflow run no longer keeps every completed stage's session in memory until it ends, which cut peak heap from 2.8 GB to 0.87 GB in a 600-stage test and shortened GC pauses. Rebuilding the chat no longer flashes "Loading isolated renderer…" ([#4166](https://github.com/bastani-inc/atomic/pull/4166), [#4167](https://github.com/bastani-inc/atomic/pull/4167))
|
|
11
|
+
- **A lighter terminal under heavy load:** a workflow run no longer keeps every completed stage's session in memory until it ends, which cut peak heap from 2.8 GB to 0.87 GB in a 600-stage test and shortened GC pauses. Rebuilding the chat no longer flashes "Loading isolated renderer…", and rows keep the state you left them in ([#4166](https://github.com/bastani-inc/atomic/pull/4166), [#4167](https://github.com/bastani-inc/atomic/pull/4167), [#4169](https://github.com/bastani-inc/atomic/pull/4169))
|
|
12
12
|
- **Broadcasts reach pooled stages at once:** stages on local workers get queued and sticky Intercom messages before their first turn, without a 60 s stall or a warning per stage ([#4164](https://github.com/bastani-inc/atomic/pull/4164))
|
|
13
13
|
- **Content-filter refusals fall back:** a provider content-filter refusal, such as OpenAI's 422 `cyber_policy`, moves to the next `fallbackModels` entry instead of retrying the model that refused; turn it off with `retry.fallbackOnContentFilter` ([#4163](https://github.com/bastani-inc/atomic/pull/4163))
|
|
14
14
|
- **Workflow worktrees from anywhere:** `git_worktree_dir` can name a worktree of any repository, and a non-empty folder gets its own checkout inside it ([#4162](https://github.com/bastani-inc/atomic/pull/4162))
|
|
@@ -36,6 +36,20 @@ Local worker pools that hold up under faults and load, a lighter terminal under
|
|
|
36
36
|
- A stage on a local worker (`workflows.localWorkers`) that starts after a sticky Intercom broadcast (`workflow:<rootRunId>/**`) or a name or pattern send to future stages gets those messages before its first turn, under "Messages received before you started" with the real sender, even when the root is too busy to answer. Before, each such stage on a busy root could wait 60 seconds and then print a long "starting without its queued Intercom messages" warning in the chat, listing messages it may already have received. The worker now delivers the stage's queued messages itself and never waits on the root. If the root later fails to record or deliver them, the run's status shows one `Intercom delivery:` line for the whole run, not a warning per stage ([#4164](https://github.com/bastani-inc/atomic/pull/4164)).
|
|
37
37
|
- A workflow run no longer keeps the live session of every stage it completed in memory until the whole run ends. A stage run with `ctx.task`, `ctx.chain` or `ctx.parallel` gives its session back as soon as it completes, and one run with `ctx.stage` once the workflow no longer holds its stage context; its saved transcript still opens from the graph, `/workflow attach` or the stage chat. A session hosting long runs with hundreds of completed stages held gigabytes this way, and the garbage collection that memory caused made the interactive TUI, `/tasks` and queued messages slow to respond. ([#4166](https://github.com/bastani-inc/atomic/pull/4166))
|
|
38
38
|
- In the interactive TUI, rows that the engine renders, such as tool results and extension messages, keep their last frame when the chat is rebuilt (after compaction, `/reload` or switching sessions), instead of showing `Loading isolated renderer…` until a busy engine renders each one again. A rebuilt chat also no longer leaves the renderers of the rows it replaced running in the engine for the rest of the session, and engine messages reach chat rows through one subscription instead of one per row. ([#4167](https://github.com/bastani-inc/atomic/pull/4167))
|
|
39
|
+
- A rebuilt chat in the interactive TUI no longer briefly shows a collapsed tool row with its expanded output when the row was collapsed while its expanded frame was still on its way from the engine. Rows the chat drops while the engine connection is backed up also stay released, instead of being rendered again in the engine by a render request that arrived after their disposal. ([#4169](https://github.com/bastani-inc/atomic/pull/4169))
|
|
40
|
+
- A child on a local worker pool (`workflows.localWorkers`) whose replay needs a tool process permit no longer waits forever when the only capacity left under `admission.limits.process` (or `admission.userLimits.process`) is held by quarantined permits of process groups that survived SIGKILL. Those permits stay quarantined, and capacity held by running work is still waited for as before. A process admission that only quarantined permits block now waits at most 15 seconds for those groups to exit, then fails with the retryable `worker_cleanup_unconfirmed`, naming the groups and the limit. That ends the child's attempt, so it replays within its three attempts and then fails with `worker_retry_limit`, letting the root finish. If the groups exit in time, the replay goes on normally. ([#4170](https://github.com/bastani-inc/atomic/pull/4170))
|
|
41
|
+
|
|
42
|
+
## [0.9.61-alpha.3] - 2026-11-02
|
|
43
|
+
|
|
44
|
+
Replays on a local worker that can no longer wait forever, and a steadier chat renderer.
|
|
45
|
+
|
|
46
|
+
- **Replays can't wait forever on cleanup:** when process groups that survived SIGKILL fill `admission.limits.process`, a child's replay now fails after a bounded wait with a retryable error naming those groups and the limit, instead of waiting indefinitely; the groups' permits stay quarantined ([#4170](https://github.com/bastani-inc/atomic/pull/4170))
|
|
47
|
+
- **Chat rows stay as you left them:** a row released under engine backpressure no longer comes back, and a row you collapse no longer shows its expanded output after the chat is rebuilt ([#4169](https://github.com/bastani-inc/atomic/pull/4169))
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- A rebuilt chat in the interactive TUI no longer briefly shows a collapsed tool row with its expanded output when the row was collapsed while its expanded frame was still on its way from the engine. Rows the chat drops while the engine connection is backed up also stay released, instead of being rendered again in the engine by a render request that arrived after their disposal. ([#4169](https://github.com/bastani-inc/atomic/pull/4169))
|
|
52
|
+
- A child on a local worker pool (`workflows.localWorkers`) whose replay needs a tool process permit no longer waits forever when the only capacity left under `admission.limits.process` (or `admission.userLimits.process`) is held by quarantined permits of process groups that survived SIGKILL. Those permits stay quarantined, and capacity held by running work is still waited for as before. A process admission that only quarantined permits block now waits at most 15 seconds for those groups to exit, then fails with the retryable `worker_cleanup_unconfirmed`, naming the groups and the limit. That ends the child's attempt, so it replays within its three attempts and then fails with `worker_retry_limit`, letting the root finish. If the groups exit in time, the replay goes on normally. ([#4170](https://github.com/bastani-inc/atomic/pull/4170))
|
|
39
53
|
|
|
40
54
|
## [0.9.61-alpha.2] - 2026-11-02
|
|
41
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/intercom",
|
|
3
|
-
"version": "0.9.61-alpha.
|
|
3
|
+
"version": "0.9.61-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.61-alpha.
|
|
3
|
+
"version": "0.9.61-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.61-alpha.
|
|
3
|
+
"version": "0.9.61-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -20723,6 +20723,7 @@ class SqlResourceAdmission {
|
|
|
20723
20723
|
epoch: this.options.attempt.epoch
|
|
20724
20724
|
}) : null);
|
|
20725
20725
|
});
|
|
20726
|
+
let quarantinedSince;
|
|
20726
20727
|
try {
|
|
20727
20728
|
for (;; ) {
|
|
20728
20729
|
signal?.throwIfAborted();
|
|
@@ -20732,7 +20733,9 @@ class SqlResourceAdmission {
|
|
|
20732
20733
|
}
|
|
20733
20734
|
const hostReason = this.hostReason();
|
|
20734
20735
|
const pressure = this.stageStartPressure(kind, request?.delegated);
|
|
20736
|
+
let quarantined = undefined;
|
|
20735
20737
|
const granted = await this.storage.transaction(async (tx) => {
|
|
20738
|
+
quarantined = undefined;
|
|
20736
20739
|
await tx.run(`UPDATE ${this.owners} SET expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + ? WHERE id = ?`, OWNER_LEASE_MS, this.owner);
|
|
20737
20740
|
const { root } = this.options;
|
|
20738
20741
|
if (!await this.assertCurrentAttempt(tx)) {
|
|
@@ -20750,6 +20753,8 @@ class SqlResourceAdmission {
|
|
|
20750
20753
|
if (control && control.state !== "running")
|
|
20751
20754
|
reason ??= `root ${control.state}`;
|
|
20752
20755
|
}
|
|
20756
|
+
const held = [];
|
|
20757
|
+
let blockedOtherwise = reason !== undefined;
|
|
20753
20758
|
for (const scope of [`root:${root}`, "user"]) {
|
|
20754
20759
|
const row = await tx.get(`SELECT config FROM ${this.configuration} WHERE scope = ?`, scope);
|
|
20755
20760
|
const limit = row ? JSON.parse(String(row.config))[kind] : undefined;
|
|
@@ -20759,9 +20764,21 @@ class SqlResourceAdmission {
|
|
|
20759
20764
|
const args = scope === "user" ? [kind] : [kind, root];
|
|
20760
20765
|
const count = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted'`, ...args);
|
|
20761
20766
|
const older = await tx.get(`SELECT 1 AS waiting FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'waiting' AND reason IS NOT ? AND ticket < (SELECT ticket FROM ${this.permits} WHERE operation = ?) LIMIT 1`, ...args, STAGE_START_PRESSURE_REASON, operation);
|
|
20762
|
-
|
|
20763
|
-
|
|
20767
|
+
const used = Number(count?.count);
|
|
20768
|
+
if (used < limit && !older)
|
|
20769
|
+
continue;
|
|
20770
|
+
reason ??= `${scope} ${kind}`;
|
|
20771
|
+
const quarantinedPermits = kind === "process" && used >= limit ? await tx.all(`SELECT process_identity FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted' AND reason = ?`, ...args, QUARANTINED_REASON) : [];
|
|
20772
|
+
if (quarantinedPermits.length > 0 && used - quarantinedPermits.length < limit)
|
|
20773
|
+
held.push({
|
|
20774
|
+
limit: `${scope === "user" ? "admission.userLimits" : "admission.limits"}.${kind} (${limit})`,
|
|
20775
|
+
groups: quarantinedPermits.flatMap(({ process_identity }) => process_identity === null ? [] : [JSON.parse(process_identity).id])
|
|
20776
|
+
});
|
|
20777
|
+
else
|
|
20778
|
+
blockedOtherwise = true;
|
|
20764
20779
|
}
|
|
20780
|
+
if (!blockedOtherwise && held.length > 0)
|
|
20781
|
+
quarantined = held;
|
|
20765
20782
|
if (reason && request?.delegated)
|
|
20766
20783
|
throw new Error(`Delegated session cannot be admitted without capacity headroom: ${reason}`);
|
|
20767
20784
|
const stageStart = kind === "session" && request?.delegated !== true;
|
|
@@ -20774,6 +20791,14 @@ class SqlResourceAdmission {
|
|
|
20774
20791
|
signal?.throwIfAborted();
|
|
20775
20792
|
return release;
|
|
20776
20793
|
}
|
|
20794
|
+
if (quarantined === undefined)
|
|
20795
|
+
quarantinedSince = undefined;
|
|
20796
|
+
else {
|
|
20797
|
+
const now = performance.now();
|
|
20798
|
+
quarantinedSince ??= now;
|
|
20799
|
+
if (now - quarantinedSince >= QUARANTINED_CAPACITY_WAIT_MS)
|
|
20800
|
+
throw new QuarantinedCapacityError(quarantined, now - quarantinedSince);
|
|
20801
|
+
}
|
|
20777
20802
|
await delay(50 + Math.floor(Math.random() * 25), undefined, { signal });
|
|
20778
20803
|
}
|
|
20779
20804
|
} catch (error) {
|
|
@@ -20815,9 +20840,9 @@ class SqlResourceAdmission {
|
|
|
20815
20840
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'host reboot confirmed' WHERE owner = ? AND state = 'granted'`, owner.id);
|
|
20816
20841
|
} else if (stopped) {
|
|
20817
20842
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'owner death confirmed' WHERE owner = ? AND kind = 'session' AND state = 'granted'`, owner.id);
|
|
20818
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON);
|
|
20843
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON, QUARANTINED_REASON);
|
|
20819
20844
|
} else {
|
|
20820
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, owner.id);
|
|
20845
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, QUARANTINED_REASON, owner.id);
|
|
20821
20846
|
}
|
|
20822
20847
|
if (owner.host === admissionHost && process.platform !== "win32") {
|
|
20823
20848
|
const processes = await tx.all(`SELECT * FROM ${this.permits} WHERE owner = ? AND state = 'granted' AND kind = 'process' AND process_identity IS NOT NULL`, owner.id);
|
|
@@ -20845,9 +20870,10 @@ class SqlResourceAdmission {
|
|
|
20845
20870
|
return true;
|
|
20846
20871
|
if (permit.state === "waiting")
|
|
20847
20872
|
return stopped || !!permit.expired;
|
|
20848
|
-
|
|
20873
|
+
const quarantined = permit.reason === QUARANTINED_REASON;
|
|
20874
|
+
if (stopped && (permit.kind === "session" || permit.reason !== UNCONFIRMED_REASON && !quarantined))
|
|
20849
20875
|
return true;
|
|
20850
|
-
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON)
|
|
20876
|
+
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON && !quarantined)
|
|
20851
20877
|
return true;
|
|
20852
20878
|
return permit.kind === "process" && permit.host === admissionHost && process.platform !== "win32" && permit.process_identity !== null && processGroupStopped(permit.process_identity);
|
|
20853
20879
|
}
|
|
@@ -20858,7 +20884,9 @@ class SqlResourceAdmission {
|
|
|
20858
20884
|
await this.reconcile();
|
|
20859
20885
|
return this.storage.transaction(async (tx) => {
|
|
20860
20886
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'worker exit confirmed' WHERE root = ? AND holder = ? AND state IN ('waiting', 'granted') AND (kind = 'session' OR state = 'waiting')`, this.options.root, holder);
|
|
20861
|
-
|
|
20887
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, this.options.root, holder, QUARANTINED_REASON);
|
|
20888
|
+
const held = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted'`, this.options.root, holder);
|
|
20889
|
+
return Number(held?.count);
|
|
20862
20890
|
});
|
|
20863
20891
|
}
|
|
20864
20892
|
async reapStoppedProcesses(holder) {
|
|
@@ -20905,10 +20933,13 @@ async function settleProcessGroups(storage, root, scope, reap) {
|
|
|
20905
20933
|
survivors = await settle(REAP_KILL_GRACE_MS);
|
|
20906
20934
|
}
|
|
20907
20935
|
const confirmed = [...groups].flatMap(([group, operations]) => survivors.includes(group) ? [] : operations);
|
|
20908
|
-
|
|
20936
|
+
const quarantined = reap ? survivors.flatMap((group) => groups.get(group) ?? []) : [];
|
|
20937
|
+
if (confirmed.length > 0 || quarantined.length > 0)
|
|
20909
20938
|
await storage.transaction(async (tx) => {
|
|
20910
20939
|
for (const operation of confirmed)
|
|
20911
20940
|
await tx.run(`UPDATE ${permits} SET state = 'released', reason = 'process group cleanup confirmed' WHERE operation = ? AND state = 'granted'`, operation);
|
|
20941
|
+
for (const operation of quarantined)
|
|
20942
|
+
await tx.run(`UPDATE ${permits} SET reason = ? WHERE operation = ? AND state = 'granted'`, QUARANTINED_REASON, operation);
|
|
20912
20943
|
});
|
|
20913
20944
|
return { survivors, unprobeable };
|
|
20914
20945
|
}
|
|
@@ -20925,11 +20956,20 @@ async function reapProcessGroups(journal, root, scope) {
|
|
|
20925
20956
|
return { survivors: [], unprobeable: [] };
|
|
20926
20957
|
return settleProcessGroups(storage, root, scope, true);
|
|
20927
20958
|
}
|
|
20928
|
-
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
20959
|
+
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", QUARANTINED_REASON = "cleanup unconfirmed; permit quarantined", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", QUARANTINED_CAPACITY_WAIT_MS, QuarantinedCapacityError, NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
20929
20960
|
var init_resource_admission = __esm(() => {
|
|
20930
20961
|
init_internal();
|
|
20931
20962
|
init_sql_run_journal();
|
|
20963
|
+
init_workflow_infrastructure_error();
|
|
20932
20964
|
init_admission_host();
|
|
20965
|
+
QUARANTINED_CAPACITY_WAIT_MS = REAP_TERM_GRACE_MS + REAP_KILL_GRACE_MS;
|
|
20966
|
+
QuarantinedCapacityError = class QuarantinedCapacityError extends WorkflowInfrastructureError {
|
|
20967
|
+
constructor(capacity, waitedMs) {
|
|
20968
|
+
const limits = capacity.map(({ limit }) => limit).join(" and ");
|
|
20969
|
+
const groups = [...new Set(capacity.flatMap(({ groups }) => groups))].join(", ");
|
|
20970
|
+
super("worker_cleanup_unconfirmed", `Local worker process cleanup unconfirmed; process permit refused after ${Math.round(waitedMs)} ms: ${limits} ${capacity.length === 1 ? "is" : "are"} held by quarantined permits of process groups ${groups} that survived SIGKILL`);
|
|
20971
|
+
}
|
|
20972
|
+
};
|
|
20933
20973
|
});
|
|
20934
20974
|
|
|
20935
20975
|
// dist/builtin/workflows/src/workers/admission-binding.ts
|
|
@@ -17462,6 +17462,7 @@ class SqlResourceAdmission {
|
|
|
17462
17462
|
epoch: this.options.attempt.epoch
|
|
17463
17463
|
}) : null);
|
|
17464
17464
|
});
|
|
17465
|
+
let quarantinedSince;
|
|
17465
17466
|
try {
|
|
17466
17467
|
for (;; ) {
|
|
17467
17468
|
signal?.throwIfAborted();
|
|
@@ -17471,7 +17472,9 @@ class SqlResourceAdmission {
|
|
|
17471
17472
|
}
|
|
17472
17473
|
const hostReason = this.hostReason();
|
|
17473
17474
|
const pressure = this.stageStartPressure(kind, request?.delegated);
|
|
17475
|
+
let quarantined = undefined;
|
|
17474
17476
|
const granted = await this.storage.transaction(async (tx) => {
|
|
17477
|
+
quarantined = undefined;
|
|
17475
17478
|
await tx.run(`UPDATE ${this.owners} SET expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + ? WHERE id = ?`, OWNER_LEASE_MS, this.owner);
|
|
17476
17479
|
const { root } = this.options;
|
|
17477
17480
|
if (!await this.assertCurrentAttempt(tx)) {
|
|
@@ -17489,6 +17492,8 @@ class SqlResourceAdmission {
|
|
|
17489
17492
|
if (control && control.state !== "running")
|
|
17490
17493
|
reason ??= `root ${control.state}`;
|
|
17491
17494
|
}
|
|
17495
|
+
const held = [];
|
|
17496
|
+
let blockedOtherwise = reason !== undefined;
|
|
17492
17497
|
for (const scope of [`root:${root}`, "user"]) {
|
|
17493
17498
|
const row = await tx.get(`SELECT config FROM ${this.configuration} WHERE scope = ?`, scope);
|
|
17494
17499
|
const limit = row ? JSON.parse(String(row.config))[kind] : undefined;
|
|
@@ -17498,9 +17503,21 @@ class SqlResourceAdmission {
|
|
|
17498
17503
|
const args = scope === "user" ? [kind] : [kind, root];
|
|
17499
17504
|
const count = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted'`, ...args);
|
|
17500
17505
|
const older = await tx.get(`SELECT 1 AS waiting FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'waiting' AND reason IS NOT ? AND ticket < (SELECT ticket FROM ${this.permits} WHERE operation = ?) LIMIT 1`, ...args, STAGE_START_PRESSURE_REASON, operation);
|
|
17501
|
-
|
|
17502
|
-
|
|
17506
|
+
const used = Number(count?.count);
|
|
17507
|
+
if (used < limit && !older)
|
|
17508
|
+
continue;
|
|
17509
|
+
reason ??= `${scope} ${kind}`;
|
|
17510
|
+
const quarantinedPermits = kind === "process" && used >= limit ? await tx.all(`SELECT process_identity FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted' AND reason = ?`, ...args, QUARANTINED_REASON) : [];
|
|
17511
|
+
if (quarantinedPermits.length > 0 && used - quarantinedPermits.length < limit)
|
|
17512
|
+
held.push({
|
|
17513
|
+
limit: `${scope === "user" ? "admission.userLimits" : "admission.limits"}.${kind} (${limit})`,
|
|
17514
|
+
groups: quarantinedPermits.flatMap(({ process_identity }) => process_identity === null ? [] : [JSON.parse(process_identity).id])
|
|
17515
|
+
});
|
|
17516
|
+
else
|
|
17517
|
+
blockedOtherwise = true;
|
|
17503
17518
|
}
|
|
17519
|
+
if (!blockedOtherwise && held.length > 0)
|
|
17520
|
+
quarantined = held;
|
|
17504
17521
|
if (reason && request?.delegated)
|
|
17505
17522
|
throw new Error(`Delegated session cannot be admitted without capacity headroom: ${reason}`);
|
|
17506
17523
|
const stageStart = kind === "session" && request?.delegated !== true;
|
|
@@ -17513,6 +17530,14 @@ class SqlResourceAdmission {
|
|
|
17513
17530
|
signal?.throwIfAborted();
|
|
17514
17531
|
return release;
|
|
17515
17532
|
}
|
|
17533
|
+
if (quarantined === undefined)
|
|
17534
|
+
quarantinedSince = undefined;
|
|
17535
|
+
else {
|
|
17536
|
+
const now = performance.now();
|
|
17537
|
+
quarantinedSince ??= now;
|
|
17538
|
+
if (now - quarantinedSince >= QUARANTINED_CAPACITY_WAIT_MS)
|
|
17539
|
+
throw new QuarantinedCapacityError(quarantined, now - quarantinedSince);
|
|
17540
|
+
}
|
|
17516
17541
|
await delay(50 + Math.floor(Math.random() * 25), undefined, { signal });
|
|
17517
17542
|
}
|
|
17518
17543
|
} catch (error) {
|
|
@@ -17554,9 +17579,9 @@ class SqlResourceAdmission {
|
|
|
17554
17579
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'host reboot confirmed' WHERE owner = ? AND state = 'granted'`, owner.id);
|
|
17555
17580
|
} else if (stopped) {
|
|
17556
17581
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'owner death confirmed' WHERE owner = ? AND kind = 'session' AND state = 'granted'`, owner.id);
|
|
17557
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON);
|
|
17582
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON, QUARANTINED_REASON);
|
|
17558
17583
|
} else {
|
|
17559
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, owner.id);
|
|
17584
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, QUARANTINED_REASON, owner.id);
|
|
17560
17585
|
}
|
|
17561
17586
|
if (owner.host === admissionHost && process.platform !== "win32") {
|
|
17562
17587
|
const processes = await tx.all(`SELECT * FROM ${this.permits} WHERE owner = ? AND state = 'granted' AND kind = 'process' AND process_identity IS NOT NULL`, owner.id);
|
|
@@ -17584,9 +17609,10 @@ class SqlResourceAdmission {
|
|
|
17584
17609
|
return true;
|
|
17585
17610
|
if (permit.state === "waiting")
|
|
17586
17611
|
return stopped || !!permit.expired;
|
|
17587
|
-
|
|
17612
|
+
const quarantined = permit.reason === QUARANTINED_REASON;
|
|
17613
|
+
if (stopped && (permit.kind === "session" || permit.reason !== UNCONFIRMED_REASON && !quarantined))
|
|
17588
17614
|
return true;
|
|
17589
|
-
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON)
|
|
17615
|
+
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON && !quarantined)
|
|
17590
17616
|
return true;
|
|
17591
17617
|
return permit.kind === "process" && permit.host === admissionHost && process.platform !== "win32" && permit.process_identity !== null && processGroupStopped(permit.process_identity);
|
|
17592
17618
|
}
|
|
@@ -17597,7 +17623,9 @@ class SqlResourceAdmission {
|
|
|
17597
17623
|
await this.reconcile();
|
|
17598
17624
|
return this.storage.transaction(async (tx) => {
|
|
17599
17625
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'worker exit confirmed' WHERE root = ? AND holder = ? AND state IN ('waiting', 'granted') AND (kind = 'session' OR state = 'waiting')`, this.options.root, holder);
|
|
17600
|
-
|
|
17626
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, this.options.root, holder, QUARANTINED_REASON);
|
|
17627
|
+
const held = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted'`, this.options.root, holder);
|
|
17628
|
+
return Number(held?.count);
|
|
17601
17629
|
});
|
|
17602
17630
|
}
|
|
17603
17631
|
async reapStoppedProcesses(holder) {
|
|
@@ -17644,10 +17672,13 @@ async function settleProcessGroups(storage, root, scope, reap) {
|
|
|
17644
17672
|
survivors = await settle(REAP_KILL_GRACE_MS);
|
|
17645
17673
|
}
|
|
17646
17674
|
const confirmed = [...groups].flatMap(([group, operations]) => survivors.includes(group) ? [] : operations);
|
|
17647
|
-
|
|
17675
|
+
const quarantined = reap ? survivors.flatMap((group) => groups.get(group) ?? []) : [];
|
|
17676
|
+
if (confirmed.length > 0 || quarantined.length > 0)
|
|
17648
17677
|
await storage.transaction(async (tx) => {
|
|
17649
17678
|
for (const operation of confirmed)
|
|
17650
17679
|
await tx.run(`UPDATE ${permits} SET state = 'released', reason = 'process group cleanup confirmed' WHERE operation = ? AND state = 'granted'`, operation);
|
|
17680
|
+
for (const operation of quarantined)
|
|
17681
|
+
await tx.run(`UPDATE ${permits} SET reason = ? WHERE operation = ? AND state = 'granted'`, QUARANTINED_REASON, operation);
|
|
17651
17682
|
});
|
|
17652
17683
|
return { survivors, unprobeable };
|
|
17653
17684
|
}
|
|
@@ -17664,11 +17695,20 @@ async function reapProcessGroups(journal, root, scope) {
|
|
|
17664
17695
|
return { survivors: [], unprobeable: [] };
|
|
17665
17696
|
return settleProcessGroups(storage, root, scope, true);
|
|
17666
17697
|
}
|
|
17667
|
-
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
17698
|
+
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", QUARANTINED_REASON = "cleanup unconfirmed; permit quarantined", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", QUARANTINED_CAPACITY_WAIT_MS, QuarantinedCapacityError, NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
17668
17699
|
var init_resource_admission = __esm(() => {
|
|
17669
17700
|
init_internal();
|
|
17670
17701
|
init_sql_run_journal();
|
|
17702
|
+
init_workflow_infrastructure_error();
|
|
17671
17703
|
init_admission_host();
|
|
17704
|
+
QUARANTINED_CAPACITY_WAIT_MS = REAP_TERM_GRACE_MS + REAP_KILL_GRACE_MS;
|
|
17705
|
+
QuarantinedCapacityError = class QuarantinedCapacityError extends WorkflowInfrastructureError {
|
|
17706
|
+
constructor(capacity, waitedMs) {
|
|
17707
|
+
const limits = capacity.map(({ limit }) => limit).join(" and ");
|
|
17708
|
+
const groups = [...new Set(capacity.flatMap(({ groups }) => groups))].join(", ");
|
|
17709
|
+
super("worker_cleanup_unconfirmed", `Local worker process cleanup unconfirmed; process permit refused after ${Math.round(waitedMs)} ms: ${limits} ${capacity.length === 1 ? "is" : "are"} held by quarantined permits of process groups ${groups} that survived SIGKILL`);
|
|
17710
|
+
}
|
|
17711
|
+
};
|
|
17672
17712
|
});
|
|
17673
17713
|
|
|
17674
17714
|
// dist/builtin/workflows/src/workers/admission-binding.ts
|
|
@@ -81929,7 +81969,18 @@ async function runWorker(start) {
|
|
|
81929
81969
|
const resourceAdmission = {
|
|
81930
81970
|
async acquire(kind, signal, requestOptions) {
|
|
81931
81971
|
await awaitAdmission(signal);
|
|
81932
|
-
|
|
81972
|
+
if (!admission)
|
|
81973
|
+
return async () => {};
|
|
81974
|
+
try {
|
|
81975
|
+
return await admission.acquire(kind, signal, requestOptions);
|
|
81976
|
+
} catch (error) {
|
|
81977
|
+
if (error instanceof QuarantinedCapacityError && !attemptFenced) {
|
|
81978
|
+
poolFault = error.message;
|
|
81979
|
+
attemptFenced = true;
|
|
81980
|
+
controller.abort(new WorkAttemptFencedError(error.message));
|
|
81981
|
+
}
|
|
81982
|
+
throw error;
|
|
81983
|
+
}
|
|
81933
81984
|
}
|
|
81934
81985
|
};
|
|
81935
81986
|
const admitted = admittedSessionAdapter(adapter, resourceAdmission, async (signal) => {
|
|
@@ -17573,6 +17573,7 @@ class SqlResourceAdmission {
|
|
|
17573
17573
|
epoch: this.options.attempt.epoch
|
|
17574
17574
|
}) : null);
|
|
17575
17575
|
});
|
|
17576
|
+
let quarantinedSince;
|
|
17576
17577
|
try {
|
|
17577
17578
|
for (;; ) {
|
|
17578
17579
|
signal?.throwIfAborted();
|
|
@@ -17582,7 +17583,9 @@ class SqlResourceAdmission {
|
|
|
17582
17583
|
}
|
|
17583
17584
|
const hostReason = this.hostReason();
|
|
17584
17585
|
const pressure = this.stageStartPressure(kind, request?.delegated);
|
|
17586
|
+
let quarantined = undefined;
|
|
17585
17587
|
const granted = await this.storage.transaction(async (tx) => {
|
|
17588
|
+
quarantined = undefined;
|
|
17586
17589
|
await tx.run(`UPDATE ${this.owners} SET expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + ? WHERE id = ?`, OWNER_LEASE_MS, this.owner);
|
|
17587
17590
|
const { root } = this.options;
|
|
17588
17591
|
if (!await this.assertCurrentAttempt(tx)) {
|
|
@@ -17600,6 +17603,8 @@ class SqlResourceAdmission {
|
|
|
17600
17603
|
if (control && control.state !== "running")
|
|
17601
17604
|
reason ??= `root ${control.state}`;
|
|
17602
17605
|
}
|
|
17606
|
+
const held = [];
|
|
17607
|
+
let blockedOtherwise = reason !== undefined;
|
|
17603
17608
|
for (const scope of [`root:${root}`, "user"]) {
|
|
17604
17609
|
const row = await tx.get(`SELECT config FROM ${this.configuration} WHERE scope = ?`, scope);
|
|
17605
17610
|
const limit = row ? JSON.parse(String(row.config))[kind] : undefined;
|
|
@@ -17609,9 +17614,21 @@ class SqlResourceAdmission {
|
|
|
17609
17614
|
const args = scope === "user" ? [kind] : [kind, root];
|
|
17610
17615
|
const count = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted'`, ...args);
|
|
17611
17616
|
const older = await tx.get(`SELECT 1 AS waiting FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'waiting' AND reason IS NOT ? AND ticket < (SELECT ticket FROM ${this.permits} WHERE operation = ?) LIMIT 1`, ...args, STAGE_START_PRESSURE_REASON, operation);
|
|
17612
|
-
|
|
17613
|
-
|
|
17617
|
+
const used = Number(count?.count);
|
|
17618
|
+
if (used < limit && !older)
|
|
17619
|
+
continue;
|
|
17620
|
+
reason ??= `${scope} ${kind}`;
|
|
17621
|
+
const quarantinedPermits = kind === "process" && used >= limit ? await tx.all(`SELECT process_identity FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted' AND reason = ?`, ...args, QUARANTINED_REASON) : [];
|
|
17622
|
+
if (quarantinedPermits.length > 0 && used - quarantinedPermits.length < limit)
|
|
17623
|
+
held.push({
|
|
17624
|
+
limit: `${scope === "user" ? "admission.userLimits" : "admission.limits"}.${kind} (${limit})`,
|
|
17625
|
+
groups: quarantinedPermits.flatMap(({ process_identity }) => process_identity === null ? [] : [JSON.parse(process_identity).id])
|
|
17626
|
+
});
|
|
17627
|
+
else
|
|
17628
|
+
blockedOtherwise = true;
|
|
17614
17629
|
}
|
|
17630
|
+
if (!blockedOtherwise && held.length > 0)
|
|
17631
|
+
quarantined = held;
|
|
17615
17632
|
if (reason && request?.delegated)
|
|
17616
17633
|
throw new Error(`Delegated session cannot be admitted without capacity headroom: ${reason}`);
|
|
17617
17634
|
const stageStart = kind === "session" && request?.delegated !== true;
|
|
@@ -17624,6 +17641,14 @@ class SqlResourceAdmission {
|
|
|
17624
17641
|
signal?.throwIfAborted();
|
|
17625
17642
|
return release;
|
|
17626
17643
|
}
|
|
17644
|
+
if (quarantined === undefined)
|
|
17645
|
+
quarantinedSince = undefined;
|
|
17646
|
+
else {
|
|
17647
|
+
const now = performance.now();
|
|
17648
|
+
quarantinedSince ??= now;
|
|
17649
|
+
if (now - quarantinedSince >= QUARANTINED_CAPACITY_WAIT_MS)
|
|
17650
|
+
throw new QuarantinedCapacityError(quarantined, now - quarantinedSince);
|
|
17651
|
+
}
|
|
17627
17652
|
await delay(50 + Math.floor(Math.random() * 25), undefined, { signal });
|
|
17628
17653
|
}
|
|
17629
17654
|
} catch (error) {
|
|
@@ -17665,9 +17690,9 @@ class SqlResourceAdmission {
|
|
|
17665
17690
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'host reboot confirmed' WHERE owner = ? AND state = 'granted'`, owner.id);
|
|
17666
17691
|
} else if (stopped) {
|
|
17667
17692
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'owner death confirmed' WHERE owner = ? AND kind = 'session' AND state = 'granted'`, owner.id);
|
|
17668
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON);
|
|
17693
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON, QUARANTINED_REASON);
|
|
17669
17694
|
} else {
|
|
17670
|
-
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, owner.id);
|
|
17695
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, QUARANTINED_REASON, owner.id);
|
|
17671
17696
|
}
|
|
17672
17697
|
if (owner.host === admissionHost && process.platform !== "win32") {
|
|
17673
17698
|
const processes = await tx.all(`SELECT * FROM ${this.permits} WHERE owner = ? AND state = 'granted' AND kind = 'process' AND process_identity IS NOT NULL`, owner.id);
|
|
@@ -17695,9 +17720,10 @@ class SqlResourceAdmission {
|
|
|
17695
17720
|
return true;
|
|
17696
17721
|
if (permit.state === "waiting")
|
|
17697
17722
|
return stopped || !!permit.expired;
|
|
17698
|
-
|
|
17723
|
+
const quarantined = permit.reason === QUARANTINED_REASON;
|
|
17724
|
+
if (stopped && (permit.kind === "session" || permit.reason !== UNCONFIRMED_REASON && !quarantined))
|
|
17699
17725
|
return true;
|
|
17700
|
-
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON)
|
|
17726
|
+
if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON && !quarantined)
|
|
17701
17727
|
return true;
|
|
17702
17728
|
return permit.kind === "process" && permit.host === admissionHost && process.platform !== "win32" && permit.process_identity !== null && processGroupStopped(permit.process_identity);
|
|
17703
17729
|
}
|
|
@@ -17708,7 +17734,9 @@ class SqlResourceAdmission {
|
|
|
17708
17734
|
await this.reconcile();
|
|
17709
17735
|
return this.storage.transaction(async (tx) => {
|
|
17710
17736
|
await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'worker exit confirmed' WHERE root = ? AND holder = ? AND state IN ('waiting', 'granted') AND (kind = 'session' OR state = 'waiting')`, this.options.root, holder);
|
|
17711
|
-
|
|
17737
|
+
await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, this.options.root, holder, QUARANTINED_REASON);
|
|
17738
|
+
const held = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted'`, this.options.root, holder);
|
|
17739
|
+
return Number(held?.count);
|
|
17712
17740
|
});
|
|
17713
17741
|
}
|
|
17714
17742
|
async reapStoppedProcesses(holder) {
|
|
@@ -17755,10 +17783,13 @@ async function settleProcessGroups(storage, root, scope, reap) {
|
|
|
17755
17783
|
survivors = await settle(REAP_KILL_GRACE_MS);
|
|
17756
17784
|
}
|
|
17757
17785
|
const confirmed = [...groups].flatMap(([group, operations]) => survivors.includes(group) ? [] : operations);
|
|
17758
|
-
|
|
17786
|
+
const quarantined = reap ? survivors.flatMap((group) => groups.get(group) ?? []) : [];
|
|
17787
|
+
if (confirmed.length > 0 || quarantined.length > 0)
|
|
17759
17788
|
await storage.transaction(async (tx) => {
|
|
17760
17789
|
for (const operation of confirmed)
|
|
17761
17790
|
await tx.run(`UPDATE ${permits} SET state = 'released', reason = 'process group cleanup confirmed' WHERE operation = ? AND state = 'granted'`, operation);
|
|
17791
|
+
for (const operation of quarantined)
|
|
17792
|
+
await tx.run(`UPDATE ${permits} SET reason = ? WHERE operation = ? AND state = 'granted'`, QUARANTINED_REASON, operation);
|
|
17762
17793
|
});
|
|
17763
17794
|
return { survivors, unprobeable };
|
|
17764
17795
|
}
|
|
@@ -17775,11 +17806,20 @@ async function reapProcessGroups(journal, root, scope) {
|
|
|
17775
17806
|
return { survivors: [], unprobeable: [] };
|
|
17776
17807
|
return settleProcessGroups(storage, root, scope, true);
|
|
17777
17808
|
}
|
|
17778
|
-
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
17809
|
+
var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", QUARANTINED_REASON = "cleanup unconfirmed; permit quarantined", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", QUARANTINED_CAPACITY_WAIT_MS, QuarantinedCapacityError, NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
|
|
17779
17810
|
var init_resource_admission = __esm(() => {
|
|
17780
17811
|
init_internal();
|
|
17781
17812
|
init_sql_run_journal();
|
|
17813
|
+
init_workflow_infrastructure_error();
|
|
17782
17814
|
init_admission_host();
|
|
17815
|
+
QUARANTINED_CAPACITY_WAIT_MS = REAP_TERM_GRACE_MS + REAP_KILL_GRACE_MS;
|
|
17816
|
+
QuarantinedCapacityError = class QuarantinedCapacityError extends WorkflowInfrastructureError {
|
|
17817
|
+
constructor(capacity, waitedMs) {
|
|
17818
|
+
const limits = capacity.map(({ limit }) => limit).join(" and ");
|
|
17819
|
+
const groups = [...new Set(capacity.flatMap(({ groups }) => groups))].join(", ");
|
|
17820
|
+
super("worker_cleanup_unconfirmed", `Local worker process cleanup unconfirmed; process permit refused after ${Math.round(waitedMs)} ms: ${limits} ${capacity.length === 1 ? "is" : "are"} held by quarantined permits of process groups ${groups} that survived SIGKILL`);
|
|
17821
|
+
}
|
|
17822
|
+
};
|
|
17783
17823
|
});
|
|
17784
17824
|
|
|
17785
17825
|
// dist/builtin/workflows/src/workers/admission-binding.ts
|