recovery

Example of the component adhuldas/edgecommand v0.1.1
# EdgeCommand: Restart Recovery Example

Demonstrates what happens to a command that was interrupted mid-execution
by a device restart, under each of the three recovery policies.

## Purpose

A real power-loss-during-execution can't be scripted deterministically in
an automated example, so this one simulates it directly: it writes an
`EDGECOMMAND_STATE_EXECUTING` record straight into a storage backend —
exactly what would be on flash if a previous run had reached "start
executing" and then lost power before recording a terminal result — and
then calls `edgecommand_start()` to observe what each policy does with it:

- **`EDGECOMMAND_RECOVERY_FAIL`** (the default): the interrupted command is
  immediately marked `FAILED`. Safe for any handler, including ones that
  perform a non-idempotent physical action (actuating a relay, writing to
  another device), because it never re-runs anything.
- **`EDGECOMMAND_RECOVERY_RETRY`**: the interrupted command is re-queued and
  actually re-executed — with an **empty payload**, since the original
  payload was never persisted. Only safe for handlers that are idempotent
  and that can do something useful (or fail cleanly) without their original
  payload.
- **`EDGECOMMAND_RECOVERY_MANUAL`**: the interrupted command is left exactly
  as found (still `EXECUTING`) for the application to inspect and resolve
  explicitly — EdgeCommand takes no automatic action.

## Requirements

- ESP-IDF v5.0 or newer.
- Any ESP-IDF-supported target.
- No external hardware required.

## Build and flash

```sh
cd examples/recovery
idf.py set-target esp32
idf.py build flash monitor
```

## Expected output

```
I (300) recovery_example: === recovery policy: FAIL ===
I (305) recovery_example: seeded 'interrupted-fail-demo' as EXECUTING, simulating a crash mid-execution
I (320) recovery_example: outcome for 'interrupted-fail-demo' under FAIL: state=FAILED
I (330) recovery_example: === recovery policy: RETRY ===
I (335) recovery_example: seeded 'interrupted-retry-demo' as EXECUTING, simulating a crash mid-execution
I (355) recovery_example: resume_task_handler running for interrupted-retry-demo (payload_len=0)
I (400) recovery_example: outcome for 'interrupted-retry-demo' under RETRY: state=COMPLETED
I (410) recovery_example: === recovery policy: MANUAL ===
I (415) recovery_example: seeded 'interrupted-manual-demo' as EXECUTING, simulating a crash mid-execution
I (425) recovery_example: outcome for 'interrupted-manual-demo' under MANUAL: state=EXECUTING
I (430) recovery_example: done
```

Note that `interrupted-manual-demo` is reported back as still `EXECUTING`
— that's correct and expected. `MANUAL` means EdgeCommand deliberately does
not resolve it; a real application would surface this to an operator or
apply its own domain-specific logic (e.g. query the physical device state
to determine what actually happened) rather than trust the stale state.

## What's happening

- Recovery runs synchronously inside `edgecommand_start()`, before the
  worker task is created — there is no concurrent submission or execution
  activity while it runs, so it's inherently race-free.
- This example calls `edgecommand_storage_nvs_create()` itself (instead of
  leaving `edgecommand_config_t::storage` `NULL`) specifically so it can
  seed a record *before* `edgecommand_start()` runs. A normal application
  almost never needs to touch the storage backend directly like this —
  see the ownership note on `edgecommand_storage_nvs_create()` in
  `edgecommand_storage.h` for why this example must destroy the storage
  instance itself afterward, unlike the default (engine-owned) case.
- See `RELIABILITY.md` for the full policy tradeoff discussion, including
  why `RETRY` is dangerous for non-idempotent handlers and exactly what
  is/isn't durable about a command's payload.

To create a project from this example, run:

idf.py create-project-from-example "adhuldas/edgecommand=0.1.1:recovery"

or download archive (~4.22 KB)