Search by

sanchescom / laravel-cache-memory

sanchescom

Laravel cache driver backed by System V shared memory (shmop) with semaphore-guarded atomic operations.

Package info

github.com/sanchescom/laravel-cache-memory

pkg:composer/sanchescom/laravel-cache-memory

Statistics

Installs: 1 343

Dependents: 0

Suggesters: 0

Stars: 7

Open Issues: 0

2.0.0 2026-09-08 04:42 UTC

This package is auto-updated.

Last update: 2026-09-08 13:48:21 UTC


README

CI Latest Version Downloads PHP Version License

A Laravel cache driver backed by System V shared memory (shmop). Data survives across PHP-FPM/CLI worker requests on the same host without standing up Redis or Memcached — useful for small, hot, single-box caches (rate-limit counters, feature-flag snapshots, warm lookup tables) where an external cache service would be overkill.

Table of Contents

Requirements

  • PHP 8.2+
  • ext-shmop
  • ext-sysvsem
  • Laravel 11 or 12 (illuminate/cache and illuminate/support ^11.0|^12.0)

Note

Laravel 11 reached end of life in 2026 (no more security fixes). The package still supports and tests it, but new projects should target Laravel 12.

Installation

composer require sanchescom/laravel-cache-memory

The service provider (Sanchescom\Cache\MemoryServiceProvider) is registered automatically via package auto-discovery — no manual registration needed.

Quick Start

Add a memory store to config/cache.php:

// config/cache.php
'stores' => [
    // ...
    'memory' => [
        'driver' => 'memory',
        'key' => env('MEMORY_BLOCK_KEY', 771055301), // pick your OWN value — see below
        'size' => env('MEMORY_BLOCK_SIZE', 320000),  // bytes; omit to use the 320000-byte default
    ],
],

Change that key. System V IPC keys are a single host-wide namespace shared by every process on the machine, so two applications configured with the same key attach to the same segment and silently read and overwrite each other's cache entries — pick a distinctive value per application (and per environment, if several run on one host) rather than copying the one above.

Then use it like any other Laravel cache store:

use Illuminate\Support\Facades\Cache;

// One process writes...
Cache::store('memory')->put('some_key', ['value' => 'text'], 60); // 60-second TTL

// ...another process on the same host reads it back.
$data = Cache::store('memory')->get('some_key');

// Counters are exact under concurrent access (see Atomicity below).
Cache::store('memory')->increment('hits');

Configuration

Each configured store gets its own isolated shared memory segment and its own semaphore — two stores never share state, even if both use the memory driver.

Key Type Default Notes
driver string Must be memory.
key int|null derived via ftok() SysV IPC key. Cast to int by the provider. See the [!WARNING] below about leaving this unset in production.
size int|null 320000 (MemoryBlock::DEFAULT_SIZE) Total segment size in bytes, including a 4-byte internal header. Cast to int by the provider.

If key is omitted, MemoryBlock derives one from ftok(__FILE__, 'b') — i.e. from the installed package file's device and inode, not from anything you configure. If size is omitted, the segment is sized at MemoryBlock::DEFAULT_SIZE (320,000 bytes).

How It Works

  • Shared memory segment. Each store owns one shmop segment sized by size (or the 320,000-byte default). All keys for that store — the entire cache — live serialized inside this one fixed-size segment.
  • Length-prefixed framing. A write packs a 4-byte big-endian length header (pack('N', strlen($payload))) followed by the serialized payload. Reads trust that header rather than scanning for a terminator, which is what makes a read of a segment mid-write detectable instead of silently truncated.
  • One semaphore per store, paired to the segment's key. The store's constructor calls sem_get($key, 1, 0644, false) against the same key used for the shared memory segment — the same permissions as the segment itself, so the lock is no easier to grab than the data it guards. Every public operation on the store — get() included — acquires this semaphore before touching the segment and releases it afterward.
  • Garbage collection on overflow. When a write no longer fits, expired entries are purged first and the write is retried. If it still doesn't fit, the whole cache for that store is written back empty into the same segment and an E_USER_WARNING is raised, in that order (see Limitations — this is destructive and intentional, not a bug). The segment itself is never deleted on this path; only requestDeletion() does that.

Atomicity

Every store operation — reads included — runs inside the semaphore-guarded critical section described above, which is what makes increment()/decrement() exact under concurrent access instead of merely "usually correct." This is proven by a Pest test that forks 16 child processes, each incrementing the same counter 50 times, across 6 independent rounds, and asserts the final count equals exactly 16 × 50 every round. It's a real net, not a rubber stamp: run against a deliberately reverted version of the locking fix, it caught the regression in 40 out of 40 runs.

