Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions crates/app/src/source/rotorhazard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ const DRAIN_INTERVAL: Duration = Duration::from_millis(100);
/// on the wait (the drain loop still runs regardless — this only bounds the staging settle).
const STAGE_SETTLE: Duration = Duration::from_secs(15);

/// How often to RE-EMIT `stage_race` while staged-but-not-RACING inside the settle window — a
/// busy RH silently drops a stage ("status is not 'ready'"); without the retry the race never
/// starts on the RH side and no passes are ever recorded. Paced ABOVE RH's stock staging
/// sequence (~3-4s with unzeroed delays) so a retry can never land mid-staging and restart the
/// countdown it is waiting on; with the instant-start prepare applied, staging is sub-second
/// and the retry only ever fires on a genuinely dropped stage.
const STAGE_RETRY_INTERVAL: Duration = Duration::from_secs(5);

/// How long the driver keeps routing drained events into a **finishing** heat's sink after it
/// stopped the RH race (marshaling path-2). Stopping the race drives RH to `DONE`, which
/// auto-triggers the dense `current_marshal_data` / `save_laps` → `race_list` → `get_pilotrace`
Expand Down Expand Up @@ -131,6 +139,14 @@ struct ArmedHeat {
sink: PassSink,
/// Set by the driver once it has staged the RH race for this arming.
staged: bool,
/// Set once RH confirmed **RACING for this arming** (its READY/DONE → RACING transition —
/// the adapter's `SessionStarted`). Lap records drained BEFORE this are a stale replay:
/// a busy RH ("stage while status is not 'ready'") re-broadcasts the PREVIOUS race's
/// `current_laps` snapshot, and remapping those into the new heat recorded the last
/// race's laps as this race's passes (observed live: a fresh heat opening with lap 6+).
/// Lives in the shared slot so a mid-race reconnect (where RH re-sends the LIVE race's
/// snapshot for the carried adapter to dedup) keeps flowing.
started: bool,
/// Set by [`disarm`](RhConnection::disarm) when the heat left `Running`: the driver finishes the
/// heat by **stopping the RH race** (driving it to `DONE`, which auto-triggers the dense
/// `current_marshal_data`/`get_pilotrace` marshal pull — marshaling path-2), keeps routing the
Expand Down Expand Up @@ -297,6 +313,7 @@ impl RhConnection {
lineup,
sink,
staged: false,
started: false,
finishing: false,
done: false,
});
Expand Down Expand Up @@ -569,6 +586,9 @@ fn maintain(
let mut last_activity = Instant::now();
let mut probed_since_activity = false;
let mut stage_deadline: Option<Instant> = None;
// Paces the busy-RH stage retry (see STAGE_RETRY_INTERVAL); seeded in the past so the
// first retry fires as soon as it is needed.
let mut last_stage_retry = Instant::now() - STAGE_RETRY_INTERVAL;
// The settle window for a **finishing** heat (disarmed): the deadline by which the heat's sink
// stays armed after the RH race is stopped, so the DONE-triggered dense marshal pull lands in
// the right heat's log before the slot clears. `None` ⇒ no heat is finishing.
Expand Down Expand Up @@ -668,6 +688,19 @@ fn maintain(
matches!(slot.as_ref(), Some(heat) if !heat.staged)
};
if do_stage {
// Re-zero the staging delays of the format RH will ACTUALLY race, right before the
// stage. The Stage-time prepare targeted the format that was current THEN — but
// seating the heat can switch RH's effective format (a heat with a class races the
// CLASS's format, RHRace's class_format_id override), whose stock multi-second
// staging sequence then ran on top of Grid's start procedure: every race began
// ~5-7s after Grid's go (lap stamps stayed self-consistent, so the skew was
// invisible in results — but live laps, tones, and callouts all arrived that much
// late; a DB-bloated RH stretched it past 15s). By now the seat's race_status has
// long since folded, so `prepare_instant_start` targets the effective format —
// and RH is READY here, which `alter_race_format` requires. Idempotent.
if conn.prepare_instant_start().is_err() {
return true;
}
// Drop any churn accumulated since the prepare so it isn't remapped as race passes.
let _ = conn.events();
if conn.stage_race().is_err() {
Expand All @@ -689,6 +722,23 @@ fn maintain(
} else if armed.lock().expect("armed-heat lock poisoned").is_none() {
// Nothing armed: clear any stale stage wait.
stage_deadline = None;
} else if stage_deadline.is_some() {
// Staged but RH has not confirmed RACING yet (no SessionStarted this arming): a
// busy RH — the previous race's dense save still settling — logs "Attempted to
// stage race while status is not 'ready'" and DROPS the stage on the floor. The
// race would then never start (and, before the pass gate above, the replayed old
// snapshot contaminated the new heat). Re-emit the stage every couple of seconds
// until RH takes it or the settle window gives up.
let needs_restage = {
let slot = armed.lock().expect("armed-heat lock poisoned");
matches!(slot.as_ref(), Some(heat) if heat.staged && !heat.started)
};
if needs_restage && last_stage_retry.elapsed() >= STAGE_RETRY_INTERVAL {
last_stage_retry = Instant::now();
if conn.stage_race().is_err() {
return true;
}
}
}
if just_staged {
last_activity = Instant::now();
Expand Down Expand Up @@ -796,6 +846,16 @@ fn maintain(
for event in drained {
if matches!(event, Event::SessionStarted { .. }) {
saw_start = true;
heat.started = true;
}
// Gate LAP RECORDS on RH having gone RACING for THIS arming: anything
// earlier is the previous race's snapshot replayed by a still-busy RH —
// remapping it used to contaminate the fresh heat with the last race's
// laps. (Processed in drain order, so passes in the same batch AFTER the
// RACING transition flow normally; signal facts always flow — a pre-start
// trace baseline is harmless and useful.)
if matches!(event, Event::Pass(_)) && !heat.started {
continue;
}
if let Some(remapped) = remap(event, &heat.lineup, &adapter) {
if heat.sink.append_event(remapped).is_err() {
Expand Down