Repair packed agent startup and bound activation metadata retention

This commit is contained in:
Timothy J. Aveni
2026-10-08 11:19:19 -07:00
parent ffd728b061
commit 165510341d
3 changed files with 93 additions and 21 deletions
+32 -20
View File
@@ -36,33 +36,37 @@ export async function loadCheckedInstallationCandidate(
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory()) if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
throw Error("Candidate must be an immutable Nix output directory"); throw Error("Candidate must be an immutable Nix output directory");
const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root); const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root);
let releaseValidation = () => {};
try { try {
// Validate the duplicated source contract by digest, then discard its tree
// before allocating the substantially larger checked execution plan.
const workspaceDigest = await (async () => {
const input = await readJson(root, "candidate.json", scope);
try {
const { executionContracts: _legacyContracts, ...workspace } = input.value;
return hexDigest(workspace);
} finally {
input.releaseTree();
}
})();
// Sequential reads avoid partially rejected Promise.all() work allocating // Sequential reads avoid partially rejected Promise.all() work allocating
// after the failed candidate's reservation has been released. // after the failed candidate's reservation has been released.
const values = []; const values = [];
for (const file of [ for (const file of [
"candidate.json",
"execution-plan.json", "execution-plan.json",
"execution-artifacts.json", "execution-artifacts.json",
"source.json", "source.json",
"entry.json", "entry.json",
"kernel.json", "kernel.json",
"queries.json", "queries.json",
"graph-execution.json",
]) { ]) {
const parsed = await readJson(root, file, scope); const parsed = await readJson(root, file, scope);
values.push(parsed.value); values.push(parsed.value);
// candidate.json duplicates the checked plan for validation. Nothing in
// the installed candidate retains this input tree after validation.
if (file === "candidate.json") releaseValidation = parsed.releaseTree;
} }
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = values; const [plan, artifacts, source, entryRequest, kernel, queries] = values;
const { executionContracts: _legacyContracts, ...workspace } = candidate;
if ( if (
plan?.definition?.codec !== "camino-execution-plan-v1" || plan?.definition?.codec !== "camino-execution-plan-v1" ||
hexDigest(plan.definition) !== plan.digest || hexDigest(plan.definition) !== plan.digest ||
hexDigest(workspace) !== hexDigest(plan.definition.workspace) workspaceDigest !== hexDigest(plan.definition.workspace)
) )
throw new ExecutionPlanError( throw new ExecutionPlanError(
"CANDIDATE_PLAN_MISMATCH", "CANDIDATE_PLAN_MISMATCH",
@@ -113,14 +117,24 @@ export async function loadCheckedInstallationCandidate(
"Stored interpreter input must match the checked build and plan", "Stored interpreter input must match the checked build and plan",
); );
const expectedGraph = graphExecutionRepresentation(plan, installation, kernel, queries); const expectedGraph = graphExecutionRepresentation(plan, installation, kernel, queries);
if (hexDigest(graph) !== hexDigest(expectedGraph) || artifacts.execution !== hexDigest(graph)) // The stored graph embeds the same kernel/query programs. Validate it in a
throw new ExecutionPlanError( // short-lived scope and retain the representation that shares those trees.
"CANDIDATE_EXECUTION_MISMATCH", await (async () => {
root, const input = await readJson(root, "graph-execution.json", scope);
"Stored graph execution differs from checked inputs", try {
); const digest = hexDigest(input.value);
if (digest !== hexDigest(expectedGraph) || artifacts.execution !== digest)
throw new ExecutionPlanError(
"CANDIDATE_EXECUTION_MISMATCH",
root,
"Stored graph execution differs from checked inputs",
);
} finally {
input.releaseTree();
}
})();
const provenance = { const provenance = {
workspaceRevision: workspace.id as string, workspaceRevision: plan.definition.workspace.id as string,
source: { repository: source.repository as string, commit: source.commit as string }, source: { repository: source.repository as string, commit: source.commit as string },
generator: artifacts.generator as string, generator: artifacts.generator as string,
packages: (plan as ExecutionPlan).definition.packages.map((pkg) => ({ packages: (plan as ExecutionPlan).definition.packages.map((pkg) => ({
@@ -142,9 +156,9 @@ export async function loadCheckedInstallationCandidate(
return { return {
reference, reference,
workspace: plan.definition.workspace.workspaceId, workspace: plan.definition.workspace.workspaceId,
world: hexDigest(graph), world: hexDigest(expectedGraph),
plan: kernel.plan as string, plan: kernel.plan as string,
graphExecution: graph, graphExecution: expectedGraph,
provenance, provenance,
entry, entry,
kernel, kernel,
@@ -161,8 +175,6 @@ export async function loadCheckedInstallationCandidate(
} catch (error) { } catch (error) {
scope.release(); scope.release();
throw error; throw error;
} finally {
releaseValidation();
} }
} }
+5 -1
View File
@@ -9,7 +9,11 @@ export class CandidateMetadataBudget {
private bytes = 0; private bytes = 0;
private scopes = 0; private scopes = 0;
private readonly owners = new Map<string, number>(); private readonly owners = new Map<string, number>();
constructor(limits = { bytes: 512 * 1024 * 1024, ownerBytes: 256 * 1024 * 1024, candidates: 128 }) { // A cutover holds both the serving and proposed installation plus transient
// parsing space. The 57-repository reader retains ~133 MiB per installation;
// a 256 MiB owner ceiling cannot admit its ordinary atomic replacement.
// Keep the aggregate ceiling independent and leave room for parsing overlap.
constructor(limits = { bytes: 512 * 1024 * 1024, ownerBytes: 384 * 1024 * 1024, candidates: 128 }) {
for (const value of Object.values(limits)) for (const value of Object.values(limits))
if (!Number.isSafeInteger(value) || value < 1) throw Error("INVALID_CANDIDATE_METADATA_BUDGET"); if (!Number.isSafeInteger(value) || value < 1) throw Error("INVALID_CANDIDATE_METADATA_BUDGET");
if (limits.ownerBytes > limits.bytes) throw Error("INVALID_CANDIDATE_METADATA_BUDGET"); if (limits.ownerBytes > limits.bytes) throw Error("INVALID_CANDIDATE_METADATA_BUDGET");
+56
View File
@@ -4,6 +4,62 @@ import { mkdtemp, writeFile, symlink, rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { CandidateMetadataBudget, readCandidateMetadata } from "../src/execution-world/metadata.js"; import { CandidateMetadataBudget, readCandidateMetadata } from "../src/execution-world/metadata.js";
import { loadCheckedInstallationCandidate } from "../src/execution-world/candidate.js";
test(
"a real checked application can stage its replacement within the default metadata budget",
{
skip: !process.env.QX_CHECKED_CANDIDATE,
},
async () => {
const root = process.env.QX_CHECKED_CANDIDATE!;
const budget = new CandidateMetadataBudget();
const first = await loadCheckedInstallationCandidate(root, {}, { budget, owner: "reader" });
try {
const retained = budget.snapshot().bytes;
const second = await loadCheckedInstallationCandidate(root, {}, { budget, owner: "reader" });
try {
assert.equal(budget.snapshot().bytes, retained * 2);
assert.deepEqual(second.reference, first.reference, "Compaction preserves exact checked identity");
assert.equal(second.graphExecution.kernel, second.kernel, "Graph representation shares the validated program");
assert.equal(second.graphExecution.queries, second.queries);
// A deliberately tighter pool admits the serving installation but must
// reject replacement parsing without leaking a candidate or tree lease.
const tight = new CandidateMetadataBudget({
bytes: retained + 1,
ownerBytes: retained + 1,
candidates: 2,
});
const blocker = tight.acquire("reader");
blocker.charge(retained);
try {
await assert.rejects(
loadCheckedInstallationCandidate(root, {}, { budget: tight, owner: "reader" }),
/CANDIDATE_METADATA_LIMIT/,
);
assert.equal(tight.snapshot().bytes, retained);
assert.equal(tight.snapshot().candidates, 1);
} finally {
blocker.release();
}
const other = budget.acquire("other");
try {
assert.throws(() => other.charge(budget.limits.bytes), /CANDIDATE_METADATA_LIMIT/);
assert.equal(budget.snapshot().bytes, retained * 2, "Rejected admission must not leak charges");
} finally {
other.release();
}
} finally {
second.metadata.release();
}
assert.equal(budget.snapshot().bytes, retained, "The serving candidate survives failed or completed staging");
} finally {
first.metadata.release();
}
assert.equal(budget.snapshot().bytes, 0);
assert.equal(budget.snapshot().candidates, 0);
},
);
test("concurrent candidate reservations enforce aggregate and owner limits and release exactly once", () => { test("concurrent candidate reservations enforce aggregate and owner limits and release exactly once", () => {
const budget = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 600, candidates: 3 }); const budget = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 600, candidates: 3 });