RTE RBC 2oo2 Reference Application 0.1.0 build 0006685
SIL 4 2oo2 Dual-Site Fault-Tolerant ERTMS Radio Block Centre Reference Architecture
Loading...
Searching...
No Matches
Hot/warm standby: design investigation (2026-09-24)

Status: investigation only, no vital code changed.** Root TODO.dox's own framing of this item ("not a simple flip... needs STANDBY to independently compute its own `sessions[]`/`db` first") is correct, and this document exists to turn that framing into a concrete, reviewable design before anyone touches ab_gp_logic.c/ab_gp_proc_*.c (SIL 4-target vital computation) to act on it. See "Why this is a design doc, not a patch" at the end for why this session stopped here.

Current architecture (as-is, already accurately documented in the code)

  • ab_gp_channel_negotiate.c's build_state_transfer_registry() own header comment already states the constraint precisely: only the ONLINE replica runs the full ma_logic/ERTMS computation; STANDBY has no independently-computed sessions[]/db of its own. A promotion (apply_state_transfer(), ADR-029/REQ-RBC-006) works around this by unconditionally overwriting the newly-promoted site's sessions[] from a snapshot the old ONLINE side staged into the negotiation payload every cycle - the transfer, not independent computation, is what currently satisfies "a promoted site must not forget an in-flight Movement Authority."
  • standby_mode (rte_redundancy_config_t, "hot"/"warm"/"cold") is parsed and stored but not acted on anywhere in RBC_GP - build_state_transfer_registry() always registers both fields regardless of its value, which is honest (skipping registration for "hot" today would silently violate REQ-RBC-006 under the current architecture), not a bug. Update 2026-10-07:** standby_mode is acted on since the warm-standby work: warm or hot sets rte_site_role_config_t::standby_sync (ab_gp_channel_negotiate_init()), which adds the pre-commit snapshot send and the 3-cycle staleness rule; both fields are still always registered. See site redundancy and A/B agreement, section 5.9.

The promising finding this investigation turned up

STANDBY already receives the same external input data ONLINE does - it just discards it immediately, unprocessed.** pre_execute.c's on_relay_envelope_received() (the callback that decodes every train/IL/CTC envelope arriving from Gateway/role C over the relay links) opens with:

if (!ctx->is_online)
{
return;
}

Gateway/role C relays every envelope to both A and B regardless of which one is currently ONLINE (ab_gp_channel_poll_relays() runs and drains the queue either way - the log line literally says "standby, not processed") - it has to, since role C has no idea which site/role is ONLINE for a given train at relay time. So the data STANDBY would need to compute its own sessions[]/db independently is already arriving at STANDBY's own process today and is thrown away at this one gate, not missing from the transport.

This changes the shape of the problem: hot standby is not "give STANDBY a new data feed," it is "stop discarding the feed STANDBY already gets, and run the same computation ONLINE runs against it - while making sure only ONLINE's own <em>output</em> (the actual train/IL/CTC-facing response, gated separately and already correctly in <tt>post_execute.c</tt>'s <tt>stage_peer_sample_if_online()</tt> / <tt>send_il_keepalive_if_online()</tt> / GA output draining) reaches the outside world."

Proposed shape (design, not yet implemented)

  1. Split "process this input" from "emit a response" in pre_execute.c/execute.c. Move the !ctx->is_online early-return out of on_relay_envelope_received()'s own input-decode path; let it decode and apply every envelope to ctx->sessions[]/ctx->db (and run whatever ma_logic/ERTMS procedure computation that implies) regardless of is_online. The existing is_online gates already in post_execute.c (output staging/sending) stay exactly as they are today - they are the actual point where "only ONLINE talks to the outside world" is enforced, and they are already correct.
  2. Keep the transfer-based REQ-RBC-006 mechanism as the primary safety guarantee, not replaced by this. apply_state_transfer() still unconditionally overwrites the promoted site's sessions[] from the peer's transferred snapshot on every promotion, exactly as today. Once STANDBY genuinely computes its own sessions[]/db independently, this transfer becomes a cross-check** (do the newly-promoted site's own independently-computed state and the old-ONLINE-side's transferred snapshot agree?) rather than the sole source of truth - a strictly additive safety improvement, not a change to what's already verified. A disagreement here would be a genuinely interesting new signal (either a real divergence bug, or evidence the two sides received input in a different order/timing) worth its own handling policy - not designed here.
  3. standby_mode == "hot" then genuinely means something: build_state_transfer_registry() could skip re-registering/re-transferring sessions[]/db on promotion when STANDBY already has current, itself-computed values agreeing with the transfer - faster promotion (no snapshot-decode step), matching what "hot" standby is actually supposed to buy over "cold." "warm" might mean "compute at a reduced cadence" (not every cycle) - a real middle ground this design doc does not attempt to fully specify.

