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 Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Shared cache

Shared cache

Askr ships a small shared-memory cache exposed to PHP. It's a fixed-slot hash table living in an anonymous shared mmap the master creates before forking, so every worker process sees the same physical table — no IPC, no locks on the hot path beyond a per-slot spinlock.

This gives you cache, atomic counters (rate limiting) and locks in the Askr binary — no Redis for small/mid deployments.

Enabling it

askr serve --root ./public --worker-script examples/laravel-worker.php \
  --cache-slots 16384

or in askr.toml:

[cache]
slots = 16384   # each slot is ~4.3 KB; 16384 ≈ 70 MB

The cache is off when slots = 0 (the default). Sizing: pick roughly expected_entries × 1.3. Each slot holds one entry inline; keys are capped at 250 bytes and values at ~4 KB (larger values simply aren't cached).

PHP API

When the cache is enabled, these functions are available to any PHP running under Askr (web workers, the worker loop, and queue/scheduler sidecars):

Function
askr_cache_get(string $key): ?string Value, or null on miss/expiry.
askr_cache_set(string $key, string $value, int $ttl = 0): bool ttl seconds (0 = forever). false if too large.
askr_cache_delete(string $key): bool true if it existed.
askr_cache_increment(string $key, int $delta = 1, int $ttl = 0): int Atomic add; returns the new value.
askr_cache_touch(string $key, int $ttl = 0): bool Atomically refresh a key's TTL without rewriting its value (false if absent).
askr_cache_flush(): void Empty the table.
askr_cache_set('greeting', 'hello', 60);
echo askr_cache_get('greeting');              // hello  (from any worker)
$n = askr_cache_increment('rate:'.$ip, 1, 60); // atomic across all workers

The counter is atomic across every worker process — ideal for rate limiting.

Laravel cache driver

examples/AskrCacheStore.php is a Laravel Store backed by these functions. Register it in your worker script or a service provider:

use Illuminate\Support\Facades\Cache;

require '/opt/askr/examples/AskrCacheStore.php';

Cache::extend('askr', fn ($app) =>
    Cache::repository(new AskrCacheStore(config('cache.prefix', ''))));

Add a store in config/cache.php (or set CACHE_STORE=askr):

'askr' => ['driver' => 'askr'],

Then use the cache normally — it's shared across all workers:

Cache::put('user:1', $user, 300);
$user = Cache::get('user:1');
Cache::increment('hits');                       // atomic
Cache::remember('report', 600, fn () => build()); // computed once, shared

Integers/floats are stored unserialized so increment()/decrement() (used by Laravel's rate limiter) are truly atomic in shared memory; other values are serialized.

Large values, sessions & locks (0.6.0)

Two size classes: a small region (--cache-slots, 4 KB values — counters, locks, small entries) and an optional large region (--cache-large-slots / [cache] large_slots, 64 KB values — sessions, cached fragments, serialized collections). set routes by size and clears the key from the other region; get/delete check both. This keeps big values working without wasting 64 KB per counter.

With the large region enabled, the AskrCacheStore covers what usually needs Redis on a single box:

// sessions
// .env: SESSION_DRIVER=cache  SESSION_STORE=askr

// atomic locks (across all worker processes, via askr_cache_add)
Cache::lock('deploy', 10)->get(fn () => run_migration());

askr_cache_add($key, $value, $ttl) is an atomic set-if-absent — the primitive behind Cache::add() and Cache::lock(). The store implements Laravel's LockProvider, so Cache::lock() is truly atomic in shared memory.

Redis use Askr replacement
cache askr_cache_* + AskrCacheStore (small + large regions)
rate limiting / counters askr_cache_increment (atomic)
atomic locks askr_cache_add + Cache::lock()
sessions large region + SESSION_STORE=askr
pub/sub broadcast ring + Pusher WS (see BROADCAST)
queues askr_queue_* + AskrQueue driver (shared-memory, delayed + retries)

Automatic page caching (Laravel)

The kwhorne/askr-laravel package can derive the tags for you, so there's no list to keep correct:

Route::get('/products/{product}', ProductController::class)
    ->middleware('askr.cache:300');

Every Eloquent model the response read becomes a tag, so $product->save() clears exactly the pages that showed it. Responses that read many models degrade to class-level tags (products instead of products:42), and a response with more dependencies than an entry can hold is not cached at all — an entry that can't be invalidated is worse than a cache miss.

See the package README for the full behaviour, and askr cache-report for deciding which routes deserve it.

Semantics & limits

  • Eviction: the probe window (16 slots) evicts an expired entry, else the oldest-written one; askr_cache_evictions_total counts evictions. Sizing the table generously avoids eviction under load.
  • Oversize values: a value larger than the largest slot (64 KB) isn't cached — set returns false and askr_cache_oversize_total increments (also logged at debug), so dropped large sessions/fragments are visible rather than silent.
  • Deletion is tombstoned: a deleted/expired slot becomes a tombstone (not an empty hole), so a colliding key stored later in the same probe chain stays reachable; inserts reuse tombstones.
  • TTL is lazy (checked on read) — expired entries free their slot on the next access to it.
  • Best-effort under crashes: a per-slot spinlock is stolen if a holder dies mid-operation, so a crashed worker can't deadlock the table; a torn write can only yield a stale/garbage value (never memory unsafety — reads are length clamped), which a cache tolerates.
  • Not persistent: the table lives in RAM and is empty on restart.

For very large values, many GB of data, or cross-host sharing, use a real cache (Redis/Memcached) — Askr's cache targets the common single-host case.