Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CacheLayer is a PHP 8.3+ caching toolkit built around four deliberately separate
CacheLayer
├── Cache
│ ├── PSR-6 and PSR-16
│ ├── versioned tags
│ ├── generation-tagged records
│ ├── bounded stampede protection
│ └── tiering
├── Node Cache
Expand Down Expand Up @@ -71,9 +71,9 @@ $cache->setTagged('article.7', $article, ['articles', 'author.12'], 600);
$cache->invalidateTags(['articles', 'author.12']);
```

Each record embeds its complete tag-version snapshot. Tag versions begin at zero, invalidation increments them atomically, and reads fetch all required versions in a batch. A mismatch makes the complete record stale; there are no per-entry reverse tag indexes or partially tagged writes.
Each tagged record embeds its complete snapshot of opaque 128-bit tag generations. Invalidation replaces each generation, and reads fetch all required generations in a batch. A missing or mismatched generation makes the complete record stale, so lost metadata cannot resurrect an older record. There are no per-entry reverse tag indexes or partially tagged writes.

Zero and negative PSR-16 TTLs delete the key. Missing tag metadata means version zero.
Zero and negative PSR-16 TTLs delete the key. Namespaces are validated—not normalized—and must be 1–64 characters matching `[A-Za-z0-9_.-]+`.

## Native bulk paths

Expand All @@ -94,7 +94,7 @@ Bulk methods validate once and call the adapter’s native bulk contract. Deferr
| File / PHP files | optimized sequential filesystem access |
| Redis Cluster | fixed hash buckets and same-slot grouped operations |

Redis Cluster uses 128 stable bucket hash tags. Memcached and Redis Cluster clear a namespace by advancing epochs, so they do not scan, flush other namespaces, or maintain a permanent key membership index.
Redis Cluster uses 128 stable bucket hash tags. Memcached and Redis Cluster clear a namespace by replacing opaque namespace/bucket generations, so they do not scan, flush other namespaces, or maintain a permanent key membership index.

## Adapters

Expand All @@ -109,7 +109,7 @@ Cache::sqlite(); Cache::mongodb(); Cache::scylla();
Cache::tiered([...]);
```

Data and internal metadata use physically separate key spaces. SQL-like stores can install schema explicitly with `PdoCacheSchema::install()` and pass `initializeSchema: false` to `PdoCacheAdapter` in deployment-controlled environments.
Data and internal metadata use physically separate key spaces. Adapters are public for PSR-6 use, but tagging, stampede protection, policy-aware error handling, and metrics are facade responsibilities; use `Cache` for consistent CacheLayer semantics. SQL-like stores can install schema explicitly with `PdoCacheSchema::install()` and pass `initializeSchema: false` to `PdoCacheAdapter` in deployment-controlled environments.

`phpFiles` creates executable PHP files and is only appropriate for a trusted directory and trusted payloads. Never point SQLite at NFS, SMB, or another shared network filesystem.

Expand Down Expand Up @@ -169,21 +169,21 @@ Cluster Cache adds durable invalidation around independent Node Caches. It keeps
```php
$runtime->invalidateKey('product.42');
$runtime->invalidateTags(['products', 'catalog']);
$runtime->invalidateNamespace();
$runtime->clearNamespace();
$runtime->consume();
```

It does not replicate values and is not a distributed lock, session store, or counter system.
Failed local invalidation stops consumption without advancing the cursor; operators can repair the cause and retry. A poison event can only be skipped explicitly with `skipEventAfterClear()`, which clears the local namespace before advancing. Plain key invalidation cannot fence an in-flight resolver, so mutable read-through data that requires ordering should also use a tag generation. It does not replicate values and is not a distributed lock, session store, or counter system.

## Atomic counters and memoization

`AtomicCounters` uses an `AtomicCounterStoreInterface`; Redis/Valkey is the distributed implementation. Counters are never emulated with cache `get()` plus `set()`.

