File size: 35,774 Bytes
1e251f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 | """WIN2 back-to-back (chain) mode (NDM 2026-10-27: "We do not need to keep restoring production -- go window after window").
Coordinator-only. A chained window still opens and closes its own HOLD and keeps every fence, quiet and absence proof;
at a SUCCESSFUL end it leaves production DOWN (no W8 restore, no post-close watch) and writes MARKER. The next chained
window's preflight accepts "MARKER + production down + no HOLD" instead of "production serving". Any stop or failure,
or the last window of a chain (WINDOW_CHAIN_LAST=1), runs the normal strict W8 restore + watch and then end_chain().
S92 FIX 1 (WIN2-R2): production is down for the whole gap between chained windows, so a stop OUTSIDE a window's own
HOLD must restore too. `end_action` / `restore_required` decide it from the marker as well as the HOLD; a stop with no
owned HOLD while the marker is present takes `abort`: when no HOLD exists, the strict W8 start|check|close (under the
reviewed scoped recovery's own HOLD) + post-close watch, end_chain() only on RESTORED + watch PASS; when a HOLD exists, alert
and touch nothing. chain_prev_ok.sh runs that abort on any refusal while the marker is present (exit 3).
Hold placeholders (WIN2-R2 rule from R-PC1/R-PP4; WIN2-R3 root fix of R-NBW's defect): a tmux `dsv41-hold` session
running "vllm serve [DSV41-OFFLOAD hold] owner=<chain owner>" stays up on both Sparks for the whole chain, so other users
(NVO drama3, the local-law queue) never see the Sparks as free while production is down. These CHAIN placeholders live on
the DEFAULT tmux socket and outlive every window scope: the runner starts them from its own unscoped process over plain
ssh (`placeholders_up`, refused inside a scoped_remote scope) before the first fence of its `-hold` scope, whose own
placeholder sits on that scope's private socket and dies with the fence. Every probe uses the absolute tmux binary on the
default socket (PLACEHOLDER_TMUX), so it sees them from anywhere, including inside a `-restore-close` scope whose tmux shim
would look at its private socket. The owner is in the process title on the Spark and in the marker; start, probe and stop
refuse a placeholder of any other owner (or of none, e.g. window.sh open's). A chained end closes only the HOLD file
(`chained_close(owner)`), the next chained preflight requires this chain's placeholders, and they come down only in
`end_chain()`: after the strict W8 restore verified and the post-close watch passed (a window's own restore, or `abort`).
window.sh close cannot remove them when it runs under a scope (its kill reaches only the scope's private socket).
The marker also carries the chain's production snapshot (path + sha256 of the rollback.json captured by the window that
started from serving production), so any runner can follow any other runner's chained window (R-NBW request).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import shlex
import subprocess
import sys
import time
import urllib.request
import uuid
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import cast
from offload.prefill_maxlen.evidence import Json, record
# DSV41_CHAIN_MARKER / DSV41_CHAIN_PRODUCTION_URL are TEST-ONLY overrides (S92 note: chain_prev_ok.sh could not be
# rehearsed end to end without touching the live marker). `status` prints both; production runs never set them.
MARKER = Path(os.environ.get('DSV41_CHAIN_MARKER') or '/opt/dsv41/campaigns/DSV41-OFFLOAD/scratch/CHAINED')
SCHEMA = 'dsv41-chain-v1'
PRODUCTION_MODELS = os.environ.get('DSV41_CHAIN_PRODUCTION_URL') or 'http://HEAD_IP:8000/v1/models'
K1_CAP = 327680
COORDINATOR = 'Deepseek/coordinator'
PLACEHOLDER_HOSTS = ('sparkhead', 'sparkworker-wired')
PLACEHOLDER_SESSION = 'dsv41-hold'
PLACEHOLDER_TITLE = 'vllm serve [DSV41-OFFLOAD hold]'
# The absolute binary on the explicit default socket: never a scoped_remote tmux shim (which sends any `dsv41-hold` command
# to the scope's private socket, R-NBW scratch/RNBW/placeholder-proof.txt) and never the server of an inherited $TMUX.
PLACEHOLDER_TMUX = '/usr/bin/tmux -L default'
OWNER_PATTERN = '[A-Za-z0-9_-]{1,64}'
# One Spark's chain-placeholder step: `probe`, `up` (start unless this owner's is up) or `down` (stop this owner's). The last
# line is "PLACEHOLDER <state>": UP owner=<o> pid=<p> | ABSENT | DOWN | FOREIGN owner=<o|none> pid=<p> | BROKEN <why> |
# REFUSED <why> (exit 3). A foreign or broken placeholder is never touched. `up` and `down` refuse inside any scoped_remote
# window scope (dsv41w-*.scope): a default-socket server born there would die with the scope's fence. Each action's script
# carries only its own branch, so a probe contains no mutation. No `${...}` anywhere: systemd >= 258 expands it in the
# command line of a `systemd-run --scope` (scoped_remote's launch; Controller runs 259), unbraced `$x` and `$(...)` it leaves.
_PLACEHOLDER_STATE = r"""set -u
owner=@OWNER@
title='@TITLE@'
t() { @TMUX@ "$@"; }
state() {
t has-session -t =@SESSION@ 2>/dev/null || { echo ABSENT; return; }
local panes cmd who
panes=$(t list-panes -s -t =@SESSION@ -F '#{pane_pid}' 2>/dev/null) || { echo 'BROKEN pane query failed'; return; }
case "$panes" in ''|*[!0-9]*) echo "BROKEN panes: $(echo $panes)"; return ;; esac
cmd=$(tr '\0' '\n' < "/proc/$panes/cmdline" 2>/dev/null | head -n 1)
case "$cmd" in
"$title owner=$owner") echo "UP owner=$owner pid=$panes" ;;
"$title owner="*) who=$(printf '%s' "$cmd" | cut -c@OWNER_FROM@-); echo "FOREIGN owner=$who pid=$panes" ;;
"$title") echo "FOREIGN owner=none pid=$panes" ;;
'') echo "BROKEN pane runs: nothing" ;;
*) echo "BROKEN pane runs: $cmd" ;;
esac
}
s=$(state)
"""
_PLACEHOLDER_UNSCOPED = r"""if grep -Eq '/dsv41w-[^/]*\.scope(/|$)' /proc/self/cgroup; then
echo "PLACEHOLDER REFUSED: @ACTION@ inside a scoped_remote window scope; start and stop only from an unscoped process"
exit 3
fi
"""
_PLACEHOLDER_ACTIONS = {
'probe': '',
'up': _PLACEHOLDER_UNSCOPED + r"""if [ "$s" = ABSENT ]; then
t new-session -d -s @SESSION@ "exec -a '$title owner=$owner' sleep 86400" >/dev/null 2>&1
# The pane runs tmux's fork, then the shell, until the exec lands.
for _ in $(seq 50); do s=$(state); case "$s" in UP*|FOREIGN*|ABSENT) break ;; esac; sleep 0.1; done
fi
""",
'down': _PLACEHOLDER_UNSCOPED + r"""case "$s" in
UP*)
pid=$(printf '%s' "$s" | sed 's/.*pid=//')
t kill-session -t =@SESSION@ 2>/dev/null
for _ in $(seq 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
if kill -0 "$pid" 2>/dev/null; then s="BROKEN pid $pid survived kill-session"; else s=DOWN; fi ;;
esac
""",
}
CHAINED_CLOSE_OK = 'CHAINED CLOSE RECORDED'
PLACEHOLDERS_KEPT = ('chain placeholders (default tmux socket, owner in the process title) kept up between chained windows; '
'only end_chain() takes them down, after the strict W8 restore and a post-close watch PASS')
ABORT_RESTORED = 'RESTORED: K1 START VERIFIED, greedy 6/6 IDENTICAL, PRODUCTION HOLD OK, window closed (chain abort)'
def enabled() -> bool:
return os.environ.get('WINDOW_CHAIN') == '1'
def last() -> bool:
return os.environ.get('WINDOW_CHAIN_LAST') == '1'
def skip_restore(stopped: bool) -> bool:
"""True only for a chained, non-last window that ended without any stop."""
return enabled() and not last() and not stopped
def marker_present() -> bool:
"""The marker exists in any form (a torn or unreadable marker still means a chain left production down)."""
return MARKER.exists() or MARKER.is_symlink()
def end_action(stopped: bool, hold_written: bool) -> str:
"""How a window must end (S92 FIX 1). Chained runners use this, never `hold_written` alone:
'chained' - chained, non-last, no stop: owner = chain_owner(); placeholders_up(owner) BEFORE the first fence of the
window's scopes; stop the experiment, prove the Sparks quiet, close the own HOLD with
chained_close(owner) in its `-restore-close` scope, finish_chained(..., placeholder_owner=owner);
production stays down.
'restore' - the window owns a HOLD: its own strict W8 restore + post-close watch, then end_chain() on watch PASS
(end_chain(owner) when this window started chain placeholders before any marker recorded them).
'abort' - no owned HOLD but the chain marker is present (production is down from the previous window, e.g. a
pre-HOLD failure of a chained window): abort(out), which restores with the strict W8 + watch when no
HOLD exists and only alerts when one does. Record its summary as the window's `restore`.
'none' - no HOLD, no marker: production was never touched by this window or the chain.
"""
if skip_restore(stopped):
return 'chained'
if hold_written:
return 'restore'
return 'abort' if marker_present() else 'none'
def restore_required(stopped: bool, hold_written: bool) -> bool:
"""True whenever production must be brought back: the window's own HOLD, or the chain marker without a clean
chained end (S92 FIX 1). The window's own restore applies when it holds a HOLD; otherwise `abort`."""
return end_action(stopped, hold_written) in ('restore', 'abort')
def allow_unreachable(mode: str) -> bool:
"""HOLD admission rule for a runner start: in mode 'chained' production is down by design, so the admission HOLD
opens with hold_admission.open_script(..., allow_unreachable=True); in mode 'production' it must see :8000 serving."""
if mode not in ('chained', 'production'):
raise ValueError(f'unknown chain preflight mode {mode!r}')
return mode == 'chained'
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _snapshot_commit(path: Path) -> str:
data = record(cast(Json, json.loads(path.read_text())))
commit = data.get('production_commit') or data.get('commit')
if not (isinstance(commit, str) and re.fullmatch('[0-9a-f]{40}', commit)):
raise ValueError(f'{path}: production snapshot lacks a 40-hex production commit')
return commit
def snapshot_ref(path: Path) -> dict[str, Json]:
"""{'path', 'sha256', 'production_commit'} of a production capture (a window's rollback.json)."""
if not path.is_absolute() or not path.is_file():
raise ValueError(f'production snapshot {path} is not an existing absolute file')
return {'path': str(path), 'sha256': _sha256(path), 'production_commit': _snapshot_commit(path)}
def finish_chained(window_out: Path, quiet_proof: Mapping[str, Json], *, placeholder_owner: str,
production_snapshot: Path | None = None) -> None:
"""Write MARKER (temp + fsync + rename) at a clean chained end; the chain placeholders of `placeholder_owner` stay up.
The owner is the one `placeholders_up` proved on both Sparks (`chain_owner()`); a chain keeps one owner, so a marker
that already records another owner refuses.
The marker's production snapshot is, in order: the explicit path; the current marker's snapshot when this window ran
chained (marker present at its start, so it could not capture production); else this window's own rollback.json (it
started from serving production). With none of them the marker records null and `production_snapshot()` refuses
later; an inherited snapshot whose bytes changed refuses now."""
if not (enabled() and not last()):
raise ValueError('finish_chained outside a non-last chained window')
if not quiet_proof:
raise ValueError('chained finish requires the Spark quiet proof')
check_owner(placeholder_owner)
recorded = marker_owner() if marker_present() else None
if recorded is not None and recorded != placeholder_owner:
raise ValueError(f'chain placeholder owner {placeholder_owner} differs from the marker\'s {recorded}')
snapshot: Json = None
if production_snapshot is not None:
snapshot = snapshot_ref(production_snapshot)
elif marker_present():
current = read_marker()
inherited = current.get('production_snapshot') if current is not None else None
if isinstance(inherited, dict):
snapshot = snapshot_ref(Path(str(inherited.get('path'))))
if snapshot['sha256'] != inherited.get('sha256'):
raise ValueError('chain production snapshot changed since it was recorded')
elif (window_out / 'rollback.json').is_file():
snapshot = snapshot_ref(window_out / 'rollback.json')
data: dict[str, Json] = {'schema': SCHEMA, 'window': str(window_out), 'time': time.time(),
'utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'quiet_proof': cast(Json, dict(quiet_proof)), 'production_snapshot': snapshot,
'placeholder_owner': placeholder_owner, 'placeholders': PLACEHOLDERS_KEPT}
MARKER.parent.mkdir(parents=True, exist_ok=True)
temp = MARKER.with_name(MARKER.name + '.new')
with temp.open('w') as handle:
handle.write(json.dumps(data, sort_keys=True, allow_nan=False) + '\n')
handle.flush()
os.fsync(handle.fileno())
os.replace(temp, MARKER)
def read_marker() -> dict[str, Json] | None:
if not MARKER.exists():
return None
data = record(cast(Json, json.loads(MARKER.read_text())))
if data.get('schema') != SCHEMA:
raise ValueError(f'{MARKER}: unknown chain marker schema')
return data
def production_snapshot() -> tuple[Path, dict[str, Json]]:
"""The chain's production capture for a chained window (which cannot capture production while it is down):
(path, parsed rollback.json), after checking the sha256 recorded in the marker. Refuses without a marker."""
marker = read_marker()
if marker is None:
raise ValueError('no chain marker: capture production from the serving server instead')
ref = marker.get('production_snapshot')
if not isinstance(ref, dict):
raise ValueError('chain marker carries no production snapshot (none captured, or written before WIN2-R2)')
path = Path(str(ref.get('path')))
if not path.is_absolute() or not path.is_file() or _sha256(path) != ref.get('sha256'):
raise ValueError(f'chain production snapshot {path} missing or changed since the marker was written')
return path, record(cast(Json, json.loads(path.read_text())))
def preflight(production_serving: bool, hold_exists: bool, *, placeholders_present: bool | None = None) -> str:
"""'chained' (marker, production down, no HOLD, both hold placeholders up) or 'production' (no marker, production
serving); else STOP. A chained preflight must pass the placeholder probe result (`placeholders_present()`)."""
if hold_exists:
raise ValueError('STOP: a HOLD exists; no window may start')
marker = read_marker()
if marker is not None:
if not enabled():
raise ValueError('STOP: production is down in a chain (marker present); run with WINDOW_CHAIN=1 or restore W8')
if production_serving:
raise ValueError('STOP: chain marker present but production is serving; inspect before continuing')
if placeholders_present is None:
raise ValueError('STOP: a chained preflight must prove the hold placeholders on both Sparks '
'(placeholders_present=chain.placeholders_present())')
if not placeholders_present:
raise ValueError('STOP: hold placeholders missing between chained windows; the Sparks looked free while '
'production was down')
return 'chained'
if not production_serving:
raise ValueError('STOP: production not serving and no chain marker')
return 'production'
def abort_attempt() -> Path:
"""Record of the ONE automatic chain abort per stopped chain (S96 FIX 2); clear() removes it."""
return MARKER.with_name(MARKER.name + '.abort')
def clear(*, placeholders_released: bool = False) -> None:
"""Remove the marker. Runners call end_chain() instead, which takes the chain placeholders down first; a marker that
records a placeholder owner refuses here unless the caller states the placeholders were released (end_chain does,
as does the coordinator's `clear --placeholders-released` after removing them by hand)."""
if not placeholders_released and marker_present() and marker_owner() is not None:
raise ValueError('the chain marker records placeholder owner '
f'{marker_owner()}: end the chain with chain.end_chain() (placeholders down, then the marker)')
MARKER.unlink(missing_ok=True)
MARKER.with_name(MARKER.name + '.new').unlink(missing_ok=True)
abort_attempt().unlink(missing_ok=True)
def production_serving(url: str = PRODUCTION_MODELS, timeout: float = 15.0) -> bool:
"""Production answers /v1/models with the K1 cap (the same test the coordinator's prev-ok gate uses)."""
try:
with urllib.request.urlopen(url, timeout=timeout) as response:
data = record(cast(Json, json.loads(response.read(1024 * 1024))))
except (OSError, ValueError):
return False
rows = data.get('data')
return isinstance(rows, list) and any(isinstance(r, dict) and r.get('max_model_len') == K1_CAP for r in rows)
def hold_exists() -> bool:
"""HOLD file on sparkhead (read-only ssh test); an ssh failure counts as 'exists' so nothing starts blind."""
result = subprocess.run(['ssh', '-o', 'BatchMode=yes', 'sparkhead', 'test ! -e ~/dsv41-deploy/HOLD'],
capture_output=True, timeout=30, check=False)
return result.returncode != 0
def check_owner(owner: str) -> str:
if not isinstance(owner, str) or re.fullmatch(OWNER_PATTERN, owner) is None:
raise ValueError(f'invalid chain placeholder owner {owner!r} ({OWNER_PATTERN} required)')
return owner
def new_owner() -> str:
return 'chain-' + time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()) + '-' + uuid.uuid4().hex[:8]
def marker_owner() -> str | None:
"""The placeholder owner the marker records (None without a marker, or for a marker written before WIN2-R3)."""
marker = read_marker()
owner = marker.get('placeholder_owner') if marker is not None else None
return check_owner(owner) if isinstance(owner, str) else None
def chain_owner() -> str:
"""The owner a chained end starts or keeps: the marker's while this window ran chained, else a new chain's.
Call it once per window and keep the value (end_chain needs it if the window fails before finish_chained)."""
if marker_present():
owner = marker_owner()
if owner is None:
raise ValueError('the chain marker records no placeholder owner (written before the WIN2-R3 placeholder fix); '
'restore W8 and start a new chain')
return owner
return new_owner()
def placeholder_script(action: str, owner: str) -> str:
"""One Spark's step as a self-contained `bash -c` command, for plain ssh or a scope's ssh shim alike."""
if action not in _PLACEHOLDER_ACTIONS:
raise ValueError(f'unknown placeholder action {action!r}')
script = ((_PLACEHOLDER_STATE + _PLACEHOLDER_ACTIONS[action] + 'echo "PLACEHOLDER $s"\n')
.replace('@OWNER@', check_owner(owner)).replace('@ACTION@', action)
.replace('@OWNER_FROM@', str(len(PLACEHOLDER_TITLE + ' owner=') + 1))
.replace('@TITLE@', PLACEHOLDER_TITLE).replace('@TMUX@', PLACEHOLDER_TMUX)
.replace('@SESSION@', PLACEHOLDER_SESSION))
return shlex.join(['bash', '-c', script])
Run = Callable[[str, str], tuple[int, str]]
def plain_ssh(host: str, command: str) -> tuple[int, str]:
"""An ordinary (unscoped) ssh from the calling process, as the runners and the coordinator gate use."""
try:
result = subprocess.run(['ssh', '-o', 'BatchMode=yes', host, command], capture_output=True, text=True,
timeout=60, check=False)
except subprocess.TimeoutExpired:
return 124, 'ssh timed out after 60 s'
return result.returncode, result.stdout + result.stderr
def placeholder_states(action: str, owner: str, *, run: Run = plain_ssh,
hosts: tuple[str, ...] = PLACEHOLDER_HOSTS) -> dict[str, str]:
"""{host: state} for one action on every Spark; a missing state line is 'ERROR rc=<rc>: <output tail>'."""
command = placeholder_script(action, owner)
states: dict[str, str] = {}
for host in hosts:
rc, text = run(host, command)
lines = [line for line in text.splitlines() if line.startswith('PLACEHOLDER ')]
states[host] = lines[-1].removeprefix('PLACEHOLDER ') if lines else f'ERROR rc={rc}: {text.strip()[-300:]}'
return states
def placeholders_up(owner: str, *, run: Run = plain_ssh) -> dict[str, str]:
"""Start (or keep) this chain's placeholders on both Sparks and prove them; raises unless both are UP.
Call from the runner's own unscoped process (plain ssh) BEFORE the first fence of the window's `-hold` scope, so the
Sparks never look free. A placeholder of another owner (or of none) refuses and is left alone."""
states = placeholder_states('up', owner, run=run)
if not all(state.startswith('UP ') for state in states.values()):
raise ValueError(f'chain placeholders of {owner} not up on both Sparks: {states}')
return states
def placeholders_present(owner: str | None = None, *, run: Run = plain_ssh) -> bool:
"""Both Sparks run THIS chain's placeholder (default socket; owner from the marker unless given). Absent, foreign,
broken or unreachable counts as missing; so does a marker without an owner."""
if owner is None:
owner = marker_owner()
if owner is None:
return False
return all(state.startswith('UP ') for state in placeholder_states('probe', owner, run=run).values())
def placeholders_down(owner: str, *, run: Run = plain_ssh) -> dict[str, str]:
"""Stop this chain's placeholders (from an unscoped process). DOWN or ABSENT per host counts as removed; a foreign or
broken placeholder is left alone and reported."""
return placeholder_states('down', owner, run=run)
def released(states: Mapping[str, str]) -> bool:
return bool(states) and all(state in ('DOWN', 'ABSENT') for state in states.values())
def chained_close(owner: str) -> str:
"""Shared "close our own HOLD without production" (R-NBW request): window.sh close refuses without :8000 and would kill
placeholders. Run it ONLY inside the window's own `-restore-close` scope on sparkhead
(owner.command('launch', f'{tag}-restore-close', ['bash', '-c', chain.chained_close(owner)], ...)): it proves this
chain's placeholders on both Sparks, then removes ONLY the HOLD file, which that scope's rm shim RECORDS (refusing a
HOLD of another window); the runner's commit-close removes it after every phase is fenced and proved absent.
Through the scope's ssh shim each Spark's check runs in a scope of the same tag whose `tmux` is the shim (private
socket); the probe calls PLACEHOLDER_TMUX, so it sees the default-socket chain placeholders."""
probe = shlex.quote(placeholder_script('probe', owner))
return ('set -euo pipefail; cd ~/dsv41-deploy; set -a; . ./dsv41.env; set +a; '
'for node in "$HEAD" "$WORKER"; do '
f'state=$(ssh -o BatchMode=yes "$node" {probe} | tail -n 1) '
'|| { echo "hold placeholder probe failed on $node"; exit 1; }; '
'case "$state" in "PLACEHOLDER UP "*) ;; '
f'*) echo "hold placeholder of {owner} missing on $node: $state"; exit 1 ;; esac; done; '
f'rm -f "$HOME/dsv41-deploy/HOLD"; echo {CHAINED_CLOSE_OK}')
def end_chain(owner: str | None = None, *, run: Run = plain_ssh) -> dict[str, Json]:
"""The chain's last step, ONLY after the strict W8 restore verified AND the post-close watch PASSED (a window's own
restore, or abort): take this chain's placeholders down on both Sparks, then clear the marker.
`owner` defaults to the marker's; a window that started placeholders before any marker recorded them (its chained end
failed before finish_chained) passes its own. A foreign, broken or unreachable placeholder is left alone and the result
says ALERT; the marker is cleared either way, because production is verified serving."""
result: dict[str, Json] = {}
if owner is None and marker_present():
try:
owner = marker_owner()
except (OSError, ValueError) as exc:
result['marker_read'] = f'unreadable: {type(exc).__name__}: {exc}'
result['placeholder_owner'] = owner
if owner is None:
result['placeholders'] = 'NONE: no placeholder owner recorded'
result['status'] = ('ENDED WITH ALERT: marker unreadable, its placeholders (if any) not removed'
if 'marker_read' in result else 'ENDED: no chain placeholders to remove')
else:
try:
states = placeholders_down(check_owner(owner), run=run)
except (OSError, subprocess.SubprocessError, ValueError) as exc:
states = {'error': f'{type(exc).__name__}: {exc}'}
result['placeholders'] = cast(Json, dict(states))
result['status'] = ('ENDED: chain placeholders down on both Sparks' if released(states) else
'ENDED WITH ALERT: chain placeholders not removed (left alone); coordinator action required')
clear(placeholders_released=True)
result['marker'] = 'cleared'
return result
def gate(serving: Callable[[], bool] = production_serving, hold: Callable[[], bool] = hold_exists,
placeholders: Callable[[], bool] = placeholders_present) -> str:
"""Between-window gate: 'chained' or 'production' (see preflight); raises otherwise. The placeholders (this chain's
owner, default socket) are probed only while the chain marker is present."""
present: bool | None = None
if marker_present():
if marker_owner() is None:
raise ValueError('STOP: the chain marker records no placeholder owner (written before the WIN2-R3 placeholder '
'fix), so its placeholders cannot be proved; restore W8')
present = placeholders()
return preflight(production_serving=serving(), hold_exists=hold(), placeholders_present=present)
Recover = Callable[[Path, Callable[[str], None]], Mapping[str, Json]]
Watch = Callable[[Path, Callable[[str], None]], Mapping[str, Json]]
def _tag(out: Path, suffix: str) -> str:
"""Scoped-recovery id, unique per window (parent and own directory names), at most 64 characters."""
base = re.sub('[^A-Za-z0-9_-]', '-', f'{out.parent.name}-{out.name}')
return (base[-(63 - len(suffix)):] + '-' + suffix).lstrip('-')
def _recover_production(out: Path, log: Callable[[str], None]) -> Mapping[str, Json]:
"""The reviewed post_close_watch.ScopedRecovery: its own HOLD (admission allow-unreachable), production stop, both
Sparks proved quiet, W8 start|check|close each verified by restore_verified (six distinct IDENTICAL), every phase
fenced, HOLD released by commit-close. Any failure raises with that HOLD retained."""
from offload.launch.windows import post_close_watch as watchdog
recovery = watchdog.ScopedRecovery(_tag(out, 'abort'), out / 'abort-commands')
log('CHAIN ABORT: strict W8 recovery under its own scoped HOLD (start, check, close)')
recovery.recover(time.monotonic() + watchdog.RECOVERY_S)
return {'start': 'PASS', 'check': 'PASS', 'close': 'PASS', 'hold': 'own scoped HOLD opened, released by commit-close',
'phases': cast(Json, list(recovery.phases))}
def _watch_production(out: Path, log: Callable[[str], None]) -> Mapping[str, Json]:
from offload.launch.windows import post_close_watch as watchdog
result = watchdog.watch(watchdog.ProductionHealth(),
watchdog.ScopedRecovery(_tag(out, 'watch'), out / 'post-close-commands'), log=log)
return cast(Mapping[str, Json], dict(result.__dict__))
def abort(out: Path, *, hold: Callable[[], bool] = hold_exists, recover: Recover = _recover_production,
watch: Watch = _watch_production, end: Callable[[], Mapping[str, Json]] = end_chain,
log: Callable[[str], None] = print, serving: Callable[[], bool] = production_serving) -> dict[str, Json]:
"""Coordinator chain abort (S92 FIX 1): bring production back after a chain stopped outside a window's own HOLD.
No marker -> nothing to do. A HOLD exists (or its probe fails) -> ALERT and touch nothing: its owner, or the
coordinator by hand, restores. Production serving (S96 FIX 2a: the recovery would drain it, open a HOLD that pauses
K24 and refuse at the guarded stop) -> ALERT and touch nothing. A previous automatic abort of this chain that did not
end RESTORED + watch PASS (S96 FIX 2b: one automatic recovery per stopped chain, the post-close watch rule) -> ALERT;
`abort --again` by hand forgets it. Otherwise the strict W8 restore (start, check, close; `_recover_production`), then the
15-minute post-close watch; end_chain() (chain placeholders down, then the marker) ONLY when every W8 step verified
AND the watch PASSED. Until then the chain placeholders stay up: production is not proved. Writes out/summary.json in
the shape prev-ok.sh / chain_prev_ok.sh read (restore.status RESTORED..., post_close_watch.status PASS).
"""
out.mkdir(parents=True, exist_ok=False)
result: dict[str, Json] = {'schema': 'dsv41-chain-abort-v1', 'utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'marker_path': str(MARKER), 'chain': 'ABORT', 'restore': {'status': 'NOT RUN'}}
def finish(status: str) -> dict[str, Json]:
result['status'] = status
(out / 'summary.json').write_text(json.dumps(result, indent=1, allow_nan=False) + '\n')
log(f'CHAIN ABORT: {status}')
return result
if not marker_present():
result['restore'] = {'status': 'NOT NEEDED: no chain marker'}
return finish('NOT NEEDED: no chain marker; production was not left down by a chain')
try:
result['marker'] = read_marker()
except (OSError, ValueError) as exc:
result['marker'] = f'unreadable: {type(exc).__name__}: {exc}'
if hold():
result['restore'] = {'status': 'NOT RUN: a HOLD exists'}
return finish('ALERT: a HOLD exists (or its probe failed); production is down; chain abort touched nothing; '
'the HOLD owner (or the coordinator by hand) must restore')
if serving():
result['restore'] = {'status': 'NOT RUN: production serving'}
return finish('ALERT: chain marker present but production is serving; chain abort touched nothing; inspect, then '
'`chain end` (placeholders down, marker cleared) once production is the verified K1 restore')
try:
with abort_attempt().open('x') as handle:
handle.write(json.dumps({'out': str(out), 'utc': result['utc']}) + '\n')
except FileExistsError:
result['restore'] = {'status': 'NOT RUN: a previous chain abort did not restore'}
return finish('ALERT: a previous chain abort did not end RESTORED + watch PASS ('
+ abort_attempt().read_text().strip() + '); one automatic recovery per stopped chain; '
'coordinator action required (`chain abort --again` by hand)')
try:
steps = dict(recover(out, log))
except (Exception, KeyboardInterrupt) as exc:
result['restore'] = {'status': 'FAILED', 'error': f'{type(exc).__name__}: {exc}'}
return finish('FAILED: strict W8 recovery unverified; production down; its HOLD (if opened) retained; marker kept')
result['restore'] = {'status': ABORT_RESTORED, **steps}
watched = dict(watch(out, log))
result['post_close_watch'] = cast(Json, watched)
if watched.get('status') != 'PASS':
return finish('RESTORED BUT POST-CLOSE WATCH FAILED: marker kept; coordinator action required')
ended = dict(end())
result['end_chain'] = cast(Json, ended)
result['chain'] = 'ABORTED: strict restore verified, watch PASS, marker cleared'
# Exit 0 for any 'RESTORED: chain ended' (production is verified); an ALERT about placeholders rides in the text.
return finish(f'RESTORED: chain ended by abort; watch PASS; marker cleared; {ended.get("status")}')
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('cmd', choices=('gate', 'status', 'clear', 'present', 'abort', 'placeholders', 'end'))
parser.add_argument('--out', type=Path, help='abort: fresh output directory (summary.json, W8 logs, watch)')
parser.add_argument('--owner', help='placeholders/end: chain placeholder owner (default: the marker\'s)')
parser.add_argument('--placeholders-released', action='store_true',
help='clear: the coordinator removed this chain\'s placeholders by hand')
parser.add_argument('--again', action='store_true',
help='abort, by hand only: forget the previous automatic attempt (S96 FIX 2) and run once more')
args = parser.parse_args(argv)
if args.cmd == 'status':
print(json.dumps({'enabled': enabled(), 'last': last(), 'marker_path': str(MARKER),
'production_url': PRODUCTION_MODELS, 'marker': read_marker()}))
return 0
if args.cmd == 'present':
print('PRESENT' if marker_present() else 'ABSENT')
return 0
if args.cmd == 'clear':
try:
clear(placeholders_released=args.placeholders_released)
except ValueError as exc:
print(f'STOP: {exc}', file=sys.stderr)
return 2
print('chain marker cleared')
return 0
if args.cmd == 'placeholders':
owner = args.owner if args.owner is not None else marker_owner()
if owner is None:
print('STOP: no placeholder owner (marker has none; pass --owner)', file=sys.stderr)
return 2
states = placeholder_states('probe', owner)
print(json.dumps({'owner': owner, 'states': states}))
return 0 if all(state.startswith('UP ') for state in states.values()) else 1
if args.cmd == 'end':
if os.environ.get('WINDOW_COORDINATOR') != COORDINATOR:
print(f'STOP: end is coordinator-only (WINDOW_COORDINATOR={COORDINATOR}); only after a verified restore + '
'watch PASS', file=sys.stderr)
return 2
ended = end_chain(args.owner)
print(json.dumps(ended))
return 0 if str(ended['status']).startswith('ENDED:') else 4
if args.cmd == 'abort':
if os.environ.get('WINDOW_COORDINATOR') != COORDINATOR:
print(f'STOP: chain abort is coordinator-only (WINDOW_COORDINATOR={COORDINATOR})', file=sys.stderr)
return 2
if args.out is None:
print('STOP: abort needs --out', file=sys.stderr)
return 2
if args.again:
abort_attempt().unlink(missing_ok=True)
status = str(abort(args.out)['status'])
if status.startswith(('RESTORED: chain ended', 'NOT NEEDED')):
return 0
return 4 if status.startswith('ALERT') else 3
try:
print(gate())
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 2
return 0
if __name__ == '__main__':
sys.exit(main())
|