Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine
Release notes
Changelog
Elyra
Storage backends: L1 shared memory + L2 SQL Anywhere

Storage backends: L1 shared memory + L2 SQL Anywhere

Askr already replaces Redis for a single box: an in-process, shared-memory cache (crates/askr/src/cache.rs), job queue (crates/askr/src/squeue.rs), response cache (rcache.rs) and SSE/Pusher broadcasting — no external broker.

This document describes how those primitives gain a durable, replicated, multi-box tier by layering over SQL Anywhere, so the "Redis-free" story holds across a fleet and survives restarts. It is the runtime half of epic elyra-2; the substrate half is the SQL Anywhere contracts, which are now conformance-tested (executable specs, not just prose — see sql-anywhere/sqlanywhere/tests/contract_conformance.rs), so these drivers build against a proven substrate:

  • Queue → sql-anywhere/docs/contracts/QUEUE_CONTRACT.md (elyra-5) ✓ conformance-tested
  • Cache → sql-anywhere/docs/contracts/CACHE_CONTRACT.md (elyra-7) ✓ conformance-tested
  • Pub/sub → sql-anywhere/docs/contracts/PUBSUB_CONTRACT.md (elyra-6) ✓ conformance-tested

The two tiers

Tier Where Property Use
L1 anonymous shared mmap, this process tree fastest, ephemeral, single box hot cache, counters/locks, in-box queue, SSE fan-out
L2 SQL Anywhere table (embedded or sqld) durable, transactional, replicated, multi-box durable jobs, shared/edge cache, cross-node pub/sub

The API PHP sees (askr_cache_*, askr_queue_*, askr_broadcast()) and the Laravel drivers (elyra-11, elyra-12) are unchanged; only the backend differs. L1 and L2 mirror the same semantics on purpose — that is why the contracts are written to match squeue.rs / cache.rs.

Queue (elyra-9) — implemented

The durable-queue driver (crates/askr/src/squeue_sql.rs, feature sql-backend) claims against askr_jobs exactly as squeue.rs::pop reserves a slot: UPDATE … RETURNING with a subselect, a visibility timeout, attempts incremented at claim, ack = delete, release = re-arm with backoff. It implements the conformance-tested QUEUE_CONTRACT.md SQL verbatim and exposes the same push/pop/delete/release/size bridge as L1, so askr_queue_* and the Laravel driver are unchanged — only the backend differs.

Enable it (build + run):

cargo build --release -p askr --features sql-backend
ASKR_QUEUE_DB=/var/lib/askr/queue.db ./askr serve ...   # unset => L1 fallback

ASKR_QUEUE_DB points at an embedded SQL Anywhere file, an embedded replica, or a sqld-managed database. Each worker process opens its own WAL connection (safe multi-process access via SQLite file locking), so the pre-fork model needs no shared state; queue.rs dispatches L1 vs L2 once, at bridge registration.

Still open (follow-ups): connection pooling knobs, visibility renewal/heartbeat for long jobs, dead-letter move wired into the worker loop (attempts >= max_attempts), and the Laravel QUEUE_CONNECTION=sqlanywhere surface (elyra-12).

Autoscaling (elyra-8) — implemented

The backlog-driven autoscaler (--queue--queue-max) now reads its backlog via queue::stats(), which dispatches to the L2 contract's FILTER backlog query (squeue_sql::stats()) when ASKR_QUEUE_DB is set, or the shared-memory ring otherwise. So the same balance=auto worker scaling and the askr_queue_ready/total/oldest_seconds metrics work against the durable L2 queue with no code change at the call sites — the master process reads the backlog from the database and forks/drains the worker pool exactly as before.

Cache (elyra-10) — implemented

The durable cache driver (crates/askr/src/cache_sql.rs, feature sql-backend) implements the conformance-tested CACHE_CONTRACT.md verbatim and exposes the same get/set/add/delete/increment/touch/flush/forget_tag bridge as L1, so askr_cache_*, the Laravel cache store and Cache::lock() are unchanged — only the backend differs. Locks use the contract's atomic add (SETNX with expired-lock steal); counters use the atomic increment (a counter stored as INTEGER still reads back as bytes, so Cache::get after increment behaves as PHP expects); tag invalidation removes every key carrying the tag.

Enable it with ASKR_CACHE_DB=/path/to.db (unset => L1 fallback), building with --features sql-backend. Each worker process opens its own WAL connection.

Write-through L1→L2 (implemented): when the L1 shared-memory cache is also enabled (--cache-slots N), it becomes a fast local read tier in front of the durable L2. Reads hit L1 first and lazily populate it (with the remaining TTL) on a miss; writes go to L2 (the source of truth) and warm or invalidate L1. Because L1 is shared memory, every worker process on a box sees writes immediately — coherent within a box. Cross-box staleness is bounded by the entry TTL; instant cross-node tag invalidation over the pub/sub topic is a follow-up.

Broadcasting / pub-sub (elyra-13) — implemented

The durable pub/sub driver (crates/askr/src/broadcast_sql.rs, feature sql-backend) implements PUBSUB_CONTRACT.md: publish = INSERT into the append-only askr_events topic, subscribe = tail rows past a cursor. It exposes the same publish/current_seq/read_from surface and askr_broadcast() bridge as the L1 ring, so the SSE fan-out task and the Pusher endpoint are unchanged — only the backend differs. Askr tails its local copy of askr_events and fans out to connected SSE / Pusher-compatible WebSocket clients on that node, so a publish on the primary reaches Echo clients on any node via the replication log — no Redis pub/sub.

Enable it with ASKR_BROADCAST_DB=/path/to.db (unset => L1 ring), building with --features sql-backend.

The tail query is prepare_cached (compiled once per connection) and returns a cheap indexed 0-row scan when nothing is new, so the 50 ms poll is inexpensive. Note a true SQLite update_hook wakeup only fires for writes on the same connection, so it cannot wake the reader on a cross-process publish — polling the local copy (woken naturally by replication apply on a replica) is the correct transport. Durable subscriber cursors (askr_subscribers) for resume-after-restart remain a follow-up.

Status

Contracts (elyra-5/6/7) are stable and conformance-tested on the substrate side (sql-anywhere/sqlanywhere/tests/contract_conformance.rs). All three L2 drivers are implemented behind the sql-backend feature — queue (elyra-9, squeue_sql.rs), cache (elyra-10, cache_sql.rs) and broadcast (elyra-13, broadcast_sql.rs) — plus backlog-driven autoscaling on L2 (elyra-8, via queue::stats). The Laravel surface (elyra-11/12/13) ships in packages/laravel: session, cache, queue and now broadcasting (BROADCAST_CONNECTION=askr) drivers, transparent across L1/L2. Epic elyra-2 is functionally complete; remaining work is optimization (update-hook wakeups, write-through L1→L2 cache) and hardening.