fix: harden addressed registry recovery and routes

This commit is contained in:
2026-08-11 15:02:43 +02:00
parent ee5ac381b3
commit 1b24337a98
8 changed files with 220 additions and 82 deletions
@@ -1 +1,11 @@
import { mkdir, writeFile } from "node:fs/promises"; const root=process.env.JOB_ROOT; if (!root) throw new Error("JOB_ROOT required"); await mkdir(root,{recursive:true,mode:0o700}); const id=process.env.RUN_ID??"a".repeat(32); await writeFile(`${root}/${id}.json`,JSON.stringify({runId:id,phase:"request_claimed"})+"\n",{flag:"wx",mode:0o600}); process.stdout.write(JSON.stringify({ok:true,runId:id}));
import { mkdir, open, writeFile, readFile, rm } from "node:fs/promises";
import { constants } from "node:fs";
const jobs = process.env.JOB_ROOT;
if (!jobs) throw new Error("JOB_ROOT required");
const id = process.env.RUN_ID ?? "a".repeat(32);
const barrier = process.env.BARRIER;
await mkdir(jobs, { recursive: true, mode: 0o700 });
if (barrier) { await writeFile(`${barrier}/${process.pid}.ready`, "ready", { flag: "wx", mode: 0o600 }); while (true) { try { await readFile(`${barrier}/release`); break; } catch { await new Promise(r => setTimeout(r, 5)); } } }
let winner = false;
try { const h = await open(`${jobs}/${id}.json`, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); await h.writeFile(JSON.stringify({ schemaVersion: 1, runId: id, phase: "request_claimed" }) + "\n"); await h.sync(); await h.close(); winner = true; } catch (e) { if (e?.code !== "EEXIST") throw e; }
process.stdout.write(JSON.stringify({ ok: true, runId: id, winner }) + "\n");
@@ -1 +1 @@
import {describe,it,expect} from "vitest"; import * as publication from "../src/workspaces/registry-publication.js"; describe("pull job exports",()=>it("exports addressed state",()=>expect(publication.REGISTRY_SCAN_LIMITS_V1.entries).toBeGreaterThan(0)));
import {describe,it,expect} from "vitest"; import * as publication from "../src/workspaces/registry-publication.js"; describe("pull job exports",()=>it("exports addressed state and exact scan bounds",()=>expect(publication.REGISTRY_SCAN_LIMITS_V1.maximumDirectoryEntries).toBe(4096)));
+4 -2
View File
@@ -116,7 +116,7 @@ const revision: WorkspaceRevision = {
snapshotPath: "/registry/snapshots/psd-clinical.yaml",
};
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish">;
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish"> & { ensureBootstrapAddressed: ReturnType<typeof vi.fn>; publishAddressed: ReturnType<typeof vi.fn> };
function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
return {
@@ -129,6 +129,8 @@ function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
list: vi.fn(async () => [revision]),
read: vi.fn(async () => ({ workspace, revision })),
publish: vi.fn(async () => revision),
ensureBootstrapAddressed: vi.fn(async () => ({ kind: "already_active", snapshot: { schemaVersion: 1, commit: revision.commit, manifestSha256: "a".repeat(64), workspaces: [{ workspaceId: revision.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: "b".repeat(64) }] } })),
publishAddressed: vi.fn(async () => ({ plan: { targetCommit: revision.commit, targetManifestSha256: "a".repeat(64), targetWorkspaces: [{ workspaceId: revision.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: "b".repeat(64) }] } })),
...overrides,
};
}
@@ -218,7 +220,7 @@ test("returns a redacted registry status and pulls without Git credential detail
expect(status.statusCode).toBe(200);
expect(status.json()).toEqual({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: true, lastError: "git_auth_failed",
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
});
expect(pull.statusCode).toBe(200);
expect(JSON.stringify([status.json(), pull.json()])).not.toMatch(/token|password|ssh:\/\//i);
@@ -1 +1,16 @@
import {describe,it,expect} from "vitest"; describe("addressed publication process contract",()=>{it("has a bounded run id",()=>expect("a".repeat(32)).toHaveLength(32));});
import { describe, expect, it } from "vitest";
import { mkdtemp, mkdir, readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { spawn } from "node:child_process";
const worker = join(process.cwd(), "test/fixtures/workspace-registry-addressed-worker.mjs");
async function run(env: Record<string,string>) { return await new Promise<string>((resolve, reject) => { const p = spawn(process.execPath, [worker], { env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] }); let out = ""; p.stdout.on("data", b => out += b); p.on("error", reject); p.on("exit", c => c === 0 ? resolve(out) : reject(new Error(`worker ${c}`))); }); }
describe("addressed publication process ownership", () => {
it("serializes two claimers and leaves one durable final artifact", async () => {
const root = await mkdtemp(join(process.env.TMPDIR ?? "/tmp", "thoth-addressed-process-")); const barrier = await mkdir(join(root, "barrier"), { recursive: true }).then(() => join(root, "barrier")); const jobs = join(root, "jobs");
const env = { JOB_ROOT: jobs, BARRIER: barrier, RUN_ID: "a".repeat(32) };
const a = run(env), b = run(env);
for (let i = 0; i < 100; i++) { if ((await readdir(barrier)).filter(x => x.endsWith(".ready")).length === 2) break; await new Promise(r => setTimeout(r, 5)); }
await writeFile(join(barrier, "release"), "go", { flag: "wx", mode: 0o600 }); const [one, two] = await Promise.all([a,b]); const results = [JSON.parse(one), JSON.parse(two)];
expect(results.filter(x => x.winner)).toHaveLength(1); expect(await readdir(jobs)).toEqual([`${"a".repeat(32)}.json`]);
}, 5000);
});