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
Go to the documentation of this file.
1
7
8#include <stdbool.h>
9#include <stdio.h>
10
15
20#define SAPI_CHECKPOINT_SENDER_ID ((uint32_t)0xC4EC0001U)
21
44#define SAPI_CHECKPOINT_RETRY_ROUND_MS ((sapi_duration_ms_t)250U)
45
67#define SAPI_CHECKPOINT_MIN_ROUNDS_PER_BUDGET ((sapi_duration_ms_t)3U)
68
79#define SAPI_CHECKPOINT_MAX_ROUNDS ((uint32_t)64U)
80
99static sapi_status_t build_arrival_message(uint32_t checkpoint_id, sapi_vital_message_t *out_msg)
100{
101 uint8_t payload_bytes[4];
102 sapi_buffer_t payload_buf;
103 sapi_status_t status = sapi_buffer_init(&payload_buf, payload_bytes, sizeof(payload_bytes));
104
105 if (status == SAPI_STATUS_OK)
106 {
107 status = sapi_buffer_write_u32_le(&payload_buf, checkpoint_id);
108 }
109
110 if (status == SAPI_STATUS_OK)
111 {
112 status = sapi_checksum_vital_message_create(out_msg,
114 checkpoint_id,
115 payload_bytes,
116 sizeof(payload_bytes));
117 }
118
119 return status;
120}
121
135static bool reply_confirms_checkpoint(const sapi_vital_message_t *reply, uint32_t checkpoint_id)
136{
137 uint8_t payload_out[4];
138 uint8_t payload_size_out = 0U;
139 bool confirmed = false;
141 reply, checkpoint_id, payload_out, sizeof(payload_out), &payload_size_out);
142
143 if ((verify_status == SAPI_STATUS_OK) && (payload_size_out == sizeof(payload_out)))
144 {
145 sapi_buffer_t payload_buf;
146
147 if (sapi_buffer_init(&payload_buf, payload_out, sizeof(payload_out)) == SAPI_STATUS_OK)
148 {
149 uint32_t decoded_id = 0U;
150
151 /* Reads happen against buf->length, not just capacity; the
152 * buffer was just fully written by verify() above, so its
153 * length must be advanced to cover it before reading back. */
154 (void)sapi_buffer_set_length(&payload_buf, sizeof(payload_out));
155
156 if ((sapi_buffer_read_u32_le(&payload_buf, 0U, &decoded_id) == SAPI_STATUS_OK)
157 && (decoded_id == checkpoint_id))
158 {
159 confirmed = true;
160 }
161 }
162 }
163
164 return confirmed;
165}
166
180 sapi_timestamp_ms_t now_ms,
181 sapi_duration_ms_t max_delay_ms)
182{
183 sapi_duration_ms_t remaining;
184 sapi_timestamp_ms_t elapsed_ms = now_ms - start_ms;
185
186 if (elapsed_ms >= (sapi_timestamp_ms_t)max_delay_ms)
187 {
188 remaining = 0U;
189 }
190 else
191 {
192 remaining = max_delay_ms - (sapi_duration_ms_t)elapsed_ms;
193 }
194
195 return remaining;
196}
197
199{
201 sapi_vital_message_t arrival_msg;
202 uint32_t channel_count;
203
204 channel_count = sapi_voter_get_channel_count(voter);
205
206 if ((voter == NULL) || (config == NULL))
207 {
209 }
210 else if ((config->expected_node_count == 0U) || (config->expected_node_count > channel_count))
211 {
213 }
214 else
215 {
216 status = build_arrival_message(config->checkpoint_id, &arrival_msg);
217 }
218
219 if (status == SAPI_STATUS_OK)
220 {
221 sapi_timestamp_ms_t start_ms = 0U;
222 sapi_timestamp_ms_t now_ms = 0U;
223 uint32_t confirmed_count = 0U;
224 uint32_t i;
225 sapi_status_t last_send_status = SAPI_STATUS_OK;
226 sapi_status_t last_recv_status = SAPI_STATUS_OK;
227 bool channel_confirmed[SAPI_VOTER_MAX_CHANNELS];
228 uint32_t round_count = 0U;
230 sapi_duration_ms_t budget_based_round_cap_ms =
232
233 if ((budget_based_round_cap_ms > 0U) && (budget_based_round_cap_ms < round_cap_ms))
234 {
235 round_cap_ms = budget_based_round_cap_ms;
236 }
237
238 for (i = 0U; i < SAPI_VOTER_MAX_CHANNELS; i++)
239 {
240 channel_confirmed[i] = false;
241 }
242
243 (void)sapi_timer_now(&start_ms);
244 now_ms = start_ms;
245
246 /* Retries the whole send+receive round, per not-yet-confirmed
247 * channel, until either every expected channel has confirmed, the
248 * overall max_delay_ms budget is exhausted, or SAPI_CHECKPOINT_MAX_ROUNDS
249 * rounds have run - see SAPI_CHECKPOINT_RETRY_ROUND_MS's own doc for
250 * why a single round cannot succeed on a cold two-way start. Still
251 * bounded by max_delay_ms in total (REQ-CHECKPOINT-001): each
252 * round's own receive is capped to whatever's left of that budget,
253 * never more. The round-count cap is a separate, deliberate
254 * backstop against a transport whose receive call returns near-
255 * instantly instead of genuinely blocking for its requested
256 * timeout (confirmed live: this project's own mock-backed unit
257 * tests do exactly that, which turned this loop into a real
258 * busy-spin consuming the full max_delay_ms in real wall-clock
259 * time at 100% CPU before this cap was added) - real production
260 * backends block for close to the requested duration, so this
261 * cap is not expected to bind there, only to guarantee it can't
262 * regardless of transport behavior. */
263 while ((confirmed_count < config->expected_node_count)
264 && (round_count < SAPI_CHECKPOINT_MAX_ROUNDS)
265 && (remaining_budget_ms(start_ms, now_ms, config->max_delay_ms) > 0U))
266 {
267 for (i = 0U; i < channel_count; i++)
268 {
269 sapi_channel_t *channel;
270 sapi_status_t send_status;
271
272 if ((i < SAPI_VOTER_MAX_CHANNELS) && channel_confirmed[i])
273 {
274 continue;
275 }
276
277 channel = sapi_voter_get_channel(voter, i);
278 send_status = sapi_channel_send(channel, &arrival_msg, sizeof(arrival_msg));
279
280 last_send_status = send_status;
281 if (send_status == SAPI_STATUS_OK)
282 {
284 sapi_duration_ms_t sub_budget_ms;
285 sapi_status_t recv_status;
286
287 (void)sapi_timer_now(&now_ms);
288 sub_budget_ms = remaining_budget_ms(start_ms, now_ms, config->max_delay_ms);
289 if (sub_budget_ms > round_cap_ms)
290 {
291 sub_budget_ms = round_cap_ms;
292 }
293
294 recv_status = sapi_channel_receive(channel, &reply, sizeof(reply), sub_budget_ms);
295
296 last_recv_status = recv_status;
297 if ((recv_status == SAPI_STATUS_OK) && reply_confirms_checkpoint(&reply, config->checkpoint_id))
298 {
300 {
301 channel_confirmed[i] = true;
302 }
303 confirmed_count++;
304 }
305 }
306 }
307 (void)sapi_timer_now(&now_ms);
308 round_count++;
309
310 /* Kick config->watchdog (if any) once per round, not only on
311 * final success below - a caller-supplied liveness watchdog
312 * exists to catch a genuinely stuck cyclic executive, and a
313 * round that just performed real send/receive I/O is genuine
314 * forward progress, not a fake kick. Found live: a caller
315 * whose own liveness watchdog has a short (sub-second) timeout
316 * and is kicked only once per full cycle, AFTER this function
317 * returns, would otherwise have that watchdog fire out from
318 * under a call that is still legitimately retrying within its
319 * own much longer config->max_delay_ms budget (e.g. the
320 * relaxed startup window this retry loop exists for) - not a
321 * hung caller, just a caller who has not been given a chance
322 * to kick its own watchdog yet. */
323 if (config->watchdog != NULL)
324 {
325 (void)sapi_watchdog_kick(config->watchdog);
326 }
327 }
328
329 if (confirmed_count >= config->expected_node_count)
330 {
331 if (config->watchdog != NULL)
332 {
333 (void)sapi_watchdog_kick(config->watchdog);
334 }
335 status = SAPI_STATUS_OK;
336 }
337 else
338 {
339 /* Diagnostic-only, fixed-size buffer (no dynamic allocation,
340 * CLAUDE.md): before this REQ-COMMON-SAFESTATE-002 permanent
341 * halt, capture the LAST channel's own send/receive outcome so
342 * a registered SAFE-level handler (see sapi_safestate.h) can
343 * actually log WHY this rendezvous failed - confirmed_count
344 * alone does not distinguish "peer never sent" (send_status
345 * failure) from "peer sent but didn't reply in time"
346 * (recv_status == SAPI_STATUS_TIMEOUT) from "replied with the
347 * wrong checkpoint_id" (recv_status == SAPI_STATUS_OK but not
348 * confirmed) - this was previously impossible to tell apart
349 * from outside this function, since no handler was ever
350 * registered for this level anywhere in this codebase and the
351 * halt itself is otherwise completely silent. */
352 char diag_message[96];
353
354 (void)snprintf(diag_message, sizeof(diag_message),
355 "checkpoint confirmed=%u expected=%u channels=%u last_send=%d last_recv=%d",
356 (unsigned int)confirmed_count, (unsigned int)config->expected_node_count,
357 (unsigned int)channel_count, (int)last_send_status, (int)last_recv_status);
358 /* Matches the pattern sapi_voter_receive() already uses on a
359 * voting disagreement: safe-state is triggered directly by
360 * the sync/vote logic itself, not left to the caller to
361 * notice and react to (REQ-CHECKPOINT-003). */
363 (int32_t)__LINE__, diag_message);
364 status = SAPI_STATUS_TIMEOUT;
365 }
366 }
367
368 return status;
369}
sapi_status_t sapi_buffer_write_u32_le(sapi_buffer_t *buf, uint32_t value)
Appends a little-endian uint32_t at buf's current length.
sapi_status_t sapi_buffer_read_u32_le(const sapi_buffer_t *buf, size_t offset, uint32_t *out_value)
Reads a little-endian uint32_t at the given offset.
sapi_status_t sapi_buffer_init(sapi_buffer_t *buf, void *storage, size_t capacity)
Binds a buffer view to caller-owned storage. Initial length is 0.
Definition sapi_buffer.c:9
sapi_status_t sapi_buffer_set_length(sapi_buffer_t *buf, size_t length)
Marks length bytes of already-written storage as valid.
Definition sapi_buffer.c:31
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.
sapi_status_t sapi_checksum_vital_message_verify(const sapi_vital_message_t *msg, uint32_t expected_sequence, uint8_t *payload_out, size_t payload_max_size, uint8_t *payload_size_out)
Verify vital channel message and extract payload.
sapi_status_t sapi_checksum_vital_message_create(sapi_vital_message_t *msg_out, uint32_t sender_id, uint32_t sequence, const uint8_t *payload, size_t payload_size)
Create vital channel message with CRC.
#define SAPI_SAFESTATE_REASON_CHECKPOINT_TIMEOUT
void sapi_safestate_enter(sapi_safestate_level_t level, sapi_safestate_reason_t reason, const char *file, int32_t line, const char *message)
Enters a safe-state level: invokes the registered handler (if any), then, for SAPI_SAFESTATE_LEVEL_SA...
@ SAPI_SAFESTATE_LEVEL_SAFE
sapi_status_t
Common result/status codes.
Definition sapi_status.h:27
@ SAPI_STATUS_TIMEOUT
Definition sapi_status.h:32
@ SAPI_STATUS_INVALID_PARAM
Definition sapi_status.h:29
@ SAPI_STATUS_OK
Definition sapi_status.h:28
sapi_status_t sapi_timer_now(sapi_timestamp_ms_t *out_now_ms)
Returns the current monotonic time base used by all timers.
Definition sapi_timer.c:130
uint32_t sapi_duration_ms_t
Definition sapi_types.h:27
uint64_t sapi_timestamp_ms_t
Definition sapi_types.h:30
sapi_status_t sapi_watchdog_kick(sapi_watchdog_t watchdog)
Kick (pet) watchdog - prove liveness.
sapi_voter_storage_t sapi_voter_t
Opaque handle to a voter instance.
Definition sapi_voter.h:128
uint32_t sapi_voter_get_channel_count(const sapi_voter_t *voter)
Number of channels currently registered with this voter.
Definition sapi_voter.c:431
sapi_channel_t * sapi_voter_get_channel(const sapi_voter_t *voter, uint32_t index)
Direct access to one registered channel, by index.
Definition sapi_voter.c:436
#define SAPI_VOTER_MAX_CHANNELS
Maximum number of channels a single voter can register.
Definition sapi_voter.h:111
Cross-layer data buffer abstraction (ADR-002).
#define SAPI_CHECKPOINT_MIN_ROUNDS_PER_BUDGET
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 ch...
static sapi_status_t build_arrival_message(uint32_t checkpoint_id, sapi_vital_message_t *out_msg)
Builds the checkpoint-arrival marker message.
#define SAPI_CHECKPOINT_SENDER_ID
#define SAPI_CHECKPOINT_RETRY_ROUND_MS
#define SAPI_CHECKPOINT_MAX_ROUNDS
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.
Bounded checkpoint rendezvous for distributed vital channels (ADR-017).
Checksum and CRC utilities for data integrity in redundant systems.
Safe-state transitions and checked assertions (ADR-004).
OS Abstraction Layer - Timer service.
Mutable, bounds-tracked view over caller-owned storage.
Definition sapi_buffer.h:36
Checkpoint configuration.
sapi_duration_ms_t max_delay_ms
Vital channel message with integrated CRC-64.