Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
sapi_checkpoint.c File Reference

Implementation of the bounded checkpoint rendezvous (ADR-017). More...

Include dependency graph for sapi_checkpoint.c:

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.

Detailed Description

Implementation of the bounded checkpoint rendezvous (ADR-017).

Definition in file sapi_checkpoint.c.

Macro Definition Documentation

◆ SAPI_CHECKPOINT_SENDER_ID

#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.

◆ SAPI_CHECKPOINT_RETRY_ROUND_MS

#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.

◆ SAPI_CHECKPOINT_MIN_ROUNDS_PER_BUDGET

#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.

◆ SAPI_CHECKPOINT_MAX_ROUNDS

#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.

Function Documentation

◆ build_arrival_message()

sapi_status_t build_arrival_message ( uint32_t checkpoint_id,
sapi_vital_message_t * out_msg )
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.

Parameters
checkpoint_idCheckpoint sequence number to encode.
out_msgReceives the built message. Must not be NULL.
Returns
SAPI_STATUS_OK on success; a sapi_buffer_t/sapi_checksum error status otherwise.

Definition at line 99 of file sapi_checkpoint.c.

◆ reply_confirms_checkpoint()

bool reply_confirms_checkpoint ( const sapi_vital_message_t * reply,
uint32_t checkpoint_id )
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.

Parameters
replyCandidate reply message. Must not be NULL.
checkpoint_idExpected checkpoint sequence number.
Returns
true if reply is a valid, current confirmation; false otherwise.

Definition at line 135 of file sapi_checkpoint.c.

◆ remaining_budget_ms()

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 )
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.

Parameters
start_msMonotonic time the rendezvous budget started.
now_msCurrent monotonic time.
max_delay_msTotal budget allotted to the rendezvous.
Returns
Remaining budget in ms; 0 if the deadline has already passed.

Definition at line 179 of file sapi_checkpoint.c.