Open questions - must be answered before implementation, not assumed

  1. Is ma_logic/the ERTMS procedure engine (ab_gp_proc_*.c, ab_gp_logic.c) free of any side effect beyond returning updated sessions[]/db state? A/B are separate OS processes (no shared memory), so there is no cross-process race from running the same computation twice - but if any procedure function touches something with a side effect meaningful beyond its own process (a shared sequence counter later compared cross-process via checkpoint/ cross-compare in a way that assumes only ONLINE ever advances it, a watchdog kick tied to "real" work happening, an NVM write) then running it unconditionally on STANDBY too could have consequences beyond "compute a value and discard it." This needs a real audit of that code, not an assumption either way - not done here.
  2. CPU/timing budget: running the full computation pipeline on both replicas roughly doubles the per-process compute cost of whichever cycle work today only ONLINE does. RTE_EXAMPLE_ AB_CYCLE_PERIOD_MS's existing budget was sized against ONLINE-only load; whether STANDBY doing the same work fits inside the same cycle budget on real target hardware (not just this session's own dev machine) needs measuring, not assuming.
  3. Does the checkpoint/cross-compare mechanism need to know about this? Today's checkpoint marks (RTE_CHECKPOINT_MARK()) are placed at points in the ONLINE-only code path; if STANDBY starts running the same computation, whether/how that should show up in checkpoint traffic (or deliberately not) is a real design question, not obvious from this investigation alone.
  4. What "agree" means for the cross-check in point 2 above (exact match required? a tolerance? which fields?) needs its own decision, informed by what real divergence (if any) looks like once independent computation is actually running - not something to specify blind, before there is real data to look at.

Why this is a design doc, not a patch

ab_gp_logic.c/ab_gp_proc_*.c are SIL 4-target vital computation deciding real Movement Authorities - the highest-stakes code in this workspace. Open question 1 above in particular cannot be answered by reading code quickly; it needs a careful, deliberate audit pass of its own, ideally with a second reviewer, before the on_relay_envelope_received() gate described in step 1 is actually moved. Producing that audit and the resulting patch is real follow-up work, not something to rush through in the same pass that found the promising input-data finding above.

Audit of open question 1 (2026-09-24) - side effects of the ONLINE computation

Static read of ma_logic/, ertms_procedures/, state_machines/, train/, db/ (grep for statics, I/O, channel sends, logging, timers, watchdog, safestate). Not a substitute for the second-reviewer audit this document asks for - one reader, one pass, grep-driven.

  • Module-level state is process-local only: g_validators (ab_gp_logic.c), s_train_contexts (ab_gp_ertms_engine.c), g_train_context_subscribers, g_route_context_subscribers. A and B are separate processes, so running the same computation on STANDBY cannot race with ONLINE. No file, NVM, env, wall-clock, timer or watchdog access found in these directories.
  • Outbound sends are the real side effect. ab_gp_proc_dispatch.c (build_and_stage_reply()) and post_execute.c (train/IL/CTC outputs, CTC keepalive) call ab_gp_channel_stage_relay(). If STANDBY ran the same pipeline unchanged it would stage the same replies as ONLINE, and the Gateway would see duplicates. Required for any implementation: verify that the staged-send flush (ab_gp_channel_stage.c, the loop over staged_send_count) is gated on ONLINE, or add that gate. Not verified here - this is the first thing to check.
  • Direct (unstaged) sends: ab_gp_channel_send_relay() in send_il_keepalive_if_online() in post_execute.c (IL keepalive), ab_gp_status_tick() in ab_gp_status.c (UTIL status) and ab_gp_channel_raise_ctc_alarm() in ab_gp_channel.c (CTC alarm) bypass staging; each needs its own ONLINE/STANDBY decision.
  • Logging: train/ab_gp_train.c writes [ONLINE]-tagged log lines and JRU events; STANDBY running the same code would emit them too (misleading tag, duplicate JRU/diagnostic records via ab_gp_jru_forward.c). Needs a role-aware tag or suppression.
  • Cross-compare: not analysed. Whether STANDBY's result may enter the checkpoint/cross-compare is a design decision, not something the audit can answer.

Net: question 1 changes from "unknown" to "no hidden process-external state found, but outbound sends and logs are side effects and must be gated". Questions 2 (timing budget) and the second-reviewer pass remain open.