Safe API Framework 0.1.0 build 384e6ae
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
rte_checkpoint.c
Go to the documentation of this file.
1
7
8#include <stdbool.h>
9#include <stdio.h>
10
15
20#define RTE_CHECKPOINT_SENDER_ID ((uint32_t)0xC4EC0001U)
21
44#define RTE_CHECKPOINT_RETRY_ROUND_MS ((rte_duration_ms_t)250U)
45
67#define RTE_CHECKPOINT_MIN_ROUNDS_PER_BUDGET ((rte_duration_ms_t)3U)
68
79#define RTE_CHECKPOINT_MAX_ROUNDS ((uint32_t)64U)
80
99static rte_status_t build_arrival_message(uint32_t checkpoint_id, rte_vital_message_t *out_msg)
100{
101 uint8_t payload_bytes[4];
102 rte_buffer_t payload_buf;
103 rte_status_t status = rte_buffer_init(&payload_buf, payload_bytes, sizeof(payload_bytes));
104
105 if (status == RTE_STATUS_OK)
106 {
107 status = rte_buffer_write_u32_le(&payload_buf, checkpoint_id);
108 }
109
110 if (status == RTE_STATUS_OK)
111 {
112 status = rte_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 rte_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 == RTE_STATUS_OK) && (payload_size_out == sizeof(payload_out)))
144 {
145 rte_buffer_t payload_buf;
146
147 if (rte_buffer_init(&payload_buf, payload_out, sizeof(payload_out)) == RTE_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)rte_buffer_set_length(&payload_buf, sizeof(payload_out));
155
156 if ((rte_buffer_read_u32_le(&payload_buf, 0U, &decoded_id) == RTE_STATUS_OK)
157 && (decoded_id == checkpoint_id))
158 {
159 confirmed = true;
160 }
161 }
162 }
163
164 return confirmed;
165}
166
180 rte_timestamp_ms_t now_ms,
181 rte_duration_ms_t max_delay_ms)
182{
183 rte_duration_ms_t remaining;
184 rte_timestamp_ms_t elapsed_ms = now_ms - start_ms;
185
186 if (elapsed_ms >= (rte_timestamp_ms_t)max_delay_ms)
187 {
188 remaining = 0U;
189 }
190 else
191 {
192 remaining = max_delay_ms - (rte_duration_ms_t)elapsed_ms;
193 }
194
195 return remaining;
196}
197
198/* rte_channel_checkpoint()'s own per-channel send+receive+confirm attempt, extracted: sends the
199 * arrival message to channel index i, and on a successful send, waits (bounded by round_cap_ms
200 * and whatever is left of the overall max_delay_ms budget from start_ms) for a reply confirming
201 * checkpoint_id. Reports the outcome only - updating channel_confirmed[]/confirmed_count stays
202 * the caller's job, same "do the I/O, let the caller interpret it" split
203 * rte_voter_receive()'s own extraction already established (see git history). *now_ms is
204 * updated (rte_timer_now() is called again before the receive, to size its own sub-budget
205 * accurately) - an out-parameter, not a return value, to match the loop variable it feeds back
206 * into on every call. No logic changed from the block this replaces. */
207static bool checkpoint_try_channel(rte_voter_t *voter, uint32_t i, const rte_vital_message_t *arrival_msg,
208 uint32_t checkpoint_id, rte_timestamp_ms_t start_ms, rte_timestamp_ms_t *now_ms,
209 rte_duration_ms_t max_delay_ms, rte_duration_ms_t round_cap_ms,
210 rte_status_t *out_last_send_status, rte_status_t *out_last_recv_status)
211{
212 rte_channel_t *channel;
213 rte_status_t send_status;
214 bool confirmed = false;
215
216 channel = rte_voter_get_channel(voter, i);
217 send_status = rte_channel_send(channel, arrival_msg, sizeof(*arrival_msg));
218 *out_last_send_status = send_status;
219 if (send_status == RTE_STATUS_OK)
220 {
222 rte_duration_ms_t sub_budget_ms;
223 rte_status_t recv_status;
224
225 (void)rte_timer_now(now_ms);
226 sub_budget_ms = remaining_budget_ms(start_ms, *now_ms, max_delay_ms);
227 if (sub_budget_ms > round_cap_ms)
228 {
229 sub_budget_ms = round_cap_ms;
230 }
231
232 recv_status = rte_channel_receive(channel, &reply, sizeof(reply), sub_budget_ms);
233 *out_last_recv_status = recv_status;
234 confirmed = (recv_status == RTE_STATUS_OK) && reply_confirms_checkpoint(&reply, checkpoint_id);
235 }
236 return confirmed;
237}
238
240{
242 rte_vital_message_t arrival_msg;
243 uint32_t channel_count;
244
245 channel_count = rte_voter_get_channel_count(voter);
246
247 if ((voter == NULL) || (config == NULL))
248 {
250 }
251 else if ((config->expected_node_count == 0U) || (config->expected_node_count > channel_count))
252 {
254 }
255 else
256 {
257 status = build_arrival_message(config->checkpoint_id, &arrival_msg);
258 }
259
260 if (status == RTE_STATUS_OK)
261 {
262 rte_timestamp_ms_t start_ms = 0U;
263 rte_timestamp_ms_t now_ms = 0U;
264 uint32_t confirmed_count = 0U;
265 uint32_t i;
266 rte_status_t last_send_status = RTE_STATUS_OK;
267 rte_status_t last_recv_status = RTE_STATUS_OK;
268 bool channel_confirmed[RTE_VOTER_MAX_CHANNELS];
269 uint32_t round_count = 0U;
271 rte_duration_ms_t budget_based_round_cap_ms =
273
274 if ((budget_based_round_cap_ms > 0U) && (budget_based_round_cap_ms < round_cap_ms))
275 {
276 round_cap_ms = budget_based_round_cap_ms;
277 }
278
279 for (i = 0U; i < RTE_VOTER_MAX_CHANNELS; i++)
280 {
281 channel_confirmed[i] = false;
282 }
283
284 (void)rte_timer_now(&start_ms);
285 now_ms = start_ms;
286
287 /* Retries the whole send+receive round, per not-yet-confirmed
288 * channel, until either every expected channel has confirmed, the
289 * overall max_delay_ms budget is exhausted, or RTE_CHECKPOINT_MAX_ROUNDS
290 * rounds have run - see RTE_CHECKPOINT_RETRY_ROUND_MS's own doc for
291 * why a single round cannot succeed on a cold two-way start. Still
292 * bounded by max_delay_ms in total (REQ-CHECKPOINT-001): each
293 * round's own receive is capped to whatever's left of that budget,
294 * never more. The round-count cap is a separate, deliberate
295 * backstop against a transport whose receive call returns near-
296 * instantly instead of genuinely blocking for its requested
297 * timeout (confirmed live: this project's own mock-backed unit
298 * tests do exactly that, which turned this loop into a real
299 * busy-spin consuming the full max_delay_ms in real wall-clock
300 * time at 100% CPU before this cap was added) - real production
301 * OSAdapters block for close to the requested duration, so this
302 * cap is not expected to bind there, only to guarantee it can't
303 * regardless of transport behavior. */
304 while ((confirmed_count < config->expected_node_count)
305 && (round_count < RTE_CHECKPOINT_MAX_ROUNDS)
306 && (remaining_budget_ms(start_ms, now_ms, config->max_delay_ms) > 0U))
307 {
308 for (i = 0U; i < channel_count; i++)
309 {
310 if ((i < RTE_VOTER_MAX_CHANNELS) && channel_confirmed[i])
311 {
312 continue;
313 }
314
315 if (checkpoint_try_channel(voter, i, &arrival_msg, config->checkpoint_id, start_ms, &now_ms,
316 config->max_delay_ms, round_cap_ms, &last_send_status,
317 &last_recv_status))
318 {
320 {
321 channel_confirmed[i] = true;
322 }
323 confirmed_count++;
324 }
325 }
326 (void)rte_timer_now(&now_ms);
327 round_count++;
328
329 /* Kick config->watchdog (if any) once per round, not only on
330 * final success below - a caller-supplied liveness watchdog
331 * exists to catch a genuinely stuck cyclic executive, and a
332 * round that just performed real send/receive I/O is genuine
333 * forward progress, not a fake kick. Found live: a caller
334 * whose own liveness watchdog has a short (sub-second) timeout
335 * and is kicked only once per full cycle, AFTER this function
336 * returns, would otherwise have that watchdog fire out from
337 * under a call that is still legitimately retrying within its
338 * own much longer config->max_delay_ms budget (e.g. the
339 * relaxed startup window this retry loop exists for) - not a
340 * hung caller, just a caller who has not been given a chance
341 * to kick its own watchdog yet. */
342 if (config->watchdog != NULL)
343 {
344 (void)rte_watchdog_kick(config->watchdog);
345 }
346 }
347
348 if (confirmed_count >= config->expected_node_count)
349 {
350 if (config->watchdog != NULL)
351 {
352 (void)rte_watchdog_kick(config->watchdog);
353 }
354 status = RTE_STATUS_OK;
355 }
356 else
357 {
358 /* Diagnostic-only, fixed-size buffer (no dynamic allocation,
359 * CLAUDE.md): before this REQ-COMMON-SAFESTATE-002 permanent
360 * halt, capture the LAST channel's own send/receive outcome so
361 * a registered SAFE-level handler (see rte_safestate.h) can
362 * actually log WHY this rendezvous failed - confirmed_count
363 * alone does not distinguish "peer never sent" (send_status
364 * failure) from "peer sent but didn't reply in time"
365 * (recv_status == RTE_STATUS_TIMEOUT) from "replied with the
366 * wrong checkpoint_id" (recv_status == RTE_STATUS_OK but not
367 * confirmed) - this was previously impossible to tell apart
368 * from outside this function, since no handler was ever
369 * registered for this level anywhere in this codebase and the
370 * halt itself is otherwise completely silent. */
371 char diag_message[96];
372
373 (void)snprintf(diag_message, sizeof(diag_message),
374 "checkpoint confirmed=%u expected=%u channels=%u last_send=%d last_recv=%d",
375 (unsigned int)confirmed_count, (unsigned int)config->expected_node_count,
376 (unsigned int)channel_count, (int)last_send_status, (int)last_recv_status);
377 /* Matches the pattern rte_voter_receive() already uses on a
378 * voting disagreement: safe-state is triggered directly by
379 * the sync/vote logic itself, not left to the caller to
380 * notice and react to (REQ-CHECKPOINT-003). */
382 (int32_t)__LINE__, diag_message);
383 status = RTE_STATUS_TIMEOUT;
384 }
385 }
386
387 return status;
388}
rte_status_t rte_buffer_set_length(rte_buffer_t *buf, size_t length)
Marks length bytes of already-written storage as valid.
Definition rte_buffer.c:31
rte_status_t rte_buffer_write_u32_le(rte_buffer_t *buf, uint32_t value)
Appends a little-endian uint32_t at buf's current length.
Definition rte_buffer.c:186
rte_status_t rte_buffer_init(rte_buffer_t *buf, void *storage, size_t capacity)
Binds a buffer view to caller-owned storage. Initial length is 0.
Definition rte_buffer.c:9
rte_status_t rte_buffer_read_u32_le(const rte_buffer_t *buf, size_t offset, uint32_t *out_value)
Reads a little-endian uint32_t at the given offset.
Definition rte_buffer.c:269
rte_status_t rte_channel_checkpoint(rte_voter_t *voter, const rte_checkpoint_config_t *config)
Performs one bounded checkpoint rendezvous across every channel registered with a voter.
rte_status_t rte_checksum_vital_message_verify(const rte_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.
rte_status_t rte_checksum_vital_message_create(rte_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 RTE_SAFESTATE_REASON_CHECKPOINT_TIMEOUT
void rte_safestate_enter(rte_safestate_level_t level, rte_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 RTE_SAFESTATE_LEVEL_SAF...
@ RTE_SAFESTATE_LEVEL_SAFE
rte_status_t
Common result/status codes.
Definition rte_status.h:27
@ RTE_STATUS_OK
Definition rte_status.h:28
@ RTE_STATUS_INVALID_PARAM
Definition rte_status.h:29
@ RTE_STATUS_TIMEOUT
Definition rte_status.h:32
rte_status_t rte_timer_now(rte_timestamp_ms_t *out_now_ms)
Returns the current monotonic time base used by all timers.
Definition rte_timer.c:139
uint32_t rte_duration_ms_t
Definition rte_types.h:27
uint64_t rte_timestamp_ms_t
Definition rte_types.h:30
rte_status_t rte_watchdog_kick(rte_watchdog_t watchdog)
Kick (pet) watchdog - prove liveness.
rte_voter_storage_t rte_voter_t
Opaque handle to a voter instance.
Definition rte_voter.h:149
#define RTE_VOTER_MAX_CHANNELS
Maximum number of channels a single voter can register.
Definition rte_voter.h:132
rte_channel_t * rte_voter_get_channel(const rte_voter_t *voter, uint32_t index)
Direct access to one registered channel, by index.
Definition rte_voter.c:627
uint32_t rte_voter_get_channel_count(const rte_voter_t *voter)
Number of channels currently registered with this voter.
Definition rte_voter.c:622
Cross-layer data buffer abstraction (ADR-002).
static rte_duration_ms_t remaining_budget_ms(rte_timestamp_ms_t start_ms, rte_timestamp_ms_t now_ms, rte_duration_ms_t max_delay_ms)
Returns the time budget remaining until start_ms + max_delay_ms.
static rte_status_t build_arrival_message(uint32_t checkpoint_id, rte_vital_message_t *out_msg)
Builds the checkpoint-arrival marker message.
static bool reply_confirms_checkpoint(const rte_vital_message_t *reply, uint32_t checkpoint_id)
Verifies a candidate reply's CRC/sequence (via rte_checksum) and that its decoded payload matches che...
#define RTE_CHECKPOINT_SENDER_ID
#define RTE_CHECKPOINT_RETRY_ROUND_MS
#define RTE_CHECKPOINT_MAX_ROUNDS
#define RTE_CHECKPOINT_MIN_ROUNDS_PER_BUDGET
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 rte_buffer.h:36
Checkpoint configuration.
rte_duration_ms_t max_delay_ms
Vital channel message with integrated CRC-64.