row 180 (#1678, P19 391): activation ids within one second and plan-mode refusals
next_activation() takes the UTC second once and tries the base id, then -1 through -9, against stack-rollback/, backups/stack/ and the post-attempt store, all under the lock. ACT and POST share the suffix, and with all ten taken it refuses capture-failed with nothing stopped. Adopt uses the same helper. --restore takes the old and the suffixed form. Under --plan, a bad --tester-port reports tester-port-invalid through check_refuse and tester-port-target goes through check_refuse in both modes, so the plan lists every refusal. Without --plan the output is the usage block, byte for byte as before. webui-service.sh counts the tester port's digits as written before stripping zeros, so 007333 refuses as serve does. Round 2 adds subtests C1 (adopt takes the next free id) and C2 (a bad port is usage before any pointer read) for Dewey's round 1 (#1678 comment 29352). Dewey approved round 2 in comment 29361.
This commit is contained in:
+14
-3
@@ -387,6 +387,12 @@ one final line with `result` `ok`, `refused`, `gate-failed`,
|
||||
failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||||
`state/stack-release.lock` (`run-locked`).
|
||||
|
||||
An activation id is the UTC second and the target copy's first 12 characters,
|
||||
`20261020T013708Z-89af38a83afa`. A second run in the same second adds `-1`,
|
||||
then `-2` up to `-9`, the first one with no rollback set, backup or
|
||||
post-attempt store yet, so ids still sort by time. With all ten taken the run
|
||||
refuses `capture-failed` before it stops anything. Adopt uses the same rule.
|
||||
|
||||
1. Once per host, `adopt BUSINESS` records the running stack as the pointer:
|
||||
the copy `stack-copy.sh check` reports, the bus unit's `--pm` and
|
||||
`MOSAIC_CLAUDE_BIN`, the Console's flags without `--checkout`, and the
|
||||
@@ -421,11 +427,13 @@ failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only
|
||||
this start. A pass writes the pointer.
|
||||
6. A gate failure puts back the saved units and wrapper, restores the backup
|
||||
when a migration ran, keeps the store it replaced as
|
||||
`bus.post-attempt-<stamp>`, starts the old copy and gates it. If that gate
|
||||
`bus.post-attempt-<stamp>`, where the stamp is the activation's second and
|
||||
its suffix if it has one, starts the old copy and gates it. If that gate
|
||||
fails too, both units stay stopped, the log says `rollback-failed`, and
|
||||
Jason or Mos settles it from the rollback set the message names.
|
||||
7. `rollback BUSINESS` returns to the copy before the pointer's, taken from
|
||||
the log. When the schemas differ it needs `--restore ACTIVATION`; events
|
||||
the log. When the schemas differ it needs `--restore ACTIVATION`, with or
|
||||
without a `-1` to `-9` suffix; events
|
||||
written after that backup need `--events N` with the count it prints, and
|
||||
Jason decides first. It refuses `business-unreadable` and
|
||||
`unknown-kind-for-target` like `activate`. For the second, set each kind
|
||||
@@ -834,7 +842,10 @@ unit write for the authorized operator, with no start/restart verb.
|
||||
`stack-release.sh activate ID BUSINESS --tester-port N` replaces or adds this
|
||||
Console flag in both the installed unit and the pointer. It checks the target
|
||||
copy's own Console before stopping anything; an explicit unsupported request
|
||||
refuses `tester-port-target`. Without an explicit flag, activate and rollback
|
||||
refuses `tester-port-target`. A port outside 1024 to 65535, 7332 or the
|
||||
Console's `--port` is a usage error (exit 4). Under `--plan` both are listed
|
||||
with the plan's other refusals instead, the bad port as `tester-port-invalid`,
|
||||
and the plan exits 3. Without an explicit flag, activate and rollback
|
||||
keep it between supporting copies, or drop it when the target predates support.
|
||||
The plan prints the final Console flags and any dropped `--tester-port N`;
|
||||
the steps log records a drop too. `pm-on` keeps the pointer's Console flags.
|
||||
|
||||
@@ -1095,3 +1095,105 @@ test('T42 tester-port pm-on preserves pointer Console flags',async(t)=>{
|
||||
r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);
|
||||
});
|
||||
|
||||
// Row 180 item 1 (P19 391): a date stand-in prints one second for every call that names a time,
|
||||
// so each run below starts in that second, as two runs within one real second do. Calls for
|
||||
// +%s (the gate's start mark) pass through, so each gate still reads only its own start.
|
||||
function pinClock(e){
|
||||
const dir=join(e.root,'clock'),second=Math.floor(Date.now()/1000);mkdirSync(dir);
|
||||
writeFileSync(join(dir,'date'),`#!/bin/sh\ncase "$*" in *%s*) exec /usr/bin/date "$@" ;; esac\nexec /usr/bin/date -d @${second} "$@"\n`,{mode:0o755});
|
||||
e.env={...e.env,PATH:`${dir}:${e.env.PATH}`};
|
||||
return execFileSync(join(dir,'date'),['-u','+%Y%m%dT%H%M%SZ'],{env:e.env,encoding:'utf8'}).trim();
|
||||
}
|
||||
const rollbackSets=(e)=>readdirSync(join(e.state,'stack-rollback')).sort();
|
||||
|
||||
test('T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores',async(t)=>{
|
||||
await t.test('activate then pm-on on the same copy',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});const sec=pinClock(e),id=`${sec}-${C1.slice(0,12)}`;
|
||||
let r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).activation,id);
|
||||
biz(e,HARNESS);
|
||||
r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).activation,`${id}-1`);
|
||||
assert.deepEqual(logLines(e).slice(-4).map((l)=>[l.op,l.result,l.activation]),[['activate','started',id],['activate','ok',id],['pm-on','started',`${id}-1`],['pm-on','ok',`${id}-1`]]);
|
||||
assert.deepEqual(rollbackSets(e),[id,`${id}-1`]);
|
||||
for(const a of [id,`${id}-1`])assert.ok(existsSync(join(e.state,'stack-rollback',a,'SHA256SUMS')),a);
|
||||
});
|
||||
await t.test('two restores: an old-form id and a suffixed one, each with its own post-attempt store',async(t)=>{
|
||||
const e=await mk(t);await seed(e);const sec=pinClock(e),c=`${sec}-${C.slice(0,12)}`,b=`${sec}-${B.slice(0,12)}`;
|
||||
for(const [act,restore] of [[c,b],[`${c}-1`,`${b}-1`]]){
|
||||
let r=await run(e,'activate',C,'acme','--migrate');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).activation,act);
|
||||
assert.ok(existsSync(join(e.data,'backups','stack',act,'SHA256SUMS')),act);
|
||||
r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,0,r.out+r.err);
|
||||
const post=join(e.data,`bus.post-attempt-${sec}${restore.slice(b.length)}`);
|
||||
assert.ok(r.out.startsWith(`restore: ${join(e.data,'backups','stack',act)}; the store now in place moves to ${post} and is kept; 0 event(s) were written after the backup\n`),r.out);
|
||||
assert.deepEqual([pointer(e).copy,pointer(e).activation,pointer(e).schema],[B,restore,'3g']);
|
||||
assert.equal(logLines(e).at(-1).backup,act);
|
||||
}
|
||||
assert.deepEqual(posts(e).sort(),[`bus.post-attempt-${sec}`,`bus.post-attempt-${sec}-1`]);
|
||||
assert.deepEqual(readdirSync(join(e.data,'backups','stack')).sort(),[c,`${c}-1`]);
|
||||
assert.deepEqual(rollbackSets(e),[c,`${c}-1`,b,`${b}-1`].sort());
|
||||
});
|
||||
await t.test('adopt takes the next free id too',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pointer:'none'});const id=`${pinClock(e)}-${B.slice(0,12)}`;
|
||||
mkdirSync(join(e.state,'stack-rollback',id),{recursive:true});
|
||||
const r=await run(e,'adopt','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).activation,`${id}-1`);
|
||||
assert.deepEqual(logLines(e).map((l)=>[l.op,l.result,l.activation]),[['adopt','ok',`${id}-1`]]);
|
||||
});
|
||||
await t.test('all ten names taken: capture-failed before anything stops, and the log line names no activation',async(t)=>{
|
||||
const e=await mk(t);await seed(e);const sec=pinClock(e),id=`${sec}-${C1.slice(0,12)}`;
|
||||
// A rollback set, a backup or a post-attempt store each takes a name.
|
||||
for(const s of ['','-1','-2','-3','-4','-5','-6','-7'])mkdirSync(join(e.state,'stack-rollback',`${id}${s}`),{recursive:true});
|
||||
mkdirSync(join(e.data,'backups','stack',`${id}-8`),{recursive:true});writeFileSync(join(e.data,`bus.post-attempt-${sec}-9`),'');
|
||||
await refused(e,['activate',C1,'acme'],'capture-failed',`every activation id for ${id} is taken; nothing was stopped`);
|
||||
assert.equal(logLines(e).at(-1).activation,null);
|
||||
});
|
||||
await t.test('--restore takes an id with or without -1 to -9 and nothing else',async(t)=>{
|
||||
const e=await mk(t);await seed(e);const id=`20261020T013708Z-${B.slice(0,12)}`;
|
||||
for(const bad of [`${id}-0`,`${id}-10`,`${id}-a`,`${id}-`,`${id}1`]){
|
||||
const r=await run(e,'rollback','acme','--restore',bad,'--plan');assert.equal(r.code,4,bad);
|
||||
}
|
||||
for(const good of [id,`${id}-1`,`${id}-9`]){
|
||||
const r=await run(e,'rollback','acme','--restore',good,'--plan');assert.notEqual(r.code,4,`${good}: ${r.err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals',async(t)=>{
|
||||
const usageText=async(e)=>(await run(e,'--help')).err;
|
||||
await t.test('a bad tester port',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});biz(e,HARNESS);
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length,p0=readFileSync(e.pointer,'utf8');
|
||||
const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;
|
||||
// The fixture's Console has --port e.port; a pointer without --port has the default 7332, which
|
||||
// the reason names once. --plan reads the pointer's flags and not the units, so only it changes.
|
||||
const noPort=()=>{const q=JSON.parse(p0);q.console=q.console.filter((f,i,a)=>f!=='--port'&&a[i-1]!=='--port');writeFileSync(e.pointer,JSON.stringify(q)+'\n')};
|
||||
for(const [port,reason,edit] of [['80',rule('80',e.port)],[e.port,rule(e.port,e.port)],['80',rule('80'),noPort],['7332',rule('7332'),noPort]]){
|
||||
if(edit)edit();
|
||||
const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);
|
||||
assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,'');
|
||||
assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: ${reason}`,'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm']);
|
||||
const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);
|
||||
assert.equal(u.code,4,u.out+u.err);assert.equal(u.out,'');assert.equal(u.err,await usageText(e));
|
||||
writeFileSync(e.pointer,p0);
|
||||
}
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
});
|
||||
await t.test('without --plan a bad port is usage before any pointer read',async(t)=>{
|
||||
// parse_opts checks it, so a host with no pointer gets usage and not no-pointer (exit 3, a log line).
|
||||
const e=await mk(t);await seed(e,{pointer:'none'});
|
||||
const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');
|
||||
assert.equal(u.code,4,u.out+u.err);assert.equal(u.out,'');assert.equal(u.err,await usageText(e));
|
||||
assert.deepEqual(logLines(e),[]);assert.deepEqual(startsStops(e,n),[]);
|
||||
});
|
||||
await t.test('a target Console without --tester-port',async(t)=>{
|
||||
const e=await mk(t);await seed(e);biz(e,HARNESS);
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
|
||||
const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');
|
||||
assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,'');
|
||||
assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,6 +170,19 @@ test('tester service invalid ports refuse render and install with exit 4 and lea
|
||||
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||
});
|
||||
|
||||
// Row 180 (Dewey's row 176 N2): the length counts as written, before the zeros go, as serve
|
||||
// counts it. 007333 is six digits and refuses; 07333 is five and renders.
|
||||
test('tester service counts the digits as written before it strips leading zeros', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const six = await run(dir, ['render', '--tester-port', '007333']);
|
||||
assert.equal(six.status, 4, six.stderr);
|
||||
assert.equal(six.stdout, '');
|
||||
assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');
|
||||
const five = await run(dir, ['render', '--tester-port', '07333']);
|
||||
assert.equal(five.status, 0, five.stderr);
|
||||
assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));
|
||||
});
|
||||
|
||||
test('tester service install and status show the tester port without changing send reporting', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const unitDir = join(dir, 'units');
|
||||
|
||||
@@ -73,14 +73,16 @@ parse_opts() {
|
||||
--plan) [ "$OP" = activate ] || [ "$OP" = rollback ] || usage; PLAN=1; shift ;;
|
||||
--pm) [ "$OP" = activate ] || usage; WANT_PM=1; shift ;;
|
||||
--pm-after-harness) [ "$OP" = activate ] || usage; AFTER_HARNESS=1; shift ;;
|
||||
--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] || usage; TESTER_PORT="$2"; valid_tester_port "$TESTER_PORT" 7332 || usage; shift 2 ;;
|
||||
--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;
|
||||
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--events) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] || usage; EVENTS="$2"; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$EVENTS" ] || [ -n "$RESTORE" ] || usage
|
||||
# A bad tester port is a usage error; --plan lists it as tester-port-invalid instead (row 180).
|
||||
[ -z "$TESTER_PORT" ] || [ "$PLAN" = 1 ] || valid_tester_port "$TESTER_PORT" 7332 || usage
|
||||
# --pm-after-harness replaces --pm (row 169 item 1), so the two never go together.
|
||||
[ "$WANT_PM" = 0 ] || [ "$AFTER_HARNESS" = 0 ] || usage
|
||||
local d
|
||||
@@ -633,8 +635,14 @@ console_for_target() {
|
||||
RUN_CONSOLE=("${P_CONSOLE[@]}")
|
||||
DROP_TESTER=""
|
||||
if [ -n "$TESTER_PORT" ]; then
|
||||
valid_tester_port "$TESTER_PORT" "$main" || usage
|
||||
[ "$supported" = 1 ] || refuse tester-port-target "$TO's Console does not support --tester-port"
|
||||
# --plan lists both with its other refusals (row 180); outside it they end the run.
|
||||
if ! valid_tester_port "$TESTER_PORT" "$main"; then
|
||||
[ "$PLAN" = 1 ] || usage
|
||||
local other=""
|
||||
[ "$main" = 7332 ] || other=" and the Console's --port $main"
|
||||
check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"
|
||||
fi
|
||||
[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"
|
||||
RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")
|
||||
elif [ -n "$inherited" ] && [ "$supported" = 0 ]; then
|
||||
RUN_CONSOLE=("${without_tester[@]}")
|
||||
@@ -659,10 +667,25 @@ pointer_for_business() {
|
||||
[ "$P_BUSINESS" = "$BUSINESS" ] || refuse business-mismatch "the pointer is for $P_BUSINESS"
|
||||
}
|
||||
|
||||
# The activation id is the second and the target's 12-character prefix. A second run in the same
|
||||
# second adds -1 to -9, so ids still sort by time and an id without one is still whole (row 180).
|
||||
# Callers hold the lock. Sets ACT and POST from one free suffix: no rollback set, backup or
|
||||
# post-attempt store with that name.
|
||||
next_activation() {
|
||||
local second s
|
||||
second="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
for s in "" -1 -2 -3 -4 -5 -6 -7 -8 -9; do
|
||||
ACT="$second-${TO:0:12}$s" POST="$DATA/bus.post-attempt-$second$s"
|
||||
[ -e "$STATE/stack-rollback/$ACT" ] || [ -e "$DATA/backups/stack/$ACT" ] || [ -e "$POST" ] || return 0
|
||||
done
|
||||
# Every name is another run's, so the refused line names none and closes no started run.
|
||||
ACT="" POST=""
|
||||
refuse capture-failed "every activation id for $second-${TO:0:12} is taken; nothing was stopped"
|
||||
}
|
||||
|
||||
new_activation() {
|
||||
ACT="$(date -u +%Y%m%dT%H%M%SZ)-${TO:0:12}"
|
||||
next_activation
|
||||
RS="$STATE/stack-rollback/$ACT"
|
||||
POST="$DATA/bus.post-attempt-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
}
|
||||
|
||||
# --- Verbs ---
|
||||
@@ -677,7 +700,7 @@ do_adopt() {
|
||||
read_units || refuse check-failed "cannot read $BUS_UNIT or $CONSOLE_UNIT"
|
||||
[ -n "$U_CLAUDE" ] || refuse no-claude-pin "$BUS_UNIT has no MOSAIC_CLAUDE_BIN"
|
||||
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION from $COPIES/$TO/packages/bus/src/store.mjs"
|
||||
ACT="$(date -u +%Y%m%dT%H%M%SZ)-${TO:0:12}"
|
||||
next_activation
|
||||
write_pointer "$TO" "$U_PM" 0 "$U_CLAUDE" "$T_SCHEMA" "${U_CONSOLE[@]}"
|
||||
log_line result ok
|
||||
echo "adopted $TO for $BUSINESS (pm $U_PM, schema $T_SCHEMA)"
|
||||
|
||||
@@ -122,6 +122,8 @@ tester_port_usage() {
|
||||
|
||||
tester_port_ok() {
|
||||
case "$TESTER_PORT" in ''|*[!0-9]*) tester_port_usage ;; esac
|
||||
# At most five digits as written, before the zeros go, as serve and stack-release.sh count them.
|
||||
[ "${#TESTER_PORT}" -le 5 ] || tester_port_usage
|
||||
local tester_port="$TESTER_PORT"
|
||||
# Compare decimal values, including values written with leading zeros.
|
||||
while [ "${tester_port#0}" != "$tester_port" ]; do tester_port="${tester_port#0}"; done
|
||||
|
||||
Reference in New Issue
Block a user