engine: one pack export at a time (EXPORT_LOCK around both export-pack call sites; the hunk of opencl-rdna4 23810df, whose other changes conflict with gpu-hotplug and wait for the next cut)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d5da02a851
commit
a0864f0842
1 changed files with 73 additions and 0 deletions
|
|
@ -430,6 +430,8 @@ struct MinerSlot {
|
||||||
error_at: Option<Instant>,
|
error_at: Option<Instant>,
|
||||||
/// the per-card watchdog (src/watchdog.rs): one restart, then faulted
|
/// the per-card watchdog (src/watchdog.rs): one restart, then faulted
|
||||||
watch: crate::watchdog::CardWatch,
|
watch: crate::watchdog::CardWatch,
|
||||||
|
/// pack refusals (src/watchdog.rs): the pack is exported again before the restart, capped per epoch
|
||||||
|
pack_rebuilds: crate::watchdog::PackRebuilds,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
|
|
@ -1337,6 +1339,7 @@ impl Engine {
|
||||||
last_status: None,
|
last_status: None,
|
||||||
error_at: None,
|
error_at: None,
|
||||||
watch: crate::watchdog::CardWatch::new(),
|
watch: crate::watchdog::CardWatch::new(),
|
||||||
|
pack_rebuilds: crate::watchdog::PackRebuilds::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.miners.is_empty() {
|
if self.miners.is_empty() {
|
||||||
|
|
@ -2460,6 +2463,7 @@ impl Engine {
|
||||||
if !p.alive() {
|
if !p.alive() {
|
||||||
let code = p.exit_code.unwrap_or(-1);
|
let code = p.exit_code.unwrap_or(-1);
|
||||||
let tail = tail_of(&p.log_path, 3);
|
let tail = tail_of(&p.log_path, 3);
|
||||||
|
let long_tail = if code == crate::watchdog::PACK_OUT_OF_DATE_CODE { tail_of(&p.log_path, 40) } else { Vec::new() };
|
||||||
self.miners[i].proc = None;
|
self.miners[i].proc = None;
|
||||||
let t = self.secs(now);
|
let t = self.secs(now);
|
||||||
let verdict = self.miners[i].watch.event(t, crate::watchdog::Event::Exited(code));
|
let verdict = self.miners[i].watch.event(t, crate::watchdog::Event::Exited(code));
|
||||||
|
|
@ -2469,6 +2473,37 @@ impl Engine {
|
||||||
self.miners[i].needs_rebuild = true;
|
self.miners[i].needs_rebuild = true;
|
||||||
}
|
}
|
||||||
self.miners[i].restart_at = Some(now);
|
self.miners[i].restart_at = Some(now);
|
||||||
|
} else if code == crate::watchdog::PACK_OUT_OF_DATE_CODE {
|
||||||
|
// The worker refused its program pack and the miner could not rebuild it (or hit its own
|
||||||
|
// cap): export the pack from the node again before the next start, not a blind restart;
|
||||||
|
// at most PACK_REBUILD_CAP times per epoch, then the card shows the reason
|
||||||
|
let why = long_tail.iter().rev().find_map(|l| crate::watchdog::pack_refusal(l)).unwrap_or_else(|| "the worker refused its program pack".into());
|
||||||
|
let epoch = std::fs::read_to_string(self.shared.runtime.app_dir.join("packs").join("devnet").join("seeds.txt")).ok().and_then(|s| crate::watchdog::pack_epoch_of(&s)).unwrap_or_default();
|
||||||
|
let name = self.st().mining.cards.get(card_idx).map(|c| c.name.clone()).unwrap_or_else(|| self.miners[i].label.clone());
|
||||||
|
match self.miners[i].pack_rebuilds.decide(&epoch, &why) {
|
||||||
|
crate::watchdog::PackAction::Rebuild { n, cap } => {
|
||||||
|
self.shared.event("build", "program pack out of date, rebuilding");
|
||||||
|
self.shared.log(&format!("{name}: program pack out of date, rebuilding (export {n} of {cap} for epoch {epoch}): {why}"));
|
||||||
|
self.miners[i].prepared = false; // prepare_worker exports the pack again before the start
|
||||||
|
self.miners[i].restart_at = Some(now);
|
||||||
|
if let Some(c) = self.st().mining.cards.get_mut(card_idx) {
|
||||||
|
c.state = "restarting".into();
|
||||||
|
c.hash_now = 0.0;
|
||||||
|
c.message = "program pack out of date, rebuilding".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::watchdog::PackAction::GiveUp { n: _, reason } => {
|
||||||
|
self.shared.event("error", &format!("{name}: {reason}"));
|
||||||
|
self.shared.log(&format!("{name}: {reason}; next try in 10 minutes or at the next hour"));
|
||||||
|
self.miners[i].prepared = false;
|
||||||
|
self.miners[i].restart_at = Some(now + Duration::from_secs(600));
|
||||||
|
if let Some(c) = self.st().mining.cards.get_mut(card_idx) {
|
||||||
|
c.state = "failed".into();
|
||||||
|
c.hash_now = 0.0;
|
||||||
|
c.message = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if verdict != crate::watchdog::Action::None {
|
} else if verdict != crate::watchdog::Action::None {
|
||||||
// exit 43: the miner gave up on its worker; once more, then the card is faulted
|
// exit 43: the miner gave up on its worker; once more, then the card is faulted
|
||||||
self.watchdog_verdict(i, verdict, &tail);
|
self.watchdog_verdict(i, verdict, &tail);
|
||||||
|
|
@ -2546,6 +2581,25 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The strip, the log and the card for a program pack the worker refused or the miner found stale: the miner
|
||||||
|
/// rebuilds the pack and restarts the worker itself (or exits 44 for the app to export it). One strip line per
|
||||||
|
/// 30 s: the miner prints the refusal on stderr and its own line on stdout.
|
||||||
|
fn pack_notice(&mut self, i: usize, card: usize, card_name: &str, why: &str) {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.shared.log(&format!("{card_name}: program pack out of date, rebuilding: {why}"));
|
||||||
|
if now.duration_since(self.last_error_event) >= Duration::from_secs(30) {
|
||||||
|
self.last_error_event = now;
|
||||||
|
self.shared.event("build", "program pack out of date, rebuilding");
|
||||||
|
}
|
||||||
|
self.miners[i].error_at = Some(now);
|
||||||
|
let t = self.secs(now);
|
||||||
|
self.miners[i].watch.event(t, crate::watchdog::Event::WorkerRestart("program pack out of date, rebuilding"));
|
||||||
|
if let Some(c) = self.st().mining.cards.get_mut(card) {
|
||||||
|
c.hash_now = 0.0;
|
||||||
|
c.message = "program pack out of date, rebuilding".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Applies a watchdog verdict to slot `i` (its process already stopped or gone): a restart now with the reason on
|
/// Applies a watchdog verdict to slot `i` (its process already stopped or gone): a restart now with the reason on
|
||||||
/// the card, or the card marked faulted with the reason in the UI and the log while the other cards keep mining.
|
/// the card, or the card marked faulted with the reason in the UI and the log while the other cards keep mining.
|
||||||
fn watchdog_verdict(&mut self, i: usize, verdict: crate::watchdog::Action, tail: &[String]) {
|
fn watchdog_verdict(&mut self, i: usize, verdict: crate::watchdog::Action, tail: &[String]) {
|
||||||
|
|
@ -2867,6 +2921,10 @@ impl Engine {
|
||||||
if let Some(c) = self.st().mining.cards.get_mut(card) {
|
if let Some(c) = self.st().mining.cards.get_mut(card) {
|
||||||
c.message = "the node is not answering; the miner retries".into();
|
c.message = "the node is not answering; the miner retries".into();
|
||||||
}
|
}
|
||||||
|
} else if let Some(why) = crate::watchdog::pack_refusal(text) {
|
||||||
|
// the worker refused its program pack; the miner rebuilds the pack and restarts the worker itself
|
||||||
|
// (or exits 44 for us to export it): the strip and the card name the condition in plain words
|
||||||
|
self.pack_notice(i, card, &card_name, &why);
|
||||||
} else if text.contains("WORKER MISMATCH") || text.contains("worker error") || text.contains("worker exited") || text.contains("worker killed by a guard") || text.contains("panicked") || text.contains("CUDA error") || text.contains("submit error") {
|
} else if text.contains("WORKER MISMATCH") || text.contains("worker error") || text.contains("worker exited") || text.contains("worker killed by a guard") || text.contains("panicked") || text.contains("CUDA error") || text.contains("submit error") {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now.duration_since(self.last_error_event) >= Duration::from_secs(30) {
|
if now.duration_since(self.last_error_event) >= Duration::from_secs(30) {
|
||||||
|
|
@ -2889,6 +2947,12 @@ impl Engine {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if text.contains("PACK OUT OF DATE") {
|
||||||
|
// the miner found its pack stale or refused (stdout): it rebuilds the pack before the restart
|
||||||
|
let why = crate::watchdog::pack_refusal(text).unwrap_or_else(|| short(text, 160));
|
||||||
|
self.pack_notice(i, card, &card_name, &why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if text.contains(" WORKER FAULT ") {
|
if text.contains(" WORKER FAULT ") {
|
||||||
// the miner's guards (interval, job time, cpu re-check, stall) killed the worker; it restarts it itself
|
// the miner's guards (interval, job time, cpu re-check, stall) killed the worker; it restarts it itself
|
||||||
let reason = crate::watchdog::fault_reason(text).unwrap_or_else(|| short(text, 160));
|
let reason = crate::watchdog::fault_reason(text).unwrap_or_else(|| short(text, 160));
|
||||||
|
|
@ -3398,9 +3462,17 @@ fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||||
(if m <= 2 { y + 1 } else { y }, m, d)
|
(if m <= 2 { y + 1 } else { y }, m, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One pack export at a time (5 October 2026). prepare_worker runs on a thread per card, so two cards starting
|
||||||
|
/// together ran two `igneum-miner export-pack` processes into the same folder; across an epoch change they
|
||||||
|
/// interleaved and PC 1's packs\devnet was left with one epoch's program.h and the other's seeds.txt, which every
|
||||||
|
/// OpenCL worker start then refused ("the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT") until the next
|
||||||
|
/// export. The second export of a pair rewrites the same pack, which is harmless.
|
||||||
|
static EXPORT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||||
|
|
||||||
/// Exports this hour's program pack from the node to <app data>\packs\devnet (the prebuilt workers read it with --pack).
|
/// Exports this hour's program pack from the node to <app data>\packs\devnet (the prebuilt workers read it with --pack).
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn export_pack(shared: &Arc<Shared>, bins: &Bins) -> Result<(), String> {
|
fn export_pack(shared: &Arc<Shared>, bins: &Bins) -> Result<(), String> {
|
||||||
|
let _one_at_a_time = EXPORT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let pack = shared.runtime.app_dir.join("packs").join("devnet");
|
let pack = shared.runtime.app_dir.join("packs").join("devnet");
|
||||||
let _ = std::fs::create_dir_all(&pack);
|
let _ = std::fs::create_dir_all(&pack);
|
||||||
let out = crate::detect::run_timeout(std::process::Command::new(&bins.miner).args(["export-pack", &shared.runtime.rpc_url(), &pack.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
|
let out = crate::detect::run_timeout(std::process::Command::new(&bins.miner).args(["export-pack", &shared.runtime.rpc_url(), &pack.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
|
||||||
|
|
@ -3419,6 +3491,7 @@ fn build_worker_from_source(shared: &Arc<Shared>, bins: &Bins, vendor: &str) ->
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
let _one_at_a_time = EXPORT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let pack = shared.runtime.app_dir.join("packs").join("devnet");
|
let pack = shared.runtime.app_dir.join("packs").join("devnet");
|
||||||
let _ = std::fs::create_dir_all(&pack);
|
let _ = std::fs::create_dir_all(&pack);
|
||||||
let out = crate::detect::run_timeout(Command::new(&bins.miner).args(["export-pack", &shared.runtime.rpc_url(), &pack.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
|
let out = crate::detect::run_timeout(Command::new(&bins.miner).args(["export-pack", &shared.runtime.rpc_url(), &pack.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue