a job that cannot mine never burns its budget silently: the elevated job path follows its output file while the script runs (the 5-minute progress reports carry the lines; 0.3.12), and the tune playbook's watchdog fails a run that mines nothing within 120 s of its first status line (the engine's last log line in the RESULT, the tree ended, mining restored by the runner)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c277e6d937
commit
f4370f9112
3 changed files with 76 additions and 2 deletions
|
|
@ -1131,10 +1131,17 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
}
|
||||
cmd.current_dir(&dir);
|
||||
job_env(&mut cmd, shared, job, &dir, data_root);
|
||||
// the elevated script's output reaches this side through a file: follow it while the script runs, so the
|
||||
// 5-minute progress reports carry its lines (6 October 2026: a 35-minute run that never mined showed only
|
||||
// "script running" until it ended; the lines that said why were in the file the whole time)
|
||||
let follow = if elevated { Some(follow_file(sink, out_file.clone())) } else { None };
|
||||
let ran = run_streamed(&mut cmd, sink, ctl, limit, shared, job, started, "script running")?;
|
||||
if elevated {
|
||||
if let Some(f) = follow {
|
||||
f.stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let seen = f.handle.join().unwrap_or(0);
|
||||
// the tail the follower had not read when the script ended
|
||||
if let Ok(t) = std::fs::read_to_string(&out_file) {
|
||||
for l in t.lines() {
|
||||
for l in t.lines().skip(seen) {
|
||||
sink.line(l);
|
||||
}
|
||||
}
|
||||
|
|
@ -1142,6 +1149,39 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
finish_ran(ran, "script")
|
||||
}
|
||||
|
||||
/// Follows a file another process writes (the elevated script's output), feeding each new complete line to the
|
||||
/// sink every 2 s until stopped; returns how many lines it delivered, so the caller can hand over the remainder.
|
||||
struct Follow {
|
||||
stop: Arc<std::sync::atomic::AtomicBool>,
|
||||
handle: std::thread::JoinHandle<usize>,
|
||||
}
|
||||
|
||||
fn follow_file(sink: &Sink, path: PathBuf) -> Follow {
|
||||
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let stop2 = stop.clone();
|
||||
let s = Sink { shared: sink.shared.clone(), id: sink.id.clone(), dir: sink.dir.clone(), log_path: sink.log_path.clone(), file: Mutex::new(std::fs::OpenOptions::new().append(true).open(&sink.log_path).ok()), results: Mutex::new(Vec::new()) };
|
||||
let handle = std::thread::spawn(move || {
|
||||
let mut seen = 0usize;
|
||||
loop {
|
||||
if let Ok(t) = std::fs::read_to_string(&path) {
|
||||
let lines: Vec<&str> = t.lines().collect();
|
||||
// only complete lines (the writer may be mid-line): keep the last one for the next pass unless the
|
||||
// text ends with a newline
|
||||
let complete = if t.ends_with('\n') { lines.len() } else { lines.len().saturating_sub(1) };
|
||||
for l in lines.iter().take(complete).skip(seen) {
|
||||
s.line(l);
|
||||
}
|
||||
seen = seen.max(complete);
|
||||
}
|
||||
if stop2.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break seen;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
});
|
||||
Follow { stop, handle }
|
||||
}
|
||||
|
||||
fn finish_ran(ran: Ran, what: &str) -> Result<Done, String> {
|
||||
match ran.code {
|
||||
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
||||
|
|
|
|||
|
|
@ -161,6 +161,14 @@ maximum, 14,001 MHz memory; 9070 XT present on bus 98 with OFFSET ranges `gmax_r
|
|||
10`). The offset finding changed the AMD mapping (054e041): an offset clock range closes the clock knob and the power
|
||||
ladder runs on a percent scale bounded by `plimit_range`. The re-run follows the 0.3.11 rollout.
|
||||
|
||||
## 8a. Next-cut notes (for the 0.3.12 shipper)
|
||||
|
||||
| Commit | What | Where |
|
||||
|---|---|---|
|
||||
| b671c8b | every `quit:` names its source; Power control alone decides; no cap at start under `--sweep` | main.rs, server.rs, engine.rs (separable) |
|
||||
| e600e63 | a second engine never runs the updater (`IGNEUM_APP_NO_OTA`, implied by `--sweep`) | engine.rs (6 lines, separable) |
|
||||
| 6a8297c | the elevated job path's output file is followed while the script runs, so the 5-minute progress reports carry its lines (a 35-minute run that never mined showed only "script running" on 6 October 2026); the tune playbook's watchdog fails a run that mines nothing within 120 s of its first status line, with the engine's last log line in the RESULT | jobrun.rs `follow_file`, relay/playbooks/ember-tune-pc1.ps1 |
|
||||
|
||||
## 9. Open
|
||||
|
||||
- The NVIDIA clock readback: `nvidia-smi -lgc` is confirmed only through the core clock during the hold (a mean over
|
||||
|
|
|
|||
|
|
@ -132,8 +132,34 @@ function EndTree([int] $procId, [string] $why) {
|
|||
}
|
||||
$seen = 0
|
||||
$rows = 0
|
||||
# the watchdog (coordinator, 6 October 2026): a tune engine that mines nothing for 120 s after its first status line
|
||||
# (every card "waiting" or 0.00 MH/s: no payout address, no worker, no node) fails the job at once with the engine's
|
||||
# last log line in the RESULT, its tree ended, mining restored by the job runner; a job that cannot mine never burns
|
||||
# its budget silently again
|
||||
$firstStatusAt = $null
|
||||
$lastMining = $null
|
||||
$lastEngineLine = ''
|
||||
function EngineTail() { $t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($t) { $l = Get-Content -LiteralPath $t.FullName -Tail 1 -ErrorAction SilentlyContinue; if ($l) { return [string]$l } }; return '' }
|
||||
while (-not $p.HasExited) {
|
||||
Start-Sleep -Seconds 5
|
||||
$tailLine = EngineTail
|
||||
if ($tailLine) { $lastEngineLine = $tailLine }
|
||||
if ($lastEngineLine -match ' status: ') {
|
||||
if (-not $firstStatusAt) { $firstStatusAt = Get-Date }
|
||||
if ($lastEngineLine -match ', mining \|' -or ($lastEngineLine -match '(\d+\.\d+) MH/s' -and [double]$Matches[1] -gt 0)) { $lastMining = Get-Date }
|
||||
}
|
||||
if ($firstStatusAt -and -not $lastMining -and ((Get-Date) - $firstStatusAt).TotalSeconds -gt 120) {
|
||||
Write-Output ('RESULT TUNE error=not_mining reason=no_card_mined_within_120_s_of_the_first_status_line last_log_line=' + ($lastEngineLine -replace '\s+', '_'))
|
||||
EndTree $p.Id 'watchdog: not mining'
|
||||
Write-Output 'RESULT TUNE error=no_rows'
|
||||
exit 3
|
||||
}
|
||||
if ($lastMining -and ((Get-Date) - $lastMining).TotalSeconds -gt 300) {
|
||||
Write-Output ('RESULT TUNE error=stopped_mining reason=every_card_idle_for_300_s last_log_line=' + ($lastEngineLine -replace '\s+', '_'))
|
||||
EndTree $p.Id 'watchdog: stopped mining'
|
||||
Write-Output 'RESULT TUNE error=no_rows'
|
||||
exit 3
|
||||
}
|
||||
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||
while ($seen -lt $all.Count) {
|
||||
$l = [string]$all[$seen]; $seen++
|
||||
|
|
|
|||
Loading…
Reference in a new issue