Header:
#include "mres.h"Version: 1.0.0
microres is platform-agnostic. You provide two callbacks that bridge to your hardware or OS.
typedef uint32_t (*mres_clock_fn)(void);Returns the current time in milliseconds. Must be monotonic. Wrapping at
UINT32_MAX is handled internally (unsigned subtraction). This means the
clock can wrap every ~49.7 days without issues.
Platform examples:
| Platform | Implementation |
|---|---|
| STM32 HAL | HAL_GetTick() |
| FreeRTOS | xTaskGetTickCount() * portTICK_PERIOD_MS |
| ESP-IDF | esp_timer_get_time() / 1000 |
| Zephyr | k_uptime_get_32() |
| Linux | clock_gettime(CLOCK_MONOTONIC) → ms |
| Arduino | millis() |
typedef void (*mres_sleep_fn)(uint32_t ms);Blocks execution for the given number of milliseconds. Used by
mres_retry_exec() between attempts. May be NULL — in that case, retry
executes all attempts without delay (useful for non-blocking or
event-driven architectures).
| Platform | Implementation |
|---|---|
| STM32 HAL | HAL_Delay(ms) |
| FreeRTOS | vTaskDelay(pdMS_TO_TICKS(ms)) |
| Zephyr | k_msleep(ms) |
| Linux | usleep(ms * 1000) |
| Arduino | delay(ms) |
typedef int (*mres_op_fn)(void *ctx);The operation being protected. Returns 0 on success, any negative value
on failure. The negative value is passed through as the error result,
allowing the caller to distinguish between different failure modes.
typedef enum {
MRES_OK = 0,
MRES_ERR_NULL = -1,
MRES_ERR_EXHAUSTED = -2,
MRES_ERR_OPEN = -3,
MRES_ERR_RATE_LIMITED = -4,
MRES_ERR_OP_FAILED = -5,
MRES_ERR_INVALID = -6,
} mres_err_t;| Code | Meaning |
|---|---|
MRES_OK |
Operation succeeded |
MRES_ERR_NULL |
A required pointer was NULL |
MRES_ERR_EXHAUSTED |
All retry attempts failed |
MRES_ERR_OPEN |
Circuit breaker is open, call blocked |
MRES_ERR_RATE_LIMITED |
Rate limit exceeded |
MRES_ERR_OP_FAILED |
The operation itself returned failure |
MRES_ERR_INVALID |
Invalid configuration (e.g., zero attempts) |
const char *mres_err_str(mres_err_t err);Returns a human-readable string for any error code.
Retry wraps a fallible operation and re-executes it with configurable backoff delays when it fails.
attempt 1 delay attempt 2 delay attempt 3
┌──┐ ┌────┐ ┌──┐ ┌────────┐ ┌──┐
│op│──fail──▶│wait│──▶│op│──fail──▶│ wait │──▶│op│──success
└──┘ └────┘ └──┘ └────────┘ └──┘
100ms 200ms
| Strategy | Formula | Example (base=100ms) |
|---|---|---|
MRES_BACKOFF_FIXED |
base |
100, 100, 100, 100 |
MRES_BACKOFF_LINEAR |
base × (attempt+1) |
100, 200, 300, 400 |
MRES_BACKOFF_EXPONENTIAL |
base × 2^attempt |
100, 200, 400, 800 |
All strategies respect max_delay_ms as a ceiling.
Jitter: When enabled, adds ±25% randomisation to the delay. This decorrelates retries from multiple devices hitting the same server simultaneously (the "thundering herd" problem). The jitter uses a lightweight xorshift32 PRNG seeded from the clock value — no external entropy source needed.
typedef struct {
uint8_t max_attempts;
uint32_t base_delay_ms;
uint32_t max_delay_ms;
mres_backoff_t strategy;
bool jitter;
} mres_retry_policy_t;| Field | Description |
|---|---|
max_attempts |
Total attempts including first try. 1 = no retry. Must be > 0. |
base_delay_ms |
Base delay in milliseconds. |
max_delay_ms |
Maximum delay cap. 0 = no cap. |
strategy |
Backoff strategy (FIXED, LINEAR, EXPONENTIAL). |
jitter |
Add ±25% random jitter. |
typedef struct {
const mres_retry_policy_t *policy;
uint8_t attempts;
int last_error;
} mres_retry_t;Runtime state. After execution, attempts tells you how many tries were
made and last_error holds the operation's last return value.
mres_err_t mres_retry_init(mres_retry_t *retry, const mres_retry_policy_t *policy);Initialise retry state. Must be called before mres_retry_exec.
int mres_retry_exec(mres_retry_t *retry, mres_op_fn op, void *ctx,
mres_clock_fn clock, mres_sleep_fn sleep);Execute op(ctx) with retry. Returns MRES_OK if the operation
succeeded on any attempt, or MRES_ERR_EXHAUSTED if all attempts failed.
Sleep behaviour: If sleep is NULL, all attempts execute immediately
without delay. This is useful in event-driven systems where the caller
manages timing externally.
mres_err_t mres_retry_reset(mres_retry_t *retry);Reset attempt counter and error for reuse.
uint32_t mres_delay_calc(const mres_retry_policy_t *policy, uint8_t attempt,
mres_clock_fn clock);Calculate the delay for a specific attempt. Useful for logging or implementing custom retry loops.
A circuit breaker prevents repeated calls to a failing resource. It has three states:
failure_count >= threshold
┌──────┐ ─────────────────────────▶ ┌──────┐
│CLOSED│ │ OPEN │
│ │ ◀───── success ──────────── │ │
└──────┘ └──────┘
▲ │
│ recovery_timeout │
│ elapsed ▼
│ ┌───────────┐ │
└─success─│ HALF_OPEN │◀─────────────┘
│ │
└───────────┘
│ failure
└──────▶ back to OPEN
- CLOSED: Normal operation. Calls pass through. Consecutive failures are counted.
- OPEN: Calls are blocked immediately (returns
MRES_ERR_OPEN). The operation is not invoked. Afterrecovery_timeout_ms, transitions to HALF_OPEN. - HALF_OPEN: A limited number of probe calls are allowed. If any succeeds, transitions to CLOSED. If any fails, transitions back to OPEN.
typedef struct {
uint8_t failure_threshold;
uint32_t recovery_timeout_ms;
uint8_t half_open_max_calls;
} mres_breaker_policy_t;| Field | Description |
|---|---|
failure_threshold |
Consecutive failures before tripping to OPEN. Must be > 0. |
recovery_timeout_ms |
Time in OPEN state before probing in HALF_OPEN. |
half_open_max_calls |
Maximum probe calls allowed in HALF_OPEN. |
mres_err_t mres_breaker_init(mres_breaker_t *br, const mres_breaker_policy_t *policy);Initialise breaker in CLOSED state.
int mres_breaker_call(mres_breaker_t *br, mres_op_fn op, void *ctx,
mres_clock_fn clock);Execute op(ctx) through the breaker. Returns MRES_OK on success,
MRES_ERR_OPEN if blocked, or the operation's error code on failure.
mres_breaker_state_t mres_breaker_state(const mres_breaker_t *br);
const char *mres_breaker_state_name(const mres_breaker_t *br);Query the current state.
uint32_t mres_breaker_remaining_ms(const mres_breaker_t *br, mres_clock_fn clock);Returns milliseconds until recovery attempt. 0 if not in OPEN state.
mres_err_t mres_breaker_reset(mres_breaker_t *br);Force breaker to CLOSED. Useful for manual recovery.
mres_err_t mres_breaker_report_success(mres_breaker_t *br);
mres_err_t mres_breaker_report_failure(mres_breaker_t *br, mres_clock_fn clock);Manually report outcomes without executing through the breaker. Useful for async operations where the result arrives via callback.
Token bucket rate limiter. Tokens are consumed on each call and refilled at a fixed rate.
max_tokens = 5, refill = 2/sec
●●●●● → acquire(1) → ●●●●○
acquire(1) → ●●●○○
acquire(1) → ●●○○○
...1 second passes...
refill(2) → ●●●●○
typedef struct {
uint16_t max_tokens;
uint32_t refill_ms;
uint16_t refill_count;
} mres_ratelimit_policy_t;| Field | Description |
|---|---|
max_tokens |
Bucket capacity (ceiling). Must be > 0. |
refill_ms |
Interval between refills in ms. Must be > 0. |
refill_count |
Tokens added per interval. |
mres_err_t mres_ratelimit_init(mres_ratelimit_t *rl,
const mres_ratelimit_policy_t *policy,
mres_clock_fn clock);Initialise with full tokens.
bool mres_ratelimit_acquire(mres_ratelimit_t *rl, uint16_t count,
mres_clock_fn clock);Try to consume count tokens. Returns true if allowed, false if
rate limited. Automatically refills based on elapsed time before checking.
uint16_t mres_ratelimit_tokens(mres_ratelimit_t *rl, mres_clock_fn clock);Get current available tokens (after refill).
mres_err_t mres_ratelimit_reset(mres_ratelimit_t *rl, mres_clock_fn clock);Reset to full capacity.
microres is not thread-safe by default. Each instance must be accessed from one thread at a time. If you need to share a circuit breaker or rate limiter across threads, protect it with your platform's mutex:
/* FreeRTOS example */
xSemaphoreTake(breaker_mutex, portMAX_DELAY);
mres_breaker_call(&breaker, my_op, &ctx, my_clock);
xSemaphoreGive(breaker_mutex);Different instances can be used from different threads without synchronisation — they share no global state.