wora 0.0.18

A framework for building applications (daemons, etc) that run in different environments (Linux, Kubernetes, etc).
Documentation
wora-0.0.18 has been yanked.

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 RunnerOptions through exec_async_runner_with_options
  • initial typed config and secret loading is built into the runner
  • event-driven apps can use Wora::run_event_loop to 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!("{} bytes used", snapshot.memory.used);

#[cfg(target_os = "linux")]
if let Some(meminfo) = &snapshot.linux_mm.meminfo {
    println!("available: {:?} bytes", meminfo.mem_available);
}

let mut updates = wora.subscribe_memory_stats();
tokio::spawn(async move {
    while updates.changed().await.is_ok() {
        let snapshot = updates.borrow_and_update().clone();
        println!("{} bytes used", snapshot.memory.used);
    }
});

Supported Environments

  • async-based apps
  • Unix-like environments
    • Linux-specific host details through procfs
    • macOS host details through sysinfo
  • service-manager and orchestration integrations
    • SystemdExecutor
    • LaunchdExecutor
    • ContainerExecutor

Architecture

WORA has six main pieces:

  • App: the workload lifecycle trait. Applications implement setup, main, reload_config, reload_secrets, and end.
  • 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 through Wora::report_health(...), Wora::report_readiness(...), or the cloned RuntimeStatusHandle.
  • WFS: the virtual filesystem abstraction. PhysicalVFS is the host filesystem implementation.
  • LockBackend: the single-instance coordination abstraction. ProcLockBackend uses host lock files and InMemoryLockBackend is 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-in rust-toolchain.toml pins 1.95.0 for repository builds.

Usage

Run the memory-reporting profile:

cargo run --example memory_report

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 wora::profiles::MemoryReporter;

#[tokio::main]
async fn main() -> Result<(), wora::errors::MainEarlyReturn> {
    MemoryReporter::new().run().await
}

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 wora::o11y::{LinuxMemoryCollector, LinuxMemoryOptions};
use wora::profiles::MemoryReporter;

# async fn example() -> Result<(), wora::errors::MainEarlyReturn> {
MemoryReporter::new()
    .with_linux_memory_diagnostics(
        LinuxMemoryOptions::new().enable(LinuxMemoryCollector::ProcessMappings),
    )
    .run().await
# }

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.

cargo run --example memory_diagnostics -- --diagnostic-interval 60
# Also request a bounded physical-page range:
cargo run --example memory_diagnostics -- --pfn-start 0 --page-count 1024
# Explicitly enable a private tracefs instance (requires tracefs permissions):
cargo run --example memory_diagnostics -- --trace

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:

cargo run --example basic

Run the daemon-style event loop example:

cargo run --example async_daemon -- --run-mode user

Run the Linux systemd daemon example:

cargo run --example systemd_daemon

Sample unit file:

examples/systemd_daemon.service

Run the onboarding example:

cargo run --example onboarding

Run checks:

cargo fmt --check
cargo test

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

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.

Acknowledgments