orbit-rs 0.1.0

Fleet-aware shared-memory rings over POSIX shared memory.
Documentation

orbit-rs

orbit-rs is the primitive Orbit crate: a same-host runtime substrate for recent facts shared by sibling processes.

It provides type-keyed bounded rings, optional POSIX shared-memory backing, fleet membership, heartbeat records, and small reusable substrates for cache, events, and contest/claim coordination.

orbit-rs is framework-agnostic. Application lifecycle and runtime policy belong above this crate.

It is not a database, message broker, service process, global registry, or durable log.

Model

Fleet
  process-local handle into a named fleet

OrbitTyped
  Rust type -> stable KIND byte -> ring family

Ring / Frame
  fixed-capacity append surface with bounded payloads

netid64::NetId64
  runtime-bound id carried by every frame

Substrates
  cache, event bus, contest, heartbeat, typed POD values

Frame identifiers come from the external netid64 crate. Orbit uses that runtime-bound identifier format; it does not define the identifier type itself.

Fleet

Fleet is the per-process handle into Orbit.

Fleet::join(...)
  -> process-local rings

Fleet::join_shm(...)
  -> POSIX shared-memory rings visible to sibling processes

A fleet has a name, a NodeId, an expected fleet size, and one ring registry. Every OrbitTyped::KIND maps to its own ring. Role hierarchy is outside the crate: master, worker, standalone process, or sibling tool can all join the same fleet if the embedder gives them compatible configuration.

On Unix, shared-memory ring names are derived from:

/orbit-{fleet}-{kind}-{uid}

Rings

A ring is a fixed-capacity circular append surface. Writers reserve a monotonic counter, mint a NetId64, and write a frame into:

counter % capacity

The frame shape is:

id: NetId64 | frame kind: u8 | version: u64 | payload: bytes

Two kind values are intentionally present:

id.kind()
  the OrbitTyped value family, used to choose the ring

frame.kind
  the message/opcode class inside that ring

Readers must tolerate wraparound. If a reader asks for a counter whose slot has already been overwritten, the frame is missing by design.

Typed Rings

OrbitTyped is the type-to-ring contract. A Rust type declares a stable KIND, and that KIND selects the ring family used for its frames.

use orbit_rs::OrbitTyped;

#[repr(C)]
#[derive(Clone, Copy)]
struct WorkerLoad {
    busy: u32,
    idle: u32,
}

impl OrbitTyped for WorkerLoad {
    const KIND: u8 = 12;
}

Callers do not pass ring names through the API. They pass their own type. Every worker built from the same program knows the same KIND, so Fleet::publish::<WorkerLoad> and Fleet::read_head::<WorkerLoad> meet on the same ring.

Orbital<T> is the fixed-size value helper:

T: OrbitTyped + bytemuck::Pod
  -> Orbital<T>::store(value)
  -> Fleet::publish::<T>
  -> ring selected by T::KIND
  -> Orbital<T>::load()
  -> T

This path is for small structs whose byte layout is known and stable. Variable-length records use explicit encoders in layers such as cache, event, contest, and metrics.

Cache

OrbitCache is a byte-oriented cache primitive over one dedicated ring. It does not choose a serializer and it does not know application values.

Each mutation is one frame:

put    key bytes, value bytes, optional expiry
delete key bytes
reset  prefix bytes

Reads walk backward from the ring head. The newest matching frame wins:

  • put returns a value unless expired;
  • delete shadows older puts;
  • reset shadows older entries inside the cache prefix.

Values are inline and bounded by the ring payload size. Larger object caches should be built above this primitive, for example with a ring as mutation log plus a separate shared arena.

Event Bus

OrbitEventBus is an append-only event stream over one Orbit ring. Events are not cache entries and not metrics snapshots:

cache   asks: what is the newest value for this key?
metrics asks: what is the newest sample for each node/key?
events  ask: which frames appeared since my cursor?

All topics deliberately share the same event ring. The topic is carried inside the frame payload, so adding a new event type does not allocate another shared-memory segment.

Each subscriber owns its own cursor. Polling advances that cursor across all frames, including frames later filtered out by topic. If a subscriber falls behind the fixed ring window, the poll result reports lag.

OrbitEventBus::publish(topic, payload)
  -> topic/payload/timestamp frame
  -> shared event ring
  -> OrbitEventCursor
  -> poll() / poll_topic()

Typed dispatch, application lifecycle hooks, acknowledgements, durable replay, and consumer groups belong above this primitive.

Contest

Contest is not a race primitive. It turns simultaneous interest in the same typed subject into a small claim/yield protocol.

claim typed subject
  -> observe active claims
  -> earliest active claimant receives Guard
  -> later claimants receive YieldTo(holder)
  -> dropping Guard publishes release

Every peer may publish a claim. The earliest active claim receives a drop-released Guard; later claimants receive YieldTo(holder).

A subject is a caller-defined ContestType::KIND plus a label. The owner label is only for observation; Orbit does not interpret it.

The important bias is that followers yield. Orbit does not serialize a queue or make the holder faster. It lets one peer carry a typed subject while others can observe who carries it and back off.

TTL handles abandoned claims. Releasing is tied to the Guard lifetime, so normal Rust scope becomes the release boundary for successful work.

Heartbeat

FleetHeartbeat is an Orbit substrate signal. It shows that a node is still publishing into the shared fleet fabric.

It is not process supervision. Kill/restart/readiness policy belongs to the embedding runtime.

Dependency Boundary

Application lifecycle, typed runtime adapters, handlers, process supervision, and product policy should live above orbit-rs.