The `memoize()`, `remember(object: ...)`, and `once()` helpers plus `MemoizeTrait` provide bounded process-local memoization. They are independent of persistent backend caching.
The `memoize()`, `remember(object: ...)`, and `once()` helpers plus `MemoizeTrait` provide bounded process-local memoization. Their state survives requests in persistent workers until evicted or reset with `flush_memoizers()`; call that reset at request boundaries when cross-request reuse is not intended. They are independent of persistent backend caching.

## Metrics and benchmarks

Metrics distinguish calls from key volume: `get_batch`, `get_batch_keys`, hits/misses, set/delete batch counts, tag-version fetches, promotions, lock outcomes, and backend failures. `exportMetrics()` returns a snapshot and can invoke an export hook.
Metrics distinguish calls from key volume: `get_batch`, `get_batch_keys`, hits/misses, set/delete batch counts, tag-generation fetches, promotions, lock outcomes, and backend failures. `exportMetrics()` returns a snapshot and can invoke an export hook.

PHPBench scenarios in `benchmarks/` cover single operations, 10/100/1000-key bulk operations, tagged/plain records, tier and Node promotion, codec security/compression, and remember paths. Backend-focused tests separately verify operation counts for native bulk calls. These are microbenchmarks, not production throughput claims.

Expand Down
7 changes: 4 additions & 3 deletions benchmarks/CacheBulkBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public function benchTieredL1FullHit(array $params): int
for ($index = 0; $index < $params['size']; $index++) {
$key = 'tier-full.' . $index;
$keys[] = $key;
$l1->set($key, $index, 60);
$l1->save($l1->createItem($key)->set($index)->expiresAfter(60));
}

return count($cache->getMultiple($keys));
Expand All @@ -134,7 +134,7 @@ public function benchTieredL3Promotion(array $params): int
for ($index = 0; $index < $params['size']; $index++) {
$key = 'tier3.' . $index;
$keys[] = $key;
$l3->set($key, $index, 60);
$l3->save($l3->createItem($key)->set($index)->expiresAfter(60));
}

return count($cache->getMultiple($keys));
Expand All @@ -152,14 +152,15 @@ public function benchTieredPartialHit(array $params): int
$key = 'tier.' . $index;
$keys[] = $key;
$target = $index % 2 === 0 ? $l1 : $l2;
$target->set($key, $index, 60);
$target->save($target->createItem($key)->set($index)->expiresAfter(60));
}

return count($cache->getMultiple($keys));
}

public function provideSizes(): iterable
{
yield '1 key' => ['size' => 1];
yield '10 keys' => ['size' => 10];
yield '100 keys' => ['size' => 100];
yield '1000 keys' => ['size' => 1000];
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"scylladb",
"valkey",
"weakmap",
"chain-cache"
"tiered-cache"
],
"authors": [
{
Expand Down
7 changes: 6 additions & 1 deletion docs/adapters/file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Stores one cache payload per file under a namespace directory.
Path layout:

* base dir: provided ``$dir`` or ``sys_get_temp_dir() . '/cachelayer/files'``
* namespace dir: ``cache_<sanitized-namespace>``
* namespace dir: ``cache_<validated-namespace>``
* separate ``data`` and ``meta`` subdirectories
* file name: ``hash('xxh128', $key) . '.cache'``

Expand All @@ -20,8 +20,13 @@ Highlights:
* zero service dependencies
* persists across process restarts
* atomic write flow (``tempnam`` + ``rename``)
* restrictive directory validation and atomic metadata replacement
* immutable namespace and directory configuration

Expired files are removed lazily when encountered; applications with very
large, low-read keysets should periodically clear or rotate their cache
directory as an operational maintenance policy.

Best for local/single-host environments.

Example
Expand Down
11 changes: 6 additions & 5 deletions docs/adapters/memcached.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Highlights:
* distributed in-memory cache
* ``getMulti`` based batch reads
* TTL-grouped ``setMulti`` batch writes
* namespace clear advances an epoch and never calls server-wide ``flush``
* namespace clear replaces an opaque namespace generation and never calls server-wide ``flush``
* factory auto-configures ``MemcachedLockProvider`` for ``remember()`` when using this adapter
* lock leases use ``add`` acquisition and CAS-guarded renewal/release so an
expired owner's cleanup cannot delete a replacement owner's lock
Expand All @@ -36,7 +36,8 @@ Example
['127.0.0.1', 11211, 100],
]);

