Bound holds: a timeout, a brake, and no password in the log
A check that hangs no longer stalls every consumer: it times out after 30s and counts as could-not-ask, and the rest of that pass is not asked. A consumer still not held after being applied again is checked at doubling intervals up to an hour, and said loudly, so an adapter whose create and holds disagree costs one re-apply an hour, not one a minute. The consumer's password is scrubbed from every error the harness logs.
This commit is contained in:
@@ -167,6 +167,111 @@ test("provisioner keeps a consumer applied when the backend cannot be asked", as
|
||||
stop();
|
||||
});
|
||||
|
||||
test("provisioner backs off when create does not satisfy holds, and says so", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "prov-brake-"));
|
||||
await writeFile(join(dir, "webapp.secret"), "minted-pw");
|
||||
const receives = join(dir, "cache.json");
|
||||
await writeFile(receives, JSON.stringify({ requirement: "cache", given: [{ as: "webapp-anchor", secret: join(dir, "webapp.secret") }] }));
|
||||
let creates = 0;
|
||||
let asked = 0;
|
||||
const logged: string[] = [];
|
||||
const original = console.error;
|
||||
console.error = (...a: unknown[]) => logged.push(a.join(" "));
|
||||
try {
|
||||
const stop = runProvisioner(
|
||||
"cache",
|
||||
{
|
||||
async create() {
|
||||
creates++;
|
||||
},
|
||||
async remove() {},
|
||||
async holds() {
|
||||
asked++;
|
||||
return false; // an adapter that disagrees with itself: nothing create does is ever held
|
||||
},
|
||||
},
|
||||
{ receives, everyMs: 5, verifyEveryMs: 20 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
stop();
|
||||
} finally {
|
||||
console.error = original;
|
||||
}
|
||||
// Without the brake this would be ~20 creates (one per 20ms check). With doubling waits
|
||||
// (20, 40, 80, 160ms…) it is a handful.
|
||||
assert.ok(creates >= 3 && creates <= 7, `creates: ${creates}`);
|
||||
assert.equal(asked, creates - 1);
|
||||
assert.ok(logged.some((l) => l.includes("create does not produce what holds checks")));
|
||||
});
|
||||
|
||||
test("provisioner treats a failed or hung holds as could-not-ask, and never logs the password", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "prov-timeout-"));
|
||||
await writeFile(join(dir, "a.secret"), "s3cret-pw");
|
||||
await writeFile(join(dir, "b.secret"), "other-pw");
|
||||
const receives = join(dir, "cache.json");
|
||||
await writeFile(
|
||||
receives,
|
||||
JSON.stringify({ requirement: "cache", given: [{ as: "a", secret: join(dir, "a.secret") }, { as: "b", secret: join(dir, "b.secret") }] }),
|
||||
);
|
||||
let creates = 0;
|
||||
const askedFor: string[] = [];
|
||||
const logged: string[] = [];
|
||||
const original = console.error;
|
||||
console.error = (...a: unknown[]) => logged.push(a.join(" "));
|
||||
try {
|
||||
const stop = runProvisioner(
|
||||
"cache",
|
||||
{
|
||||
async create() {
|
||||
creates++;
|
||||
},
|
||||
async remove() {},
|
||||
holds(p) {
|
||||
askedFor.push(p.as);
|
||||
// "a" fails the way a shelled-out tool does, with its arguments in the message.
|
||||
if (p.as === "a") return Promise.reject(new Error(`Command failed: tool --password ${p.password}`));
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
{ receives, everyMs: 5, verifyEveryMs: 30, holdsTimeoutMs: 20 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
stop();
|
||||
} finally {
|
||||
console.error = original;
|
||||
}
|
||||
assert.equal(creates, 2); // the first pass only; nothing is applied again on "could not ask"
|
||||
// After one consumer's check fails, the rest of that pass is not asked: "b" follows "a" and is
|
||||
// never reached, because every pass stops at "a".
|
||||
assert.ok(askedFor.length >= 2 && askedFor.every((as) => as === "a"), askedFor.join(","));
|
||||
assert.ok(logged.some((l) => l.includes("Command failed: tool --password ***")));
|
||||
assert.ok(!logged.some((l) => l.includes("s3cret-pw")));
|
||||
|
||||
// A check that never answers counts as could-not-ask once its time is up.
|
||||
let hungCreates = 0;
|
||||
const hungLogged: string[] = [];
|
||||
console.error = (...a: unknown[]) => hungLogged.push(a.join(" "));
|
||||
try {
|
||||
const stopHung = runProvisioner(
|
||||
"cache",
|
||||
{
|
||||
async create() {
|
||||
hungCreates++;
|
||||
},
|
||||
async remove() {},
|
||||
holds: () => new Promise<boolean>(() => {}),
|
||||
},
|
||||
{ receives, everyMs: 5, verifyEveryMs: 30, holdsTimeoutMs: 20 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
stopHung();
|
||||
} finally {
|
||||
console.error = original;
|
||||
}
|
||||
assert.equal(hungCreates, 2);
|
||||
assert.ok(hungLogged.some((l) => l.includes("no answer within 20ms")));
|
||||
});
|
||||
|
||||
test("modules can SERVE: a real async tool, loaded and invoked over the broker", async () => {
|
||||
resetTools();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user