Note

That test has been verified repeatedly on macOS during development. CI (.github/workflows/ci.yml) is this package's first data point on Linux — nothing here has been separately verified on Linux outside CI.

Limitations

Warning

Fixed segment size. The segment never grows. When a write would exceed the configured size, expired entries are garbage-collected first. If the write still doesn't fit, the entire store is written back empty and only then is an E_USER_WARNING raised. The order matters, because that warning does not behave the same everywhere:

  • Inside a booted Laravel application, HandleExceptions promotes E_USER_WARNING to a thrown ErrorException, so the overflowing call does not returnCache::put() throws, and your exception handler (and your logs) see it.
  • Outside Laravel — plain PHP, or any handler that swallows the warning — the same call returns normally, and put() returns true.

The state left behind is identical in both worlds: the store is empty and immediately usable again. The value you just stored, and everything else in that store, is gone. Size the store for your real working set, and treat this warning as a sizing bug in your configuration, not as routine cache pressure.

Warning

requestDeletion() disconnects other live processes from the cache. Deleting and recreating a segment produces a new segment under the same key. Any other process that already holds an open handle keeps reading and writing the old, orphaned one for the rest of its life: it only reopens when the key has no segment at all, and it has one again immediately. Both sides still share the same semaphore, so their accesses stay perfectly serialised while their data diverges — which looks exactly like a locking bug and is not one. Call requestDeletion() only when nothing else is using the store, and restart your workers afterwards. (This is why the overflow path above empties the segment instead of recreating it.)

Warning

Cache::add() is not atomic on this driver. MemoryStore implements Illuminate\Contracts\Cache\Store and has no add() method of its own, so Repository::add() falls back to a non-atomic get() + put() — two processes can both observe the key as missing and both write. increment()/decrement() are atomic (see Atomicity); add() is not, and neither is Cache::lock(), which this driver does not support at all. Both are 2.1 candidates.

Warning

Data is lost on reboot, or whenever the segment is deleted. Shared memory is not persistent storage — it does not survive a host restart, and both requestDeletion() and the overflow path above discard everything the store held.

Warning

POSIX only — no Windows support. ext-shmop and ext-sysvsem are System V IPC facilities; there is no Windows shmop/sysvsem implementation to fall back to.

Warning

The default key is not stable across symlinked deploy paths. With key left unset, the segment's SysV key comes from ftok(__FILE__, 'b') — derived from the installed package file's device and inode. A symlink-swap deploy layout (e.g. Capistrano-style releases/<n> + current symlink) copies fresh files into a new release directory on every deploy, giving that file a new inode and therefore a new default key each time you deploy — silently disconnecting old worker processes from new ones. Set an explicit key in production.

Warning

Segment and semaphore permissions are 0644 — same-user processes only. Both the shared memory segment and its paired semaphore are created owner-read-write, group/other-read-only. Only processes running as the same user that created them can write to the segment or acquire the lock; processes running as a different user can, at best, read the segment. There is currently no way to configure this.

Additionally: the paired semaphore is not removed by requestDeletion() or MemoryBlock::delete() — only the shared memory segment is. This is deliberate (see UPGRADE.md), but it means a long-lived host that cycles through many distinct keys accumulates one leftover semaphore per key. Reuse a stable, small set of keys.

Testing Your Application

For most applications, the right thing to do in tests is not to use the memory driver at all — set CACHE_STORE=array (or configure the array store directly) so tests don't touch real shared memory or leak SysV resources between runs.

If you specifically need to exercise the memory driver:

  • Give each test (or test process) its own unique key, so parallel test runs never collide on the same segment.
  • Call Cache::store('memory')->getStore()->requestDeletion() in teardown to drop the segment. Remember this leaves the semaphore in place (see Limitations) — if your test suite forks processes or runs many times against a fixed key, clean the semaphore up separately (sem_get($key) + sem_remove($semaphore)).
  • Also call Cache::purge('memory') after requestDeletion() so the resolved repository (and the Shmop handle it holds) is dropped too — deleting the segment does not detach your process from it, and a kept handle burns a SysV attachment slot per test until you hit "Unable to open shared memory segment". This package's own feature suite does exactly this in its afterEach().

Roadmap

See ROADMAP.md for planned features (an atomic add(), a Cache::lock() provider on SysV semaphores, segment usage stats, configurable permissions, and an APCu fallback driver).

Upgrading

See UPGRADE.md for breaking-change notes, including the 1.x → 2.0 migration.

Contributing

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

Versioning

We use SemVer for versioning. 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 licensed under the MIT License - see the LICENSE.md file for details.