$state = $cache->remember('user.42.state', function ($item) {
$item->expiresAfter(120);
return loadSessionState(42);
});
$state = $cache->remember(
'user.42.state',
fn () => loadSessionState(42),
ttl: 120,
);
8 changes: 6 additions & 2 deletions docs/adapters/mongodb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ Requirements:
Highlights:

* namespace-scoped document storage
* base64-encoded payload persistence
* TTL-aware read-time pruning
* BSON binary payload persistence without Base64 expansion
* TTL-aware read-time pruning; production deployments should also install a
TTL index on the expiration field for background cleanup
* native ``$in`` reads and ``bulkWrite()`` mutations

Supported injected collection methods:

* ``findOne``
* ``find``
* ``updateOne``
* ``deleteOne``
* ``deleteMany``
* ``bulkWrite``
* ``countDocuments``

Example
Expand Down
21 changes: 14 additions & 7 deletions docs/adapters/pdo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Highlights:

* unified SQL adapter for MySQL, MariaDB, PostgreSQL, and other PDO drivers
* defaults to SQLite when no DSN/PDO is provided
* physically separated data and metadata rows (``<ns>:d:<key>`` and ``<ns>:m:<name>``)
* exact namespace isolation through composite ``(namespace, kind, cache_key)`` rows
* binary payload columns with physically separated data and metadata kinds
* automatic table/index initialization
* driver-aware upsert strategy:
- PostgreSQL/SQLite: native ``ON CONFLICT``
Expand All @@ -28,6 +29,8 @@ Highlights:
* PostgreSQL locking uses the two-key advisory-lock form
* SQLite and other PDO drivers without advisory locks use an injected
``FileLockProvider`` fallback
* expired data rows are misses and can be removed in bounded batches with
``PdoCacheAdapter::pruneExpired($limit)``

Schema creation can be separated from runtime access. Run
``PdoCacheSchema::install($pdo, 'cachelayer_entries')`` during deployment,
Expand All @@ -37,8 +40,10 @@ keeps automatic initialization enabled.

PDO advisory locks remain owned by their creating connection until explicit
release or connection loss. ``refresh()`` verifies local token ownership and
connection health. The provider rejects re-entrant acquisition of the same
lock through one provider instance.
connection health; it cannot extend a real server-side timed lease because PDO
advisory locks are connection-owned. Treat ``leaseSeconds`` as API
compatibility, not automatic expiry. The provider rejects re-entrant
acquisition of the same lock through one provider instance.

Examples:

Expand Down Expand Up @@ -72,10 +77,12 @@ Typical Usage

$cache = Cache::pdo('orders');

$summary = $cache->remember('orders.summary.today', function ($item) {
$item->expiresAfter(60);
return loadOrderSummary();
}, tags: ['orders']);
$summary = $cache->remember(
'orders.summary.today',
fn () => loadOrderSummary(),
ttl: 60,
tags: ['orders'],
);

// Invalidate all related records after an order mutation.
$cache->invalidateTag('orders');
6 changes: 5 additions & 1 deletion docs/adapters/php-files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ Persists cache records as PHP files that return payload arrays.
Path layout:

* base dir: provided ``$dir`` or ``sys_get_temp_dir() . '/cachelayer/phpfiles'``
* namespace dir: ``phpcache_<sanitized-namespace>`` with separate ``data`` and ``meta`` subdirectories
* namespace dir: ``phpcache_<validated-namespace>`` with separate ``data`` and ``meta`` subdirectories
* file name: ``hash('xxh128', $key) . '.php'``

Highlights:

* persistent local cache
* opcode-cache aware (``opcache_invalidate`` on writes/deletes when available)
* OPcache invalidation occurs before replacement or unlink
* immutable namespace and directory configuration

Expired files are removed lazily when encountered. Use bounded operational
directory rotation when entries may expire without being read again.

Good for environments where opcode cache integration is desired.
Use only in trusted environments, since cache entries are stored as executable
PHP files.
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/redis-cluster.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Requirements:
Highlights:

