rullst-core 12.3.0

Core runtime, routing engine, and telemetry for the Rullst framework suite.
Documentation

Rullst Core ⚙️

rullst-core encapsulates the foundational primitives, routing engines, state management, and configuration layers of the Rullst Framework. It acts as the beating heart that orchestrates HTTP handlers, middleware, and backend worker systems.

✨ Features

  • Typed Axum Integration: Wraps Axum routing while retaining access to the underlying router and middleware ecosystem.
  • Bounded Server Functions: #[server_function] shares the versioned rullst.client v1 envelope between native Axum routes and Wasm callers, with a 256 KiB body ceiling, correlation checks, same-origin paths, CSRF header forwarding, and message-free typed failures. Identity and domain policy stay server-owned.
  • Runtime Telemetry: Exposes process/runtime snapshots and tracing-span collection for consumers such as Rullst Studio.
  • Lifecycle-aware Readiness: An opt-in process lifecycle gates new requests during startup, dependency unavailability, and graceful drain. It accepts at most 32 immutable component labels and exposes counts—not labels or errors—on /ready; dependency checks and multi-replica coordination remain host work.
  • Bounded Rate Limiting: RateLimiter keys IPv4 peers per address and IPv6 peers per /64, and bounds its process-local bucket map to 100,000 keys by dropping refilled buckets and evicting the least recently used ones. Server's limiter and Traffic Shield exempt exact GET/HEAD /health and /ready probes.
  • Typed Failures: Server, scheduler, queue, storage, and resilience APIs expose structured errors for fallible paths. The repository's zero-panic policy is CI-scoped, not an absolute runtime guarantee.
  • Dependency Injection: Type-safe, intuitive global state management across routes and background workers.
  • Environment Management: Native dotenv and TOML configuration loaders for different deployment targets (Staging, Production, Local). Server and the Artisan db:*/studio commands resolve the database the same way: process DATABASE_URL, then ./.env (never overriding the process), then [database].url. Database commands without a configured database fail instead of creating a SQLite file.
  • Local Edge Emulator: edge::EdgeServer::run binds 127.0.0.1 unless HOST/RULLST_HOST is set, serves every path including /, and rejects a body over 2 MiB with 413 (or an unreadable one with 400) instead of passing the handler an empty body. It applies no security baseline.
  • Durable Scheduled Queues: SQLite and Redis persist bounded dispatch_at timestamps and never claim a job before its millisecond due time. Delivery is poll-dependent and at-least-once.
  • Fenced Queue Leases: SQLite and Redis complete, fail or requeue a claimed job only under the attempt number pop returned, so a stale worker whose lease was recovered and claimed again cannot finish the newer claim.
  • Queue-wide Stalled-lease Recovery: each worker periodically requeues every processing lease older than its stalled_after, including other workers' leases, so every worker sharing a queue needs a stalled_after longer than the longest job_timeout among them. SQLite and Redis fail a job whose fifth lease stalls (for example because it keeps crashing its worker) instead of requeuing it forever.
  • Rolling-deploy Safe Dispatch: A worker without a handler for a job's name hands the claim back with a five-second delay (SQLite and Redis) instead of failing it, so a worker that registered that name can run it.
  • Bounded Redis Failure State: Failed jobs and dead letters are each retained up to 10,000 entries (configurable with RedisDriver::try_with_failure_retention), evicting the oldest atomically.
  • Swappable HTMX Validation Errors: ValidatedForm/ValidatedJson send HTMX requests their error fragment with 200 OK plus X-Rullst-Validation-Status: 400|422; other clients keep 400/422 JSON.
  • Cron Semantics: Scheduler::task evaluates five-field expressions in UTC with the cron crate's rules: weekdays 1 (Sunday) to 7 (Saturday), 0 rejected, and restricted day fields must both match. Prefer weekday names.
  • Bounded Background Errors: WorkerHandle and SchedulerHandle buffer at most 256 undrained errors; overflow is dropped, counted by dropped_errors() and logged as a tracing warning. Drain next_error to observe every failure. Server::schedule drains its own handle, logging each task failure; only a failed scheduler loop makes Server::run return an error.
  • Explicit Completion History: SQLite deletes successful payloads by default. Queue::sqlite_with_completed_history opts into a bounded retained history for Studio/operations, with atomic pruning and an explicit purge API.
  • Atomic Local Writes: Storage::local/LocalDriver::put writes a unique temporary file beside the object, flushes it and renames it over the key. Readers and concurrent writers see one complete version, and a failed write leaves the previous object intact. Each put creates a new file with default permissions; the directory is not fsynced, so a power loss can roll a completed put back to the previous version.
  • Encoded Object URLs: Storage::url/LocalDriver::url percent-encode each key segment. Local storage always returns /storage/<key> (serve the base directory there), never the filesystem base path.
  • Metadata-only Cache Inspection: Memory and Redis drivers can return a sorted snapshot of at most 200 logical keys, UTF-8 value lengths and TTLs without returning values. Custom drivers fail explicitly unless they opt in; authorization and safe rendering of logical keys remain caller policy.
  • Offline Sync Foundation: The optional native offline-sync feature provides bounded replay-safe mutations, explicit conflict resolution, and account-bound AES-256-GCM snapshots. Its static-dispatch coordinator bounds requests, times them out, and detects stalled cursors. Applications still own Keychain/Keystore access, atomic platform persistence, authenticated HTTP, retry, and background scheduling.

🚀 Usage

Most developers will not depend on rullst-core directly, as it is re-exported seamlessly through the primary rullst crate.

If you are developing a plugin or advanced middleware for the Rullst ecosystem, you can add it explicitly:

Install the exact stable train with cargo add rullst-core@12.3.0.

Core is runtime-only by default. Add just the database capabilities the application needs:

[dependencies]
rullst-core = { version = "12.1.0", features = ["orm", "queue-sqlite", "offline-sync"] }

Enable orm, queue-sqlite, queue-redis, offline-sync, or telemetry only when that integration is required. The primary rullst crate keeps orm and queue-sqlite in its default feature set for application compatibility.

Both built-in queue drivers implement Queue::dispatch_at for schedules up to 366 days ahead. SQLite performs an automatic additive schema migration; Redis uses a sorted set and server time, with a digest-pinned live CI contract. Custom drivers fail with QueueError::Unsupported for future jobs until they implement the scheduling method explicitly.

Successful SQLite jobs are removed unless the application explicitly calls Queue::sqlite_with_completed_history(database_url, retained_jobs). The limit must be between 1 and 100,000; completion and pruning share one transaction. Retained rows include the original payload, so the application must restrict Studio/inspection access and choose an appropriate retention policy. Use Queue::purge_completed_history to remove them.

Cache::inspect(limit) is opt-in diagnostic data, not an administration endpoint. The limit must be 1–200. CacheEntryMetadata deliberately redacts the logical key from Debug and never carries the cached value, but logical_key() still returns application data to an authorized caller. Rullst Studio converts it into a process-bound opaque token before rendering it. Cache::memory() stores a TTL too large for the monotonic clock (such as u64::MAX) as non-expiring instead of panicking.

For orchestrated deployments, construct ApplicationLifecycle, mount health_router_with_lifecycle(lifecycle.clone()), then pass the same value to Server::with_lifecycle. The server marks startup complete after binding, begins drain before Axum waits for accepted requests, and marks startup failure or termination as stopped. run_with_shutdown permits a caller-owned trigger. Admission follows the ordinary response body through completion, error or drop, including streamed data/trailers. It does not prove client receipt or track upgraded connections. wait_for_drain is bounded; supervisors separately own process kill deadlines and application WebSocket/background-task termination. The process-local registry does not probe dependencies, authorize application requests, coordinate replicas, or guarantee load-balancer propagation.

Minimal HTTP Server

use rullst_core::{Router, Server};
use rullst_core::routing::get;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = Router::new().route("/health", get(|| async { "ok" }));
    Server::new(app).run(3000).await?;
    Ok(())
}

🔐 Security Audit

Repository workflows exercise Core with unit, integration, fuzz, and Miri jobs within their declared scopes. Consult the exact workflow run and commit for evidence; these tools do not prove the absence of every panic, leak, or vulnerability.

📚 Documentation

For an architectural deep-dive into Rullst Core's event loop and middleware lifecycle, please visit the Rullst Book.