|
Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Implementation of the bounded checkpoint rendezvous (ADR-017). More...
#include "safeapi/redundancy/checkpoint/sapi_checkpoint.h"#include <stdbool.h>#include <stdio.h>#include "safeapi/utils/buffer/sapi_buffer.h"#include "safeapi/redundancy/checksum/sapi_checksum.h"#include "safeapi/utils/safestate/sapi_safestate.h"#include "safeapi/oal/timer/sapi_timer.h"Go to the source code of this file.
Macros | |
| #define | SAPI_CHECKPOINT_SENDER_ID ((uint32_t)0xC4EC0001U) |
| #define | SAPI_CHECKPOINT_RETRY_ROUND_MS ((sapi_duration_ms_t)250U) |
| #define | SAPI_CHECKPOINT_MIN_ROUNDS_PER_BUDGET ((sapi_duration_ms_t)3U) |
| #define | SAPI_CHECKPOINT_MAX_ROUNDS ((uint32_t)64U) |
Functions | |
| static sapi_status_t | build_arrival_message (uint32_t checkpoint_id, sapi_vital_message_t *out_msg) |
| Builds the checkpoint-arrival marker message. | |
| static bool | reply_confirms_checkpoint (const sapi_vital_message_t *reply, uint32_t checkpoint_id) |
| Verifies a candidate reply's CRC/sequence (via sapi_checksum) and that its decoded payload matches checkpoint_id. | |
| static sapi_duration_ms_t | remaining_budget_ms (sapi_timestamp_ms_t start_ms, sapi_timestamp_ms_t now_ms, sapi_duration_ms_t max_delay_ms) |
| Returns the time budget remaining until start_ms + max_delay_ms. | |
| sapi_status_t | sapi_channel_checkpoint (sapi_voter_t *voter, const sapi_checkpoint_config_t *config) |
| Performs one bounded checkpoint rendezvous across every channel registered with a voter. | |
Implementation of the bounded checkpoint rendezvous (ADR-017).
Definition in file sapi_checkpoint.c.
| #define SAPI_CHECKPOINT_SENDER_ID ((uint32_t)0xC4EC0001U) |
Fixed sender id for checkpoint-arrival markers. sapi_checksum's verify path does not constrain this value; it only matters for diagnostics, so a single framework-reserved constant is sufficient rather than threading a real per-channel identity through this module.
Definition at line 20 of file sapi_checkpoint.c.
| #define SAPI_CHECKPOINT_RETRY_ROUND_MS ((sapi_duration_ms_t)250U) |
Per-attempt receive sub-budget within one sapi_channel_checkpoint() call (see the retry loop below) - deliberately short relative to a typical config->max_delay_ms so a single call gets several independent send+receive rounds instead of committing its whole budget to one. Real bug this fixes, found via live Docker testing: a LISTEN-role backend channel can only reply once it has learned its peer's address from an inbound packet (sapi_posix_backend_channel_service.c's own has_peer_addr) - on a genuinely cold two-way start neither side has heard from the other yet, so a SINGLE round's send is a structural no-op on the listening side regardless of how long the matching receive is allowed to wait. One long blocking round-trip attempt can therefore never succeed on a cold start; it always spends the entire budget and then fails, triggering SAPI_SAFESTATE_REASON_CHECKPOINT_TIMEOUT on both peers - which each reboot resets, replaying the same structurally-doomed single attempt forever (a real, reproduced livelock, not a transport/network problem - confirmed via a raw UDP probe succeeding instantly against the same listening socket). Several short rounds instead give that one-time address-learning step room to complete on an early round, so a later round within the SAME call can actually exchange confirmations - still bounded by max_delay_ms in total (REQ-CHECKPOINT-001 unchanged).
Definition at line 44 of file sapi_checkpoint.c.
| #define SAPI_CHECKPOINT_MIN_ROUNDS_PER_BUDGET ((sapi_duration_ms_t)3U) |
Minimum number of retry rounds a call's own config->max_delay_ms budget is deliberately divided into, regardless of how small that budget is. SAPI_CHECKPOINT_RETRY_ROUND_MS alone only bounds a round's LENGTH from above; for a short budget (a steady-state call, not the relaxed post-reconnect window this retry loop was originally added for) that still leaves as few as one or two rounds - not enough headroom for the project's own documented real-world finding that a single round can miss under genuine container-host scheduling jitter even once a peer is already known and reachable (see SAFEAPI_EXAMPLE_AB_CHECKPOINT_RELAXED_MAX_DELAY_MS's doc in safeAPIRBC2oo2GP/src/application/AB/common/common_config.h). Confirmed live: even with the round-based retry loop below, a steady-state budget of 400ms (~1-2 rounds at the flat 250ms cap) still let a single missed round trip escalate to REQ-CHECKPOINT-003's SAFE/REBOOT often enough to keep A/WEST and B/WEST cycling reboots roughly every 40s instead of settling. Dividing max_delay_ms by this constant (applied only when that division yields something SMALLER than SAPI_CHECKPOINT_RETRY_ROUND_MS - a long relaxed budget is unaffected) trades round length for round COUNT on a short budget, without raising the budget itself.
Definition at line 67 of file sapi_checkpoint.c.
| #define SAPI_CHECKPOINT_MAX_ROUNDS ((uint32_t)64U) |
Hard cap on retry rounds within one sapi_channel_checkpoint() call - a backstop independent of config->max_delay_ms (REQ-CHECKPOINT-001 stays satisfied by the time-based bound alone; this exists only to guarantee termination even if a receive callback returns near- instantly instead of genuinely blocking for its requested timeout, as this project's own mock-backed unit tests do). The largest legitimate round count in real use is the relaxed startup budget divided by the per-round sub-budget (10000ms / 250ms = 40); this leaves headroom above that without being unbounded.
Definition at line 79 of file sapi_checkpoint.c.
|
static |
Builds the checkpoint-arrival marker message.
checkpoint_id is carried both as the sapi_vital_message_t sequence number (checked by sapi_checksum_vital_message_verify() for continuity) and, explicit-endian-encoded, as the 4-byte payload - belt and suspenders against a reply from a stale or wrong checkpoint being mistaken for a current one (REQ-CHECKPOINT-002). The payload is written little-endian rather than a raw struct copy specifically because the two channels exchanging it may be different machines, potentially different CPU architectures - a bare memcpy of a uint32_t would silently corrupt the value across a byte-order mismatch.
| checkpoint_id | Checkpoint sequence number to encode. |
| out_msg | Receives the built message. Must not be NULL. |
Definition at line 99 of file sapi_checkpoint.c.
|
static |
Verifies a candidate reply's CRC/sequence (via sapi_checksum) and that its decoded payload matches checkpoint_id.
Returns false - not an error - for anything that fails either check: a corrupted or stale/wrong-checkpoint reply must never count toward quorum (REQ-CHECKPOINT-002), but it is not itself grounds to fail the whole rendezvous - other channels may still confirm in time.
| reply | Candidate reply message. Must not be NULL. |
| checkpoint_id | Expected checkpoint sequence number. |
Definition at line 135 of file sapi_checkpoint.c.
|
static |
Returns the time budget remaining until start_ms + max_delay_ms.
Keeps the whole rendezvous bounded by max_delay_ms in total (REQ-CHECKPOINT-001) rather than re-granting a fresh max_delay_ms budget to every channel polled in sequence.
| start_ms | Monotonic time the rendezvous budget started. |
| now_ms | Current monotonic time. |
| max_delay_ms | Total budget allotted to the rendezvous. |
Definition at line 179 of file sapi_checkpoint.c.