* 128 fixed hash-tag buckets for cross-slot-safe grouped operations
* namespace clear advances each bucket epoch
* namespace clear replaces each opaque bucket generation
* no permanent key index, stale membership, or cluster-wide scan

Useful when using Redis Cluster topology.
Expand Down
10 changes: 6 additions & 4 deletions docs/adapters/redis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ Example

$cache = Cache::redis('api', 'redis://127.0.0.1:6379/0');

$response = $cache->remember('endpoint:/v1/users?page=1', function ($item) {
$item->expiresAfter(30);
return fetchApiPayload();
}, tags: ['users']);
$response = $cache->remember(
'endpoint.v1.users.page.1',
fn () => fetchApiPayload(),
ttl: 30,
tags: ['users'],
);
4 changes: 3 additions & 1 deletion docs/adapters/scylladb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Highlights:
* keyspace/table-backed cache entries with bounded partition buckets
* bucket-grouped ``IN`` reads and bounded unlogged write batches
* schema bootstrap with ``CREATE TABLE IF NOT EXISTS``
* TTL stored as absolute timestamp in ``expires``
* native Scylla TTL on data rows, plus the absolute expiration timestamp used
for read-time validation
* binary ``blob`` payload storage without Base64 expansion

Supported injected session methods:

Expand Down
2 changes: 2 additions & 0 deletions docs/adapters/shared-memory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Highlights:

* values shared across PHP processes on the same host
* namespace-specific segment key strategy
* shared locks for reads and exclusive locks for mutation
* an owner marker that rejects accidental ``ftok`` segment collisions
* good for host-local IPC cache use cases

Notes:
Expand Down
10 changes: 6 additions & 4 deletions docs/adapters/valkey.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ Example

$cache = Cache::valkey('api', 'valkey://127.0.0.1:6379/0');

$payload = $cache->remember('endpoint:/v1/users?page=1', function ($item) {
$item->expiresAfter(30);
return fetchApiPayload();
}, tags: ['users']);
$payload = $cache->remember(
'endpoint.v1.users.page.1',
fn () => fetchApiPayload(),
ttl: 30,
tags: ['users'],
);
21 changes: 13 additions & 8 deletions docs/cache.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Cache facade
============

``Infocyph\CacheLayer\Cache\Cache`` implements PSR-6, PSR-16, and
``ArrayAccess``. It adds versioned tags, native bulk operations, bounded
``ArrayAccess``. It adds generation-tagged records, native bulk operations, bounded
stampede protection, tiering, metrics, and per-instance payload policy.

Factories
Expand All @@ -21,14 +21,15 @@ Configuration is fixed when the cache is constructed.
Keys, tags, and TTL
-------------------

Keys and tags are 1--64 characters from ``A-Z``, ``a-z``, ``0-9``, ``_``,
``.``, and ``-``. Bulk input is completely validated before mutation. Zero or
negative TTL deletes the entry.
Keys, tags, and namespaces are 1--64 characters from ``A-Z``, ``a-z``, ``0-9``,
``_``, ``.``, and ``-``. Namespaces are validated without normalization, so
distinct inputs can never collapse into one cache. Bulk input is completely
validated before mutation. Zero or negative TTL deletes the entry.

Tags are stored as a version snapshot inside each record. Missing tag metadata
means version zero. Invalidation atomically increments tag versions; a read
fetches all required versions in one batch and rejects the whole record on any
mismatch.
Tags are stored as opaque 128-bit generation snapshots inside each record.
Invalidation replaces generations; a read fetches all required generations in
one batch and rejects the whole record on a missing or mismatched generation.
Consequently, evicted metadata cannot make an old tagged record valid again.

Bulk behavior
-------------
Expand Down Expand Up @@ -86,6 +87,10 @@ Construction and configuration failures throw. With the default
return ``false`` while incrementing ``backend_failure``. Set ``failOpen=false``
to propagate the backend exception.

Adapters can be used directly as PSR-6 pools, but CacheLayer's tagging,
stampede protection, metrics, and fail-open policy live in the ``Cache``
facade. Prefer the facade unless the narrower adapter behavior is intentional.

Tiering
-------

Expand Down
Loading