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
ab_gp_channel_crosscompare.c
Go to the documentation of this file.
1
8
9#include <stdio.h>
10#include <stdlib.h>
11
12#include "rte/oal/log/rte_log.h"
13#include "rte/oal/memory/rte_mem_util.h"
14#include "rte/utils/safestate/rte_safestate.h"
15
17#include "ab_gp_channel.h"
20
49#define XCOMPARE_PAYLOAD_SIZE ((size_t)RTE_EXAMPLE_MAX_TRAINS * RTE_EXAMPLE_TRAIN_SESSION_WIRE_SIZE)
50
67#define RTE_EXAMPLE_XCOMPARE_FAULT_CYCLE_ENV "RTE_EXAMPLE_XCOMPARE_FAULT_CYCLE"
68
73static uint32_t xcompare_fault_cycle(void)
74{
75 static bool parsed = false;
76 static uint32_t cycle = UINT32_MAX;
77
78 if (!parsed)
79 {
80 const char *env = getenv(RTE_EXAMPLE_XCOMPARE_FAULT_CYCLE_ENV);
81
82 if (env != NULL)
83 {
84 char *end = NULL;
85 long val = strtol(env, &end, 10);
86
87 if ((end != env) && (val >= 0))
88 {
89 cycle = (uint32_t)val;
90 }
91 }
92 parsed = true;
93 }
94 return cycle;
95}
96
100{
102 if (ctx->is_role_a && (ctx->cyc_cycle >= xcompare_fault_cycle()))
103 {
104 out[0] ^= 0xFFU;
105 RTE_EXAMPLE_LOG("[%s] cycle %u: RTE_EXAMPLE_XCOMPARE_FAULT_CYCLE reached - "
106 "corrupting own cross-compare payload (test-only fault injection)\n",
107 ctx->role_tag, ctx->cyc_cycle);
108 }
109}
110
118
127static void fault_cleanup(ab_gp_channel_context_t *ctx, rte_safestate_reason_t reason)
128{
129 ctx->own_faulted = true;
130 /* Tell this channel's own inter-site counterpart about the fault
131 * BEFORE the link closes below - see
132 * ab_gp_channel_negotiate_flush_faulted()'s own doc for why this is
133 * required (not just a nice-to-have) for the counterpart's own
134 * promotion rule to ever fire. */
136 /* Same "before the link closes" requirement applies to CTC: a
137 * dispatcher needs to see this fault too, not just A/B's own inter-
138 * site counterpart - see ab_gp_channel_raise_ctc_alarm()'s own doc. */
139 ab_gp_channel_raise_ctc_alarm(ctx, RBC_ALARM_SEVERITY_ERROR, (uint32_t)reason, "reboot fault detected");
140 /* Tear this process's own links/tasks/timer/watchdog down cleanly
141 * BEFORE re-execing: RTE_SAFESTATE_LEVEL_REBOOT (entered either by
142 * react_to_fault() below or by the cross-comparator itself) hands
143 * off to main.c's handler, which re-execs via execv() - POSIX
144 * execv() does NOT close inherited file descriptors on its own, so
145 * skipping this would leak sockets across the reboot AND, worse,
146 * mean the peer/C side never observes a real disconnect.
147 * ab_gp_channel_shutdown() is safe to call from here (main thread,
148 * not one of the background tasks - no self-join deadlock). */
149 (void)ab_gp_channel_shutdown(ctx);
150}
151
158static void react_to_fault(ab_gp_channel_context_t *ctx, rte_safestate_reason_t reason)
159{
160 fault_cleanup(ctx, reason);
161 RTE_SAFESTATE(RTE_SAFESTATE_LEVEL_REBOOT, reason);
162}
163
182{
185 uint8_t i;
186
189 for (i = 0U; i < (uint8_t)RTE_EXAMPLE_MAX_TRAINS; i++)
190 {
191 if (rte_mem_compare(local_bytes[i], peer_bytes[i], RTE_EXAMPLE_TRAIN_SESSION_WIRE_SIZE) != 0)
192 {
193 return (int)i;
194 }
195 }
196 return -1;
197}
198
199static void xcompare_on_disagreement(void *context, rte_voting_result_t result)
200{
202 uint32_t cycle = ctx->cyc_cycle;
203
204 if (result != RTE_VOTING_DISAGREED)
205 {
206 /* Defensive only: both xcompare_recv_local()/xcompare_recv_peer()
207 * above always succeed immediately (no real I/O, no timeout), so
208 * TIMEOUT/INSUFFICIENT_QUORUM can never actually occur through
209 * this pair of channels - but on_disagreement() itself is
210 * invoked for every non-AGREED result, so this guard keeps that
211 * contract explicit rather than assuming it. */
212 RTE_EXAMPLE_LOG("[%s] cycle %u: unexpected cross-compare result %d (not DISAGREED) - ignoring\n", ctx->role_tag,
213 cycle, (int)result);
214 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), cycle, ctx->role_tag,
215 ctx->is_role_a ? "B" : "A", "XCOMPARE", "unexpected non-DISAGREED result from comparator",
216 NULL);
217 return;
218 }
219
220 {
221 int bad_index = first_disagreeing_train_index(ctx);
222
223 RTE_EXAMPLE_LOG("[%s] cycle %u: DISAGREE (first differing train index=%d) - this site's role -> FAULTED, entering REBOOT\n",
224 ctx->role_tag, cycle, bad_index);
225 {
226 char extra[32];
227 (void)snprintf(extra, sizeof(extra), "train_index=%d", bad_index);
228 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), cycle, ctx->role_tag,
229 ctx->is_role_a ? "B" : "A", "DISAGREE", "FAULTED, entering REBOOT", extra);
230 }
231 }
232 /* Cleanup only - the cross-comparator itself enters REBOOT right
233 * after this callback returns, see this function's own doc above. */
235}
236
238{
239 rte_cross_comparator_config_t xcompare_cfg;
240
241 rte_mem_set(&xcompare_cfg, 0, sizeof(xcompare_cfg));
242 xcompare_cfg.log_disagreements = false; /* xcompare_on_disagreement() logs its own, richer detail */
243 /* REBOOT, not SAFE - see this file's own header. The comparator
244 * itself now unconditionally enters this level on DISAGREE, right
245 * after calling xcompare_on_disagreement() below for this
246 * application's own pre-transition cleanup (RCA/OCORA PI-API
247 * compatibility - see rte_cross_comparator.h's own note). */
248 xcompare_cfg.safestate_level = RTE_SAFESTATE_LEVEL_REBOOT;
249 xcompare_cfg.safestate_reason = RTE_EXAMPLE_REASON_AB_MTYPE_DISAGREE;
250 xcompare_cfg.on_disagreement = xcompare_on_disagreement;
251 xcompare_cfg.disagreement_context = ctx;
252
253 /* RCA/OCORA Phase 2b: no rte_channel_t/rte_cross_comparator_register_channel()
254 * calls needed any more - ab_gp_channel_crosscompare_execute() below
255 * calls rte_cross_comparator_execute_buffers() directly with the
256 * local/peer bytes already in hand. */
257 return rte_cross_comparator_init(&ctx->xcompare_cmp, &xcompare_cfg);
258}
259
291static bool per_train_readiness_gate(ab_gp_channel_context_t *ctx, uint32_t cycle)
292{
293 uint8_t i;
294
295 for (i = 0U; i < (uint8_t)RTE_EXAMPLE_MAX_TRAINS; i++)
296 {
297 const ab_gp_train_session_t *local_session = &ctx->sessions[i];
298 const ab_gp_train_session_t *peer_session = &ctx->cyc_peer_sessions[i];
299 bool fields_match;
300
301 if ((!local_session->in_use) && (!peer_session->in_use))
302 {
303 ctx->xcompare_skip_count[i] = 0U;
304 continue;
305 }
306 /* The sibling's sample is one cycle old, so a train connected or disconnected in this very cycle shows up as "held
307 * by the sibling only" for a moment. Adopt only when the same picture has lasted two cycles in a row, otherwise a
308 * session the sibling is just dropping is resurrected here and the two tables diverge (ISS-016). */
309 if ((!local_session->in_use) && peer_session->in_use)
310 {
311 if (ctx->adopt_pending[i] < UINT8_MAX)
312 {
313 ctx->adopt_pending[i]++;
314 }
315 }
316 else
317 {
318 ctx->adopt_pending[i] = 0U;
319 }
320 if ((ctx->adopt_pending[i] >= 2U) &&
321 ab_gp_session_adopt_allowed(ctx->join_window_cycles_left, local_session->in_use, peer_session->in_use))
322 {
323 /* This channel started receiving later than its A/B sibling and missed this train's connect: take the
324 * sibling's session once (see ab_gp_session_adopt_allowed()); later cycles are computed independently
325 * and cross-compared as usual. */
326 rte_mem_copy(&ctx->sessions[i], peer_session, sizeof(ctx->sessions[i]));
327 ctx->xcompare_skip_count[i] = 0U;
328 RTE_EXAMPLE_LOG("[%s] cycle %u: train index %u missing here but held by the A/B sibling - adopted its session "
329 "(joined late)\n", ctx->role_tag, cycle, (unsigned int)i);
330 rte_log_write_event(RTE_LOG_LEVEL_WARNING, site_config_name(ctx->site), cycle, ctx->role_tag,
331 ctx->is_role_a ? "B" : "A", "XCOMPARE", "adopted a missed train session from the sibling", NULL);
332 continue;
333 }
334 /* Direct field comparison (not the raw struct via
335 * rte_mem_compare() - ab_gp_train_session_t mixes bool/uint8_t/
336 * uint32_t members, so the compiler is free to insert padding;
337 * see this file's own doc above for the same reasoning applied
338 * to the real comparator below). Compares every wire-relevant
339 * field (matches ab_gp_train.c's own registered payload-collector
340 * field list exactly) - ma_pending_send/ctc_*_sent are
341 * deliberately excluded, same as the wire encoding itself. */
342 fields_match = (local_session->in_use == peer_session->in_use)
343 && (local_session->train_id == peer_session->train_id)
344 && (local_session->cycle == peer_session->cycle)
345 && (local_session->d_lrbg == peer_session->d_lrbg)
346 && (local_session->granted_length == peer_session->granted_length)
347 && (local_session->ma_seq == peer_session->ma_seq)
348 && (local_session->ma_acked == peer_session->ma_acked);
349
350 RTE_EXAMPLE_LOG("[%s] cycle %u: xcompare train index %u: local(id=%u,pos=%u,ma_len=%u,seq=%u,ack=%d) "
351 "vs peer(id=%u,pos=%u,ma_len=%u,seq=%u,ack=%d) -> %s\n",
352 ctx->role_tag, cycle, (unsigned int)i,
353 (unsigned int)local_session->train_id, (unsigned int)local_session->d_lrbg,
354 (unsigned int)local_session->granted_length, (unsigned int)local_session->ma_seq, (int)local_session->ma_acked,
355 (unsigned int)peer_session->train_id, (unsigned int)peer_session->d_lrbg,
356 (unsigned int)peer_session->granted_length, (unsigned int)peer_session->ma_seq, (int)peer_session->ma_acked,
357 fields_match ? "MATCH" : "DIFF");
358
359 if (fields_match)
360 {
361 ctx->xcompare_skip_count[i] = 0U;
362 continue;
363 }
365 {
366 ctx->xcompare_skip_count[i]++;
367 RTE_EXAMPLE_LOG("[%s] cycle %u: train index %u not yet synchronized with peer (skip %u/%u) - "
368 "skipping cross-compare this cycle\n",
369 ctx->role_tag, cycle, (unsigned int)i, (unsigned int)ctx->xcompare_skip_count[i],
371 rte_log_write_event(RTE_LOG_LEVEL_DEBUG, site_config_name(ctx->site), cycle, ctx->role_tag,
372 ctx->is_role_a ? "B" : "A", "XCOMPARE",
373 "train not yet synchronized with peer - skipping cross-compare", NULL);
374 return false;
375 }
376 RTE_EXAMPLE_LOG("[%s] cycle %u: train index %u still not synchronized with peer after %u skips - "
377 "treating as a genuine divergence, not a timing race\n",
378 ctx->role_tag, cycle, (unsigned int)i,
380 /* fall through - the real comparator below will correctly DISAGREE on this */
381 }
382 return true;
383}
384
396static void execute_cross_comparator(ab_gp_channel_context_t *ctx, uint32_t cycle)
397{
398 uint8_t local_bytes[XCOMPARE_PAYLOAD_SIZE];
399 uint8_t peer_bytes[XCOMPARE_PAYLOAD_SIZE];
400 uint8_t agreed_bytes[XCOMPARE_PAYLOAD_SIZE] = { 0 };
401 size_t agreed_size = 0U;
402 rte_voting_result_t result = RTE_VOTING_AGREED;
403 rte_status_t status;
404
405 encode_local_payload(ctx, local_bytes);
406 encode_peer_payload(ctx, peer_bytes);
407 status = rte_cross_comparator_execute_buffers(&ctx->xcompare_cmp, local_bytes, peer_bytes,
408 XCOMPARE_PAYLOAD_SIZE, &result, agreed_bytes, &agreed_size);
409
410 if (status == RTE_STATUS_OK)
411 {
412 ctx->cyc_agree = true;
413 RTE_EXAMPLE_LOG("[%s] cycle %u: cross-compare consensus result: AGREE (%u train session(s))\n", ctx->role_tag, cycle,
414 (unsigned int)RTE_EXAMPLE_MAX_TRAINS);
415 rte_log_write_event(RTE_LOG_LEVEL_INFO, site_config_name(ctx->site), cycle, ctx->role_tag,
416 ctx->is_role_a ? "B" : "A", "AGREE", "sessions match", NULL);
417 }
418}
419
421{
422 uint32_t cycle = ctx->cyc_cycle;
423 bool have_peer_data = ctx->cyc_have_peer_data;
424 uint32_t peer_cycle = ctx->cyc_peer_cycle;
425 bool crc_ok = ctx->cyc_crc_ok;
426
427 ctx->cyc_agree = false;
428
429 /* ADR-029: gated on ctx->is_online (a STANDBY pair has nothing
430 * meaningful in its own empty session table to compare), not
431 * ctx->cyc_have_channel_data - AB_SAMPLE is now sent every cycle
432 * while ONLINE regardless of whether fresh train/IL data arrived
433 * THIS specific cycle (see ab_gp_channel_send_peer_sample_if_online()'s own doc), so
434 * cross-compare runs every ONLINE cycle too, continuously
435 * reconfirming the current session table rather than only reacting
436 * to fresh arrivals - same "always every cycle" philosophy the
437 * PEER_MSG_KIND_SITE_STATE cross-check already used. */
438 if ((!ctx->is_online) || (!have_peer_data))
439 {
440 RTE_EXAMPLE_LOG("[%s] cycle %u: not online and/or no peer data yet - skipping cross-compare this cycle\n",
441 ctx->role_tag, cycle);
442 rte_log_write_event(RTE_LOG_LEVEL_DEBUG, site_config_name(ctx->site), cycle, ctx->role_tag,
443 ctx->is_role_a ? "B" : "A", "XCOMPARE", "not online/no peer data yet this cycle - skipping cross-compare",
444 NULL);
445 return RTE_STATUS_OK;
446 }
447 if (!crc_ok)
448 {
449 /* Malformed (not CRC-invalid - ab_gp_channel_payload_decode_ab_sample()
450 * already returned INVALID_PARAM for a length/field problem, not
451 * a corruption fault, which would have already diverted via
452 * RTE_SAFESTATE in ab_gp_channel.c's
453 * ab_gp_channel_read_and_stash_peer_sample()) - nothing trustworthy
454 * to compare. */
455 return RTE_STATUS_OK;
456 }
457 if (((peer_cycle > cycle) ? (peer_cycle - cycle) : (cycle - peer_cycle)) > RTE_EXAMPLE_AB_CYCLE_SKEW_TOLERANCE)
458 {
459 /* Peer's report cycle number is too far from this cycle's own to
460 * be considered comparable - independently free-running 500ms
461 * timers can legitimately be a few cycles apart; beyond the
462 * tolerance, though, this is stale enough not to compare, not a
463 * fault. See common_config.h's RTE_EXAMPLE_AB_CYCLE_SKEW_TOLERANCE
464 * comment. */
465 RTE_EXAMPLE_LOG("[%s] cycle %u: peer answered for a too-different cycle (%u) - skipping cross-compare this cycle\n",
466 ctx->role_tag, cycle, peer_cycle);
467 {
468 char extra[24];
469 (void)snprintf(extra, sizeof(extra), "PeerCycle=%u", peer_cycle);
470 rte_log_write_event(RTE_LOG_LEVEL_DEBUG, site_config_name(ctx->site), cycle, ctx->role_tag,
471 ctx->is_role_a ? "B" : "A", "XCOMPARE",
472 "peer answered for a too-different cycle - skipping cross-compare", extra);
473 }
474 /* A persistent skew is not a timing race: the two channels no longer run in step (found live after a
475 * long run: A and B differed by ~700 cycles, so the 2oo2 comparison was silently off for good). Without a
476 * comparison the pair is not 2oo2, so treat it as a fault; the restart of both channels realigns them. */
477 ctx->xcompare_skew_skips++;
478 if (ctx->xcompare_skew_skips >= 60U)
479 {
480 RTE_EXAMPLE_LOG("[%s] cycle %u: peer cycle number off by more than the tolerance for %u cycles - "
481 "cross-compare impossible, entering REBOOT\n",
482 ctx->role_tag, cycle, (unsigned int)ctx->xcompare_skew_skips);
484 }
485 return RTE_STATUS_OK;
486 }
487 ctx->xcompare_skew_skips = 0U;
488
489 /* ADR-029: per-train readiness gate - see per_train_readiness_gate()'s own doc. */
490 if (!per_train_readiness_gate(ctx, cycle))
491 {
492 return RTE_STATUS_OK;
493 }
494
495 /* Comparable this cycle - see execute_cross_comparator()'s own doc. */
496 execute_cross_comparator(ctx, cycle);
497
498 return RTE_STATUS_OK;
499}
500
502{
503 /* The agreement rule, its count and the NEGOTIATION_MISMATCH log live in rte_site_role (REQ-SITEROLE-014/016,
504 * ADR-040 step 5b): the service took the sibling's frame through the sibling source in this cycle's
505 * rte_site_role_execute() and ran the check; GP keeps the fault reaction. */
506 if (rte_site_role_sibling_verdict(&ctx->site_role) == RTE_SITE_ROLE_SIBLING_FAULT)
507 {
509 }
510}
rte_status_t ab_gp_channel_shutdown(void *context)
rte_appmanager_operations_t::shutdown hook.
void ab_gp_channel_raise_ctc_alarm(ab_gp_channel_handle_t handle, rbc_alarm_severity_t severity, uint32_t code, const char *log_message)
Raises a severity-leveled operational alarm: logs it locally (rte_log_write_event(),...
static void execute_cross_comparator(ab_gp_channel_context_t *ctx, uint32_t cycle)
Route the actual AGREED/DISAGREED decision through the real rte_cross_comparator_t (ADR-025).
rte_status_t ab_gp_channel_crosscompare_init(ab_gp_channel_context_t *ctx)
Creates this channel's rte_cross_comparator_t. Called once from ab_gp_channel_crosscompare_setup() (A...
#define XCOMPARE_PAYLOAD_SIZE
static void xcompare_on_disagreement(void *context, rte_voting_result_t result)
static void react_to_fault(ab_gp_channel_context_t *ctx, rte_safestate_reason_t reason)
static void fault_cleanup(ab_gp_channel_context_t *ctx, rte_safestate_reason_t reason)
static int first_disagreeing_train_index(const ab_gp_channel_context_t *ctx)
static void encode_local_payload(const ab_gp_channel_context_t *ctx, uint8_t out[XCOMPARE_PAYLOAD_SIZE])
static bool per_train_readiness_gate(ab_gp_channel_context_t *ctx, uint32_t cycle)
ADR-029 per-train readiness gate: skip cross-compare this cycle for any train whose local/peer sessio...
rte_status_t ab_gp_channel_crosscompare_execute(ab_gp_channel_context_t *ctx)
Runs one cycle's cross-compare decision - see this file's own header. This is ab_gp_channel....
static uint32_t xcompare_fault_cycle(void)
#define RTE_EXAMPLE_XCOMPARE_FAULT_CYCLE_ENV
static void encode_peer_payload(const ab_gp_channel_context_t *ctx, uint8_t out[XCOMPARE_PAYLOAD_SIZE])
void ab_gp_channel_crosscompare_check_site_state(ab_gp_channel_context_t *ctx)
Reacts to the A/B role agreement verdict: this channel's own independently-negotiated {online,...
The actual A-vs-B cross-compare decision: does this cycle's own M24/M15 answer match what the peer in...
void ab_gp_channel_negotiate_flush_faulted(ab_gp_channel_context_t *ctx)
Best-effort, synchronous final send of this channel's own {faulted=true, single_mode}...
This channel's own West<->East inter-site negotiation link - see ab_gp_channel.c's file header for ho...
void ab_gp_channel_payload_encode_collector(ab_gp_channel_payload_collector_id_t id, uint8_t *out, const void *items)
Encodes one collector's own items into its own slice of a combined buffer - used both standalone (e....
Generic, data-blind wire framing for the A<->B peer link's AB_SAMPLE frame (CRC-64-protected,...
bool ab_gp_session_adopt_allowed(uint32_t join_left, bool local_in_use, bool peer_in_use)
Whether a channel that has just become ONLINE adopts one train session from its A/B sibling.
GP-specific standby and site-link constants, the session-adoption rule and the stall rule.
uint32_t ab_gp_train_payload_collector_id(void)
#define RTE_EXAMPLE_TRAIN_SESSION_WIRE_SIZE
#define RTE_EXAMPLE_REASON_AB_PEER_LINK_DOWN
#define RTE_EXAMPLE_LOG(...)
#define RTE_EXAMPLE_REASON_AB_SITE_STATE_MISMATCH
#define RTE_EXAMPLE_AB_CYCLE_SKEW_TOLERANCE
#define RTE_EXAMPLE_MAX_TRAINS
#define RTE_EXAMPLE_XCOMPARE_SYNC_SKIP_LIMIT
#define RTE_EXAMPLE_REASON_AB_MTYPE_DISAGREE
@ RBC_ALARM_SEVERITY_ERROR
const char * site_config_name(rte_example_site_t site)
Definition site_config.c:52
Central application context owned by composition root (main.c).
ab_gp_train_session_t sessions[RTE_EXAMPLE_MAX_TRAINS]
uint8_t adopt_pending[RTE_EXAMPLE_MAX_TRAINS]
rte_cross_comparator_t xcompare_cmp
ab_gp_train_session_t cyc_peer_sessions[RTE_EXAMPLE_MAX_TRAINS]
uint8_t xcompare_skip_count[RTE_EXAMPLE_MAX_TRAINS]