edgesentinel
A reliability runtime for Linux edge devices.
Applications describe what they want to happen. edgesentinel handles how that survives failure.
The problem
Code running on a Raspberry Pi, an industrial gateway, or an ARM edge box fails differently than code running in a cloud region:
- The network doesn't just "have latency" -- it disappears for minutes, DNS breaks independently of the link, and MQTT brokers drop silently.
- The process doesn't get gracefully drained -- it gets OOM-killed, the device loses power mid-write, or someone power-cycles it in the field.
- There's no ops team watching a dashboard. If an operation was half-done when the power died, something on the device itself has to notice that on the next boot and decide what to do about it.
- Disk is finite and often on flash storage that wears out if you thrash it with logs.
Retry libraries help with one slice of this (a flaky call). They don't help with the rest: knowing whether you're offline at the network layer, the DNS layer, or the service layer; recovering an operation that was interrupted by a reboot; not blowing the SD card up with log writes while retrying; noticing that a supervised process is stuck in a crash loop instead of restarting it forever.
What edgesentinel is
edgesentinel is a small, asyncio-first runtime you embed in your edge application. You tell it what durable operations, retry policies, and supervised processes you have; it tracks device and network health, persists enough state locally to survive a crash or power loss, and gives you a timeline of what happened when things went wrong.
from edgesentinel import EdgeSentinel
guard = EdgeSentinel("production-gateway", data_dir="/var/lib/edgesentinel")
await guard.start()
@guard.on_state_change
async def handle_state_change(event):
print(event.previous, "->", event.current)
...
await guard.stop()
How this differs from a retry library
| Retry library | edgesentinel | |
|---|---|---|
| Scope | One function call | Whole application lifecycle |
| Network awareness | None (just fails and retries) | Link / gateway / DNS / internet / service, as separate layers |
| Survives a reboot | No | Durable operations replay from a local journal on boot |
| Process supervision | No | Detects crash loops, escalates instead of restarting forever |
| Storage awareness | No | Monitors free space / inodes, scoped cleanup policies |
| Observability | Whatever you bolt on | Built-in event timeline and incident reports |
edgesentinel uses retry, backoff, and circuit breakers internally -- they're
necessary, not sufficient. It is not a replacement for tenacity; it's the
layer above that decides when to retry, what to do if retrying never
works, and how to prove afterwards what happened.
Who should use this
Engineers building applications that run unattended on Linux edge hardware (Raspberry Pi, Jetson, industrial PCs, ARM/x86 gateways, Docker-based edge deployments) and need them to keep working -- or fail safely and recover on their own -- without a human nearby to restart things.
edgesentinel complements systemd, it doesn't replace it. Use systemd (or
your container runtime) to keep your process running; use edgesentinel inside
that process to keep your application's operations correct across network
loss, dependency failure, and crashes.
Architecture
APPLICATION
|
EDGESENTINEL
|
RELIABLE EXECUTION -> FAILURE DETECTION -> RECOVERY -> DIAGNOSTICS
edgesentinel/
├── core/ runtime, lifecycle state machine, events [Phase 1 - done]
├── persistence/ SQLite storage, migrations, journal [Phase 1 & 3 - done]
├── resilience/ retry, backoff, timeout, circuit breaker [Phase 2 - done]
├── durability/ intent journal, durable operations, replay [Phase 3 - done]
├── process/ supervisor, watchdog, health checks [Phase 4 - done]
├── network/ layered connectivity (link/gateway/DNS/internet) [Phase 4 - done]
├── storage/ free-space monitoring, scoped cleanup [Phase 4 - done]
├── diagnostics/ event timeline, incidents, reports [Phase 5 - done]
├── integrations/ MQTT, HTTP (optional extras) [Phase 6 - done]
├── metrics/ CPU/memory/temperature monitoring, mitigation [Phase 7 - done]
└── cli/ edgesentinel status / timeline / incidents [Phase 5 - done]
Development status
edgesentinel is built in phases, each one fully tested before the next begins.
This is Phase 7. See CHANGELOG.md for exactly what exists today.
Implemented and tested:
EdgeSentinelruntime:start()/stop()/ async context manager.- A strongly-typed lifecycle state machine (
BOOTING->INITIALIZING->HEALTHY/DEGRADED/OFFLINE/RECOVERING/FAILED->STOPPING->STOPPED) that rejects invalid transitions.@guard.on_state_changefor observing every transition. - An async event bus that later subsystems (diagnostics, metrics) plug into.
- Local SQLite persistence (WAL mode, versioned migrations, transactions) with the runtime's own state persisted across restarts.
- Backoff algorithms (fixed, linear, exponential) with optional full jitter,
and a
RetryPolicyfor retrying an async operation with configurable attempts, exception filtering, and anon_retryhook. - Per-attempt timeouts (
OperationTimeoutErroron expiry). - A
CircuitBreaker(CLOSED/OPEN/HALF_OPEN) with a single concurrency-safe HALF_OPEN trial and optional state persistence that survives a restart. guard.reliable(): a decorator composing circuit breaker, retry/backoff, and per-attempt timeout around an async function, publishing events onto the runtime's event bus as it retries or resolves.- A write-ahead intent journal (SQLite-backed): every
guard.durable(...)call is recorded as apendingintent before it runs, so a crash or reboot mid-call leaves a durable record instead of silently losing the operation. guard.durable(operation): a decorator giving an async function at-least-once, crash/reboot-safe execution semantics. Registering the operation works beforestart(), same asreliable(); calling it requires the runtime to be started, since it must write to the journal first.- Startup replay:
start()replays every intent leftpendingorin_progressby a previous run before the runtime becomes healthy. One intent failing (or exhaustingmax_attempts) never blocks the rest of the journal or the runtime from starting; unregistered operations are left pending and reported via adurable_operation_unhandledevent. NetworkMonitor: polls a configurable subset of LINK/GATEWAY/DNS/INTERNET checks bottom-up, stopping at the first failing layer, and can drive the runtime toHEALTHY/DEGRADED/OFFLINEas connectivity changes.tcp_reachable()/dns_resolves()ship as stdlib-only building blocks.Supervisor: restarts a crashed or exited in-process async task with backoff, giving up (and, if wired to the runtime, escalating toFAILED) after too many crashes within a sliding window instead of restarting a broken task forever.Watchdog: heartbeat-based staleness detection for tasks that hang instead of crashing -- anything that stops checking in within its timeout is reported stale and can escalate the runtime toFAILED.StorageMonitor: polls free bytes (and, optionally, free inodes) on a path and runs scoped cleanup actions in order when usage drops below a low-water mark, moving the runtime toDEGRADEDwhile low and escalating toFAILEDif cleanup runs out without freeing enough space.guard.watch_network(),guard.supervise(),guard.watchdog, andguard.watch_storage(): factory methods wiring each subsystem above to the runtime's event bus and lifecycle state. Registering beforestart()makes the runtime start and stop the subsystem automatically, same asguard.durable(...).guard.timeline: anEventLogattached to the runtime's event bus from right afterstart()runs migrations untilstop()closes the database, durably recording every event -- including boot/shutdown transitions -- to a localeventstable. Queryable by component, type, minimum severity, and time range, from a live runtime or (via a standaloneDatabase/EventLog) a stopped one.guard.incidents: anIncidentTrackergrouping spans of non-HEALTHYtime intoIncidentrecords from the samestate_changeevents, regardless of which Phase 4 subsystem triggered them.build_incidents()reconstructs the same incidents offline from a persisted timeline, for inspecting a runtime that isn't currently running.- The
edgesentinelCLI:edgesentinel --name <name> --data-dir <dir> status|timeline|incidents, reading a runtime's on-disk SQLite database directly -- no live runtime process required, thanks to WAL mode. HttpEventPublisher(edgesentinel.integrations.http): forwards events as JSON POSTs to a webhook URL. Stdlib-only (urllibwrapped inasyncio.to_thread), so no extra dependency is needed to use it.MqttPublisher(edgesentinel.integrations.mqtt): forwards events to an MQTT broker. Talks to a small structuralMqttClientprotocol rather than a concrete library, so the module itself has no import-time dependency; connecting for real needs thepaho-mqttpackage (pip install edgesentinel[mqtt]).- Both integrations support
min_severityfiltering and attach/detach the same wayEventLogandIncidentTrackerdo, and never let a publish failure (network error, bad status, broker down) propagate and crash the reliability path they're observing. MetricsMonitor(edgesentinel.metrics): polls CPU load average, memory pressure, and (where available) SoC temperature, reading them via stdlib-only, injectable checks (os.getloadavg,/proc/meminfo, a Linux thermal zone) -- nopsutildependency. Same low/high-water-mark shape asStorageMonitor: any configured threshold being crossed runs a caller-supplied sequence of mitigation actions, in order, stopping once usage is back down; exhausting them without recovering escalates the runtime the same way cleanup exhaustion does for storage.guard.watch_hardware(): factory method wiring aMetricsMonitorto the runtime's event bus and lifecycle state, same registration-before-start contract asguard.watch_network()andguard.watch_storage().
Anything described elsewhere in this repository's design docs that isn't listed under "Implemented" above is a design target, not shipped behavior.
Installation
pip install edgesentinel # not yet published -- see status above
For local development, see CONTRIBUTING.md.
pip install -e ".[dev]"
Getting started
import asyncio
from edgesentinel import EdgeSentinel, RuntimeState
async def main() -> None:
guard = EdgeSentinel("my-device", data_dir="./data")
@guard.on_state_change
async def on_change(event):
print(f"{event.previous.value} -> {event.current.value}")
async with guard:
assert guard.state is RuntimeState.HEALTHY
@guard.reliable(retries=5, timeout=10, circuit_breaker=True)
async def publish_reading(value: float) -> None:
# ... call a flaky downstream service ...
...
await publish_reading(21.5)
asyncio.run(main())
reliable() wraps the decorated function with a circuit breaker (outermost),
retry/backoff (middle), and a per-attempt timeout (innermost): a breaker trip
means "this operation isn't succeeding even after retrying", not "one
attempt failed", so a single flaky call never trips the breaker by itself.
guard = EdgeSentinel("my-device", data_dir="./data")
@guard.durable("publish_reading")
async def publish_reading(sensor_id: str, value: float) -> None:
# ... call a downstream service ...
...
async def main() -> None:
async with guard:
await publish_reading(sensor_id="temp-1", value=21.5)
durable() journals the call to local SQLite before running it. If the
process crashes or the device loses power mid-call, the intent survives on
disk; the next start() (with recovery=True, the default) replays it
automatically. Arguments must be JSON-serializable and passed by name --
the decorated function can't declare *args/**kwargs -- and the function
must be safe to run more than once, since at-least-once execution means it
may run again for the same logical call.
Testing
make test # pytest
make check # lint + typecheck + test
The test suite is deterministic: no real network access, real sleeps, or physical hardware. Concurrency and crash-recovery behavior are tested with in-process fakes (e.g. asserting 100 concurrent SQLite writes never lose a row, or that a failed initialization never leaks an open database connection).
License
MIT -- see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file edgesentinel-0.1.0.tar.gz.
File metadata
- Download URL: edgesentinel-0.1.0.tar.gz
- Upload date:
- Size: 146.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b20eb30a880364c7a446b214b024521a8e896d8b954accad7b1e428a89786d1b
|
|
| MD5 |
6e0953314269605016a7010e1c498bec
|
|
| BLAKE2b-256 |
c56fbf81d382c8fa22dbb893426927fd86a293413b53e81949d52371c6c01c30
|
Provenance
The following attestation bundles were made for edgesentinel-0.1.0.tar.gz:
Publisher:
release.yml on adhuldas/EdgeSentinel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
edgesentinel-0.1.0.tar.gz -
Subject digest:
b20eb30a880364c7a446b214b024521a8e896d8b954accad7b1e428a89786d1b - Sigstore transparency entry: 2652198202
- Sigstore integration time:
-
Permalink:
adhuldas/EdgeSentinel@054d2bffe5cb91de39ad22ef635bf8989b499ffd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/adhuldas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@054d2bffe5cb91de39ad22ef635bf8989b499ffd -
Trigger Event:
push
-
Statement type:
File details
Details for the file edgesentinel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: edgesentinel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 82.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d822fbc8542eff955abe8c7985f47ad549d7dc6bbc85e08ca866fdb3d16fd8b3
|
|
| MD5 |
fb122ae8909b7536c2ca5da67d66763a
|
|
| BLAKE2b-256 |
7f514b7301b00f217e30a94ce0419017c55d836031ecdda9e3dd30282ac32047
|
Provenance
The following attestation bundles were made for edgesentinel-0.1.0-py3-none-any.whl:
Publisher:
release.yml on adhuldas/EdgeSentinel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
edgesentinel-0.1.0-py3-none-any.whl -
Subject digest:
d822fbc8542eff955abe8c7985f47ad549d7dc6bbc85e08ca866fdb3d16fd8b3 - Sigstore transparency entry: 2652198327
- Sigstore integration time:
-
Permalink:
adhuldas/EdgeSentinel@054d2bffe5cb91de39ad22ef635bf8989b499ffd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/adhuldas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@054d2bffe5cb91de39ad22ef635bf8989b499ffd -
Trigger Event:
push
-
Statement type: