|
Safe API Framework 0.1.0 build 384e6ae
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Partially implemented (2026-09-24): step 2's both halves are done (rte_election for N-way election, rte_voter_vote_buffers() for N-way cross-compare - both below). Steps 1, 3 and 4 remain open - this ADR's own scoping is otherwise unchanged.
rte_redundancy_config_t (RTE_REDUNDANCY_TOPOLOGY_2OO2/_2OO2_REDUNDANT/_2OO3/_NMR, replica_count, quorum_size) already models arbitrary N-replica topologies at the config level (RCA/OCORA Phase 8), and rte_redundancy_config_load() already parses and validates a 2oo3/NMR file correctly. But nothing downstream of that config can actually run one: RBC_GP's own ab_gp_supports_redundancy() (ab_gp_channel.c) explicitly rejects anything except replica_count == 2 with 2OO2/2OO2_REDUNDANT:
TODO.dox carried this as "GP's A/B role still registers a fixed pair of links instead of one per the loaded redundancy topology," phrased as if it were a mechanical fix (loop the existing open-a-channel code over replica_count instead of hardcoding 2). It is not. This ADR records why, so a future pass starts from the real shape of the problem instead of re-discovering it.
rte_voter_t (ADR-025, split out of the old rte_vital_channel_t specifically to separate "N-way voting on copies of the same computation" from "cross-comparison between two independent computations") already does real N-way majority voting — including the "largest mutually-agreeing group, not just compare-to-channel-0" fix ADR-025's own history describes. rte_channel_checkpoint() already uses it. But GP's A/B role never uses rte_voter at all; rte_voter is not a drop-in replacement for what rte_dual_negotiator does (electing which ONE of N replicas is active) — it answers "do these N copies of a value agree," not "which one of us should be ONLINE."
So "N-channel topologies" is really two separate, currently-unsolved problems:
Both changes reach into vital, SIL-4-target code that a large amount of already-verified behavior depends on (rte_dual_negotiator alone backs GP's entire promotion/demotion/reboot-lifecycle policy — see RBC_GP/CLAUDE.md's "Site negotiation" section, decided 2026-09-23, for how much already sits on top of it).
RBC_Test_Env is built around exactly two sites (WEST/EAST) each running an A/B pair — six processes total (A/B x2 sites + role C's train/il/ctc/status). There is no third-replica process, no scenario config, and no smoke.sh check for anything beyond 2oo2. Even a correct N-way implementation in Platform_RTE and GP would have nothing to verify against without first extending the test environment — a third piece of scope this ADR's title doesn't mention but a real implementation would need before it could be called done, not just built.
Recommend an additive approach over a modifying one, for the same reason this session's own cognitive-complexity sweep repeatedly declined to touch high-blast-radius vital functions (rte_appmanager_run, negotiator_update_states) alongside smaller changes: rte_dual_negotiator and rte_cross_comparator are both mature, tested, and deeply depended upon for the 2oo2 case that is the only case actually running today. Rewriting either in place to grow an N-way mode risks the one topology that works.
None of steps 2-4 are scoped further here (state-machine details, wire layout, test topology) — that is real design work for its own follow-up ADR once someone is ready to commit to building this, not something to derive from this scoping pass alone.
Step 2's N-way election half is done: rte_election (include/rte/redundancy/election/, src/redundancy/election/), generalizing rte_dual_negotiator's pairwise older-startup- timestamp-wins tie-break (with the same exact-tie id fallback, and the same "winner's own degradation determines every standby's HOT/COLD label" rule) to RTE_REDUNDANCY_CONFIG_MAX_REPLICAS candidates. Deliberately transport-agnostic rather than owning an N-way rte_dual_channel_t equivalent (this ADR's own "shape TBD" note on the transport question, above) - it takes a caller-gathered candidate snapshot each rte_election_execute() call instead, the same posture rte_voter_t already established for N-way operations elsewhere in this codebase. See that module's own header for the full contract (REQ-ELECTION-001..005) and docs/MISRA_COMPLIANCE_REPORT.dox's own entry for verification detail (ctest 10/10 for this module).
Step 2's N-way cross-compare half is also done: rte_voter_vote_buffers() (include/rte/redundancy/voter/rte_voter.h) - the ADR's own first proposed option, decided: rte_voter used directly, with a buffer-based entry point matching rte_cross_comparator_execute_buffers()'s existing shape (no registered-channel I/O, same "a voter with zero registered channels is legitimate" posture), rather than a separate new primitive. Reuses rte_voter_receive()'s own majority-grouping logic exactly (two of its internal helpers were parameterized to take an explicit count instead of reading voter->channel_count, so both the registered-channel path and this new buffers path share one grouping implementation) - see docs/MISRA_COMPLIANCE_REPORT.dox's own entry for the full detail on what changed and why, and for verification (ctest 36/36, 4 new cases, in both Debug and Release).
Still not done, unchanged from this ADR's original scoping**: step 1 (GP actually selecting rte_election/rte_voter_vote_buffers() for a 2oo3/NMR topology - neither has a caller yet, and 2oo2 is completely unaffected by either's existence), and step 4 (RBC_Test_Env's third-replica scenario). rte_election also has no real N-way beacon transport wired up yet - a caller must still decide and build how it gathers each cycle's candidate snapshot (N pairwise rte_dual_channel_t instances is the most obvious option, but not the only one, and not decided here) - and the same transport gap applies to feeding rte_voter_vote_buffers() its own per-cycle buffer snapshot.