wora
Write Once Run Anywhere (WORA): A Rust framework for building applications (daemons, etc.) that run in different environments (Linux, Kubernetes, etc.). Just like Java's claim, it really doesn't run everywhere. no_std or embedded environments are not supported.
Status
This crate is an early-stage async framework. The main public API is usable, with Unix-like platforms as the primary target and environment-specific executors for systemd, launchd, and container-style deployments.
- restart policies, shutdown supervision, and signal mapping can be applied with
RunnerOptionsthroughexec_async_runner_with_options - initial typed config and secret loading is built into the runner
- event-driven apps can use
Wora::run_event_loopto auto-apply typed reloads RunnerOptions::with_signal_mapper(...)lets apps map Unix-like signals to control or app-defined events- observability can be routed through sinks via
O11yProcessor
Feature Tour
- abstracts over common boilerplate with an API
- execute the same code in different executors (with or without recompiling)
- translates platform-specific control inputs into runtime control or app-defined events
- watches existing metadata and secrets directories and emits typed reload events
- supervises readiness, health, and graceful shutdown
- exposes host, process, runtime, and tracing observability events
- ships observability sinks and a processor for fan-out
Memory samples arrive as
Event::SystemResource(SystemResourceStat::MemorySnapshot(snapshot)). Read
wora.memory_stats() at any time for the latest stored sample, including during
app setup, or use wora.subscribe_memory_stats() for a cloneable Tokio watch
receiver. These APIs work on Linux and macOS, with portable memory and swap
totals in bytes. stats_from_start() continues to return the startup snapshot.
Set the sampling interval with O11yPipeline::builder().host_stats_interval(...)
(30 seconds by default). Change it while running through
control.apply(O11ySettingsPatch::new().host_stats_interval(...)).await.
The interval controls memory events, the stored sample, and host/process
observability metrics. Each successful host refresh updates the cache before
notifying the application. A full application event queue skips that periodic
notification; the cache and watch listeners still receive the latest sample.
Failed host refreshes retain the previous sample, and watch listeners may
coalesce updates when they read slowly.
On Linux, snapshot.linux_mm and HostStats::linux_mm include Meminfo,
VmStat, MemoryPressure, SharedMemorySegments, Iomem, BuddyInfo,
ZoneInfo, Swaps, SlabInfo, and PageTypeInfo, including their nested data.
linux_mm.sysctls additionally captures memory-related sysctl values: all
regular VM settings, kernel shared-memory and NUMA-balancing settings, the
perf-event locked-memory limit, and selected filesystem cache and descriptor
inventories. Read them through snapshot.linux_mm.sysctls.values, keyed by
dotted names such as vm.swappiness and kernel.shmall. Values retain their
native text and units, including vectors and large unsigned integers.
unavailable records per-key read failures such as permission_denied, and
skipped identifies excluded action controls. Settings are refreshed at the
same interval as the other memory data.
The next memory views are included in the same snapshots and profile output:
Field under linux_mm |
Scope and contents |
|---|---|
cgroup |
The collecting process's cgroup v2: usage, limits, named statistics, local/hierarchical events, NUMA usage, swap/zswap, and pressure. Visible ancestors include usage and limit settings, nearest parent first. |
hugepages |
THP policies, khugepaged activity, per-size counters, and global/per-node HugeTLB pools from sysfs. |
process |
The collecting PID and smaps_rollup, including RSS, proportional set size (PSS), private/shared memory, dirty memory, swap and locked memory. |
Cgroup discovery uses /proc/self/cgroup and /proc/self/mountinfo, including
mount roots and escaped paths. It preserves max as an unlimited value and
keeps memory.events separate from memory.events.local. Ancestor settings
are not a computed effective limit: ancestors outside the visible mount remain
unknown. Cgroup v1 and inconsistent namespace layouts produce explicit
discovery failures. No cgroup memberships or limits are changed.
Hugepage numbers retain each file's units and size labels; policy strings retain
the brackets marking the selected option. memory.reclaim, HugeTLB demote,
and write-only controls are excluded from reads. Process rollup values retain
their units (kB means KiB); SmapsRollupValue::bytes() provides checked
conversion. All three views record individual read/parse failures without
discarding other memory data. Process page-table walks can be expensive for
large workloads; choose the reporting interval accordingly.
For captured inputs, use LinuxMemoryStats::from_roots(proc_root, sys_root);
from_proc_root collects proc-backed sources and leaves hugepages as None.
Cgroup mount points in captured mountinfo are used as written, so point them at
captured cgroup directories. The standalone readers are CgroupMemoryStats,
HugePageStats, and ProcessMemoryStats in wora::o11y::linux_mm.
The ten procfs snapshot sources are optional so unavailable or restricted files
do not interrupt other memory sampling. Values retain their source units and
page-type count bounds. Slab and page-type files commonly require root; page-type reads can be
expensive, so choose the interval accordingly. All types are re-exported through
wora::o11y::linux_mm, and the Linux dependency and fields are gated with
#[cfg(target_os = "linux")].
The following sysctl read restrictions were observed on inanna as a normal
user (Linux 6.18.55, 2026-10-16):
| Sysctl | Access | Collection behavior |
|---|---|---|
vm.mmap_rnd_bits |
Root-only read/write, mode 0600 |
Value if readable; otherwise unavailable: permission_denied |
vm.mmap_rnd_compat_bits |
Root-only read/write, mode 0600 |
Value if readable; otherwise unavailable: permission_denied |
vm.stat_refresh |
Root-only read/write, mode 0600 |
Always skipped: reading changes VM statistics |
vm.compact_memory and vm.drop_caches have mode 0200: they are write-only
action controls with no readable value, even for root. They are always skipped.
The collector does not invoke sysctl, write settings, or change privileges.
The kernel documents the read side effect of
stat_refresh.
Available names and access permissions vary with the kernel and host policy.
Inside an app lifecycle hook, use the common API and guard Linux details:
let snapshot = wora.memory_stats;
println!;
if let Some = &snapshot.linux_mm.meminfo
let mut updates = wora.subscribe_memory_stats;
spawn;
Supported Environments
- async-based apps
- Unix-like environments
- Linux-specific host details through
procfs - macOS host details through
sysinfo
- Linux-specific host details through
- service-manager and orchestration integrations
SystemdExecutorLaunchdExecutorContainerExecutor
Architecture
WORA has six main pieces:
App: the workload lifecycle trait. Applications implementsetup,main,reload_config,reload_secrets, andend.AsyncExecutor: the environment adapter. Executors provide directories, setup, readiness, and teardown behavior.Wora: the runtime context passed to apps. It carries directories, host data, event channels, runtime status, and observability options. Health and readiness are reported throughWora::report_health(...),Wora::report_readiness(...), or the clonedRuntimeStatusHandle.WFS: the virtual filesystem abstraction.PhysicalVFSis the host filesystem implementation.LockBackend: the single-instance coordination abstraction.ProcLockBackenduses host lock files andInMemoryLockBackendis useful for tests and fully virtual runner flows.O11yProcessor: the observability pipeline for fan-out into sinks.
exec_async_runner wires those pieces together by initializing observability, acquiring the runtime lock, building the Wora context, running executor setup, loading initial config and secrets, running app setup, watching the metadata and secrets directories, invoking App::main, supervising shutdown/readiness/health, and then running teardown. Runner-owned background work shares a cancellation token: teardown requests cooperative cancellation, waits for a bounded grace period, and aborts tasks that do not stop.
The runner always installs recursive watchers on both metadata_root_dir and secrets_root_dir after executor and app setup. Executors must create both watch roots even when an app uses NoConfig or NoSecrets; initial loading skips roots that are absent, but watcher installation still requires them. The default filesystem lock backend creates the parent of its lock path before acquiring the lock, allowing executor setup to create the remaining roots afterward.
Typed config and secret reloads are driven by filesystem watcher events (Event::ConfigChanged and Event::SecretChanged). ControlEvent::ReloadConfiguration, ControlEvent::Suspend, and ControlEvent::LogRotation are app-level control notifications: the runner delivers them to the app but does not automatically reload files, suspend work, or rotate sinks for the app. Use RunnerOptions::with_signal_mapper(...) when the default Unix-like signal mapping should produce app-defined events instead.
When a lock backend reports contention, the runner returns MainEarlyReturn::AlreadyRunning(...) instead of collapsing that case into a generic exit code.
Getting Started
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
Prerequisites
- Rust 1.95 or newer. The crate declares
rust-version = "1.95"and the checked-inrust-toolchain.tomlpins1.95.0for repository builds.
Usage
Run the memory-reporting profile:
Profiles combine a workload's default configuration with the existing WORA runtime so small programs need very little setup. The first profile prints one complete memory snapshot as a JSON line at startup and after every successful host sample, flushing each line immediately. Stop it with Ctrl-C or SIGTERM.
use MemoryReporter;
async
MemoryReporter defaults to a five-second interval, stdout, the application
name wora-memory-report, and a per-user Unix executor. Customize it with
.with_interval(Duration::from_secs(30)), .with_name("my-memory-report"), or
.with_writer(writer) for a Tokio async writer. Use .run_with(exec, fs, options)
to supply an existing executor and RunnerOptions. The profile uses NoConfig
and NoSecrets, and does not install a global tracing subscriber.
On Linux, expensive diagnostics are explicitly selected and sampled separately
(60 seconds by default). Cached diagnostics carry sampled_at, so their age is
visible in each report. For example:
use ;
use MemoryReporter;
# async
Use LinuxMemoryEnvironment::new(provider, options) with runner options to add
the same diagnostics to another application or custom telemetry provider.
.with_interval(...) on LinuxMemoryOptions sets the diagnostic cadence;
the profile's interval controls routine reports. Neither grants privileges.
Routine Linux reports also include NUMA nodes/tiers, zram/zswap accounting, KSM,
process status/limits/OOM preferences, and HugeTLB/cpuset cgroup context.
LinuxMemoryOptions::all_snapshots() explicitly selects the full diagnostic set:
process mappings, SLUB, kernel allocation attribution, fragmentation, MGLRU,
page-owner, kernel leak suspects, and memory blocks/EDAC/CMA. Physical and virtual
page scans require explicit bounded PageRange values. Sources unavailable due
to privileges or kernel configuration retain independent failures.
# Also request a bounded physical-page range:
# Explicitly enable a private tracefs instance (requires tracefs permissions):
The diagnostic example opts into potentially costly reads, including kmemleak.
MemoryReporter::with_linux_memory_trace(LinuxMemoryTraceOptions::new()) adds
OOM victim, reclaim timing, compaction and migration records. These flush as
additional {"timestamp":...,"memory_trace":...} lines, including startup,
kernel-loss and unfamiliar records. The service owns only its private tracefs
instance and cleans it up on normal shutdown/cancellation. An abrupt process
death may leave the instance identified in the startup record. Snapshot and
event collectors do not attempt to obtain additional privileges.
Each JSON line contains timestamp, memory, swap, and, on Linux, linux_mm.
All Linux sources listed above are included with every available nested
value, including zone per-CPU statistics, allocator counts, sysctls, cgroup memory,
hugepages and the process rollup. Unavailable
procfs snapshots appear as null; sysctls retain per-key errors and exclusions.
Source units, integer precision, and page-type lower
bounds are preserved. Slab and page-type files commonly require root; run with
suitable read permissions to include those sources. Output errors stop the
profile and are returned to the caller. On macOS, the same profile prints the
portable memory and swap totals.
Run the basic example:
Run the daemon-style event loop example:
Run the Linux systemd daemon example:
Sample unit file:
examples/systemd_daemon.service
Run the onboarding example:
Run checks:
AI Disclosure
OpenAI Codex is used to write documentation/tests and find issues. Everything is reviewed by me and I vouch for these AI-generated changes.
Versioning
We use SemVer for versioning when x.y.z when x is > 0, otherwise versioning is arbitrary. For the versions available, see the tags on this repository.
Authors
- Adam Flott - Initial work - adamflott
See also the list of contributors who participated in this project.
License
This project is dual-licensed under either the MIT License or the Apache License 2.0, at your option.