@botbuddy/cli 1.53.1 → 1.54.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/package.json +1 -1
- package/src/stack.mjs +66 -8
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -586,6 +586,7 @@ const CONSIDERED_VERDICT_TEXT = {
|
|
|
586
586
|
HOST_CAPACITY_WAIT: () => "Docker pressure full",
|
|
587
587
|
HOST_CAPACITY_BUDGET_FULL: () => "slot budget full",
|
|
588
588
|
HOST_CAPACITY_INVENTORY_UNKNOWN: () => "unknown Docker resources in its census",
|
|
589
|
+
HOST_PRESSURE: () => "host under pressure",
|
|
589
590
|
TENANT_RESERVATION_FULL: () => "tenant reservation full",
|
|
590
591
|
SLOT_HELD: () => "this slot is already leased there",
|
|
591
592
|
HOST_HELPER_OUTDATED: () => "Helper predates remote leases (protocol < 4)",
|
|
@@ -605,6 +606,33 @@ export function describeConsidered(considered) {
|
|
|
605
606
|
});
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
/**
|
|
610
|
+
* BOT-1630: the typed park reason of a queued lease, or null. The server holds a host under
|
|
611
|
+
* pressure (`docker info` failing, low memory, engine footprint) with verdict HOST_PRESSURE, and
|
|
612
|
+
* a Helper that hits it mid-provision defers the lease back to the queue with that reason.
|
|
613
|
+
*/
|
|
614
|
+
export function queuedParkReason(d) {
|
|
615
|
+
if (!d || typeof d !== "object") return null;
|
|
616
|
+
const ledger = d.ledger && typeof d.ledger === "object" ? d.ledger : null;
|
|
617
|
+
if (ledger?.verdict === "HOST_PRESSURE" || ledger?.capacity?.code === "HOST_PRESSURE") return "host_pressure";
|
|
618
|
+
const here = Array.isArray(d.considered) ? d.considered.find((c) => c?.host_key === d.host_key) : null;
|
|
619
|
+
if (here?.verdict_code === "HOST_PRESSURE") return "host_pressure";
|
|
620
|
+
if (d.provision_defer_reason === "HOST_PRESSURE") return "host_pressure";
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** BOT-1630: "docker_unresponsive (docker_command_failed:docker info:-2; clears …)" per binding dimension. */
|
|
625
|
+
export function describePressure(d) {
|
|
626
|
+
const reasons = d?.ledger?.capacity?.pressure?.reasons;
|
|
627
|
+
if (!Array.isArray(reasons) || reasons.length === 0) return null;
|
|
628
|
+
return reasons.filter((r) => r && typeof r.dimension === "string").map((r) => {
|
|
629
|
+
const parts = [];
|
|
630
|
+
if (r.observed != null) parts.push(r.limit != null ? `${r.observed} vs limit ${r.limit}` : String(r.observed));
|
|
631
|
+
if (typeof r.clears_at === "string") parts.push(`clears ${r.clears_at}`);
|
|
632
|
+
return parts.length ? `${r.dimension} (${parts.join("; ")})` : r.dimension;
|
|
633
|
+
}).join(", ") || null;
|
|
634
|
+
}
|
|
635
|
+
|
|
608
636
|
export function describeUnitLedger(ledger) {
|
|
609
637
|
if (!ledger || typeof ledger !== "object") return [];
|
|
610
638
|
const cap = ledger.capacity ?? {};
|
|
@@ -3547,16 +3575,28 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3547
3575
|
const cleanup = await requestCleanup();
|
|
3548
3576
|
return { exitCode: SIGNAL_EXIT[receivedSignal], outcome: "interrupted", leaseId, cleanup };
|
|
3549
3577
|
}
|
|
3578
|
+
// BOT-1630: ONE park budget (--timeout) across every park. A Helper that finds the host under
|
|
3579
|
+
// pressure mid-provision defers the lease back to `queued` (its place kept); run then parks
|
|
3580
|
+
// again against the same deadline instead of timing out on the provision clock.
|
|
3581
|
+
const parkDeadlineMs = (adapters.now ?? Date.now)() + opts.timeout * 1000;
|
|
3582
|
+
let parkView = request.data;
|
|
3583
|
+
let parkReason = null;
|
|
3584
|
+
for (;;) {
|
|
3550
3585
|
if (state === "queued") {
|
|
3551
3586
|
// BOT-2019: say where the lease sits (server dispatch rank, or why it has none) before parking.
|
|
3552
|
-
stderrLine(`stack: lease ${leaseId} queued — ${describeQueuePosition(
|
|
3553
|
-
for (const line of describeUnitLedger(
|
|
3554
|
-
for (const line of describeConsidered(
|
|
3587
|
+
stderrLine(`stack: lease ${leaseId} queued — ${describeQueuePosition(parkView)}`);
|
|
3588
|
+
for (const line of describeUnitLedger(parkView.ledger)) stderrLine(`stack: ${line}`);
|
|
3589
|
+
for (const line of describeConsidered(parkView.considered)) stderrLine(`stack: ${line}`);
|
|
3590
|
+
parkReason = queuedParkReason(parkView) ?? parkReason;
|
|
3591
|
+
if (parkReason === "host_pressure") {
|
|
3592
|
+
const why = describePressure(parkView);
|
|
3593
|
+
stderrLine(`stack: parked — park_reason host_pressure${why ? `: ${why}` : ""}; the lease keeps its queue place and starts when the host's pressure clears`);
|
|
3594
|
+
}
|
|
3555
3595
|
activeWaitAbort = new AbortController();
|
|
3556
3596
|
// BOT-2013: every reap state (reaping / reaped / reap_failed) means the queued lease is lost.
|
|
3557
3597
|
// BOT-2099: an auth/availability blip is retried inside the park until its deadline;
|
|
3558
3598
|
// it never releases a queued lease or becomes the lease's outcome.
|
|
3559
|
-
const parked = await resilientWait(leaseId, (s) => s !== "queued" && s != null, isReaped, { timeoutSec: opts.timeout, auth: rpc.auth, signal: activeWaitAbort.signal });
|
|
3599
|
+
const parked = await resilientWait(leaseId, (s) => s !== "queued" && s != null, isReaped, { timeoutSec: opts.timeout, deadlineMs: parkDeadlineMs, auth: rpc.auth, signal: activeWaitAbort.signal });
|
|
3560
3600
|
activeWaitAbort = null;
|
|
3561
3601
|
const incident = parkIncident ? { incident: parkIncident } : {};
|
|
3562
3602
|
if (parked?.interrupted || receivedSignal) { const cleanup = await requestCleanup(); return { exitCode: SIGNAL_EXIT[receivedSignal], outcome: "interrupted", leaseId, cleanup, ...incident }; }
|
|
@@ -3578,7 +3618,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3578
3618
|
error: `lease ${leaseId} parked ${opts.timeout}s inside a platform incident (${parkIncident.reason} since ${parkIncident.first_at}), so its admission was never observed`,
|
|
3579
3619
|
};
|
|
3580
3620
|
}
|
|
3581
|
-
return { exitCode: EXIT.TIMEOUT, outcome: "timeout", leaseId, error: `parked ${opts.timeout}s without capacity`, cleanup, ...incident, ...(ledger ? { ledger } : {}) };
|
|
3621
|
+
return { exitCode: EXIT.TIMEOUT, outcome: "timeout", leaseId, error: `parked ${opts.timeout}s without capacity`, cleanup, ...incident, ...(ledger ? { ledger } : {}), ...(parkReason ? { parkReason } : {}) };
|
|
3582
3622
|
}
|
|
3583
3623
|
if (parked?.failed) { const cleanup = await requestCleanup(); return { exitCode: EXIT.LEASE_FAILED, outcome: "error", leaseId, error: `lease was ${parked.state} while queued (reap_reason=${parked.reap_reason ?? "unknown"})`, cleanup, ...incident }; }
|
|
3584
3624
|
if (parked?.error || parked?.auth) { const cleanup = await requestCleanup(); return { exitCode: parked?.auth ? EXIT.AUTH : EXIT.LEASE_FAILED, outcome: "error", leaseId, error: parked?.error || "lease did not leave queue", cleanup, ...incident }; }
|
|
@@ -3593,17 +3633,33 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3593
3633
|
// provisionTimeout before the belt-and-braces recheck below ever notices
|
|
3594
3634
|
// the lease is no longer provisioning. isReaped (== "reaping" || "reaped")
|
|
3595
3635
|
// already covers both; cmdUp's equivalent waits use it for this reason.
|
|
3596
|
-
|
|
3636
|
+
// BOT-1630: `queued` also ends this wait — a deferred provision (HOST_PRESSURE, capacity).
|
|
3637
|
+
const active = await resilientWait(leaseId, (s) => s === "active" || s === "queued", isReaped, { timeoutSec: provisionTimeout, auth: rpc.auth, signal: activeWaitAbort.signal });
|
|
3597
3638
|
activeWaitAbort = null;
|
|
3598
3639
|
if (active?.interrupted || receivedSignal) { const cleanup = await requestCleanup(); return { exitCode: SIGNAL_EXIT[receivedSignal], outcome: "interrupted", leaseId, cleanup }; }
|
|
3599
3640
|
if (active?.authUnavailable) { const cleanup = await requestCleanup(); return authUnavailableResult(active, "provisioning", cleanup); }
|
|
3641
|
+
if (active?.state === "queued" && !active.timeout && !active.failed && !active.error && !active.auth) {
|
|
3642
|
+
const read = await readLeaseQuietly(leaseId);
|
|
3643
|
+
parkView = read?.ok && read.data?.success ? read.data : { host_key: placement.hostKey };
|
|
3644
|
+
const why = parkView.provision_defer_reason ?? "host capacity";
|
|
3645
|
+
stderrLine(`stack: lease ${leaseId} was deferred back to the queue while provisioning (${why}) — its place is kept; parking again`);
|
|
3646
|
+
state = "queued";
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3600
3649
|
if (active?.timeout) {
|
|
3601
3650
|
// BOT-1822 belt-and-braces: the zero-poll wait can still miss a live
|
|
3602
3651
|
// `stack_lease` signal (an older server before this fix, a relay hiccup,
|
|
3603
3652
|
// BOT-1816's class of dropped wakes) even though the lease genuinely went
|
|
3604
|
-
// active. One direct read before declaring
|
|
3605
|
-
// a
|
|
3653
|
+
// active or was deferred back to queued. One direct read before declaring
|
|
3654
|
+
// a timeout avoids reaping a healthy stack or destroying a kept queue place.
|
|
3606
3655
|
const recheck = await api.get(leaseId);
|
|
3656
|
+
if (recheck?.ok && recheck.data?.success && recheck.data.state === "queued") {
|
|
3657
|
+
parkView = recheck.data;
|
|
3658
|
+
const why = parkView.provision_defer_reason ?? "host capacity";
|
|
3659
|
+
stderrLine(`stack: lease ${leaseId} was deferred back to the queue while provisioning (${why}) — its place is kept; parking again`);
|
|
3660
|
+
state = "queued";
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3607
3663
|
if (!(recheck?.ok && recheck.data?.success && recheck.data.state === "active")) {
|
|
3608
3664
|
const cleanup = await requestCleanup();
|
|
3609
3665
|
return { exitCode: EXIT.TIMEOUT, outcome: "timeout", leaseId, error: `physical provision did not reach active in ${provisionTimeout}s`, cleanup, ...(parkIncident ? { incident: parkIncident } : {}) };
|
|
@@ -3621,6 +3677,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3621
3677
|
return { exitCode: active?.auth ? EXIT.AUTH : EXIT.LEASE_FAILED, outcome: "error", leaseId, error: active?.error || `lease ${active?.state ?? "failed"} before reaching active${why}`, cleanup, ...(parkIncident ? { incident: parkIncident } : {}) };
|
|
3622
3678
|
}
|
|
3623
3679
|
}
|
|
3680
|
+
break;
|
|
3681
|
+
}
|
|
3624
3682
|
const current = await api.get(leaseId);
|
|
3625
3683
|
if (!current?.ok || !current.data?.success || current.data.state !== "active" || !current.data.connection || typeof current.data.connection !== "object") {
|
|
3626
3684
|
const cleanup = await requestCleanup();
|