app: an update published over an hour before the engine started skips the hourly rollout slot
PC 1, 6 October 2026 06:58:47Z: the app came up after the 0.3.11 publish, had the installer verified 44 s later and sat on "installs at the next safe moment" with its slot at minute 35; the project lead pressed Install now at 07:00:48Z. The slot staggers a fleet through a NEW publish; a machine that was off through the publish has nothing to stagger. Now: published_at + 3,600 s <= engine start => slot_ok, logged once. The other safe-moment guards (node synced, miner idle, boundary, network drop) are unchanged. manifest::unix_from_rfc3339 with tests. For 0.3.12. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f8088c49c3
commit
50a08e8d50
2 changed files with 54 additions and 1 deletions
|
|
@ -546,3 +546,42 @@ mod tests {
|
|||
assert_eq!(fingerprint("zz"), "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix seconds of a manifest `published_at` ("2026-10-04T13:00:00Z", whole seconds, UTC); `None` for any other shape.
|
||||
pub fn unix_from_rfc3339(t: &str) -> Option<u64> {
|
||||
let t = t.trim();
|
||||
let b = t.as_bytes();
|
||||
if b.len() < 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' || !t.ends_with('Z') {
|
||||
return None;
|
||||
}
|
||||
let n = |a: usize, z: usize| t[a..z].parse::<i64>().ok();
|
||||
let (y, m, d, hh, mm, ss) = (n(0, 4)?, n(5, 7)?, n(8, 10)?, n(11, 13)?, n(14, 16)?, n(17, 19)?);
|
||||
if !(1..=12).contains(&m) || !(1..=31).contains(&d) || hh > 23 || mm > 59 || ss > 60 {
|
||||
return None;
|
||||
}
|
||||
// days from civil (Howard Hinnant), valid for every date after 1970
|
||||
let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
|
||||
let era = y2.div_euclid(400);
|
||||
let yoe = y2 - era * 400;
|
||||
let doy = (153 * m2 + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
let days = era * 146097 + doe - 719468;
|
||||
if days < 0 {
|
||||
return None;
|
||||
}
|
||||
Some((days as u64) * 86400 + (hh as u64) * 3600 + (mm as u64) * 60 + ss as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rfc3339_tests {
|
||||
use super::unix_from_rfc3339;
|
||||
#[test]
|
||||
fn a_manifest_publish_time_parses_to_unix_seconds() {
|
||||
assert_eq!(unix_from_rfc3339("1970-01-01T00:00:00Z"), Some(0));
|
||||
assert_eq!(unix_from_rfc3339("2026-10-05T23:57:49Z"), Some(1791244669));
|
||||
assert_eq!(unix_from_rfc3339("2026-10-04T13:00:00Z"), Some(1791118800));
|
||||
assert_eq!(unix_from_rfc3339(""), None);
|
||||
assert_eq!(unix_from_rfc3339("2026-10-05 23:57:49"), None);
|
||||
assert_eq!(unix_from_rfc3339("2026-13-05T23:57:49Z"), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ use std::process::Command;
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A manifest published this long before the engine started is a catch-up: the hourly rollout slot does not apply.
|
||||
const CATCH_UP_AFTER_S: u64 = 3600;
|
||||
const HEALTHY_AFTER_S: u64 = 90;
|
||||
const CHECK_EVERY_S: u64 = 3600;
|
||||
const RETRY_AFTER_ERROR_S: u64 = 600;
|
||||
|
|
@ -117,6 +119,11 @@ pub struct Updater {
|
|||
deferred_until: Option<Instant>,
|
||||
/// this machine's minute of the hour for applying (manifest::slot_minute of the machine id)
|
||||
slot: u64,
|
||||
/// When this engine started (unix seconds): an update published more than an hour before it is a catch-up, not a
|
||||
/// rollout, and skips the hourly slot (the project lead's morning of 6 October 2026: PC 1 came up after the 0.3.11 publish and
|
||||
/// sat on "installs at the next safe moment" until he pressed Install now).
|
||||
started_unix: u64,
|
||||
catch_up_logged: bool,
|
||||
/// identity counts from /api/live over the last 10 minutes, sampled while an update is ready
|
||||
live_samples: Vec<(Instant, u64)>,
|
||||
live_next: Instant,
|
||||
|
|
@ -163,6 +170,8 @@ impl Updater {
|
|||
apply_launched: None,
|
||||
deferred_until: None,
|
||||
slot: manifest::slot_minute(&shared.runtime.id8()),
|
||||
started_unix: crate::platform::unix_now(),
|
||||
catch_up_logged: false,
|
||||
live_samples: Vec::new(),
|
||||
live_next: now,
|
||||
live_busy: false,
|
||||
|
|
@ -519,7 +528,12 @@ impl Updater {
|
|||
}
|
||||
};
|
||||
let minute = (crate::platform::unix_now() / 60) % 60;
|
||||
let slot_ok = minute == self.slot || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||
let catch_up = self.manifest.as_ref().and_then(|m| manifest::unix_from_rfc3339(&m.published_at)).map(|p| p + CATCH_UP_AFTER_S <= self.started_unix).unwrap_or(false);
|
||||
if catch_up && !self.catch_up_logged {
|
||||
self.catch_up_logged = true;
|
||||
shared.log(&format!("update: {} was published over an hour before this start, so it installs at the first safe moment (no hourly slot)", self.version()));
|
||||
}
|
||||
let slot_ok = minute == self.slot || catch_up || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
||||
if !self.auto && !urgent && !self.install_asked {
|
||||
|
|
|
|||
Loading…
Reference in a new issue