Cluster Cache: Overview and Transport#

Conceptual model#

Each node owns a Node Cache and a durable cursor. All nodes in a logical cluster share one replayable invalidation transport:

                    shared durable event transport
              +------------------------------------+
              | key / tag / namespace events        |
              +------------------------------------+
                      ^                    |
                      | publish            | replay
                      |                    v
node A  Node Cache + runtime          node B  Node Cache + runtime
APCu + local SQLite                  APCu + local SQLite
cursor A                             cursor B

When node A invalidates a value, it removes the local value and publishes an event. Node B’s consumer replays that event and removes its own local value. The next read on either node obtains a fresh value from the authoritative source and caches it locally.

What Cluster Cache is and is not#

Cluster Cache is:

  • a durable, replayable invalidation protocol on top of Node Cache;

  • suitable for multiple application servers with independent local storage;

  • able to distribute key, tag, and namespace invalidations;

  • tolerant of an offline node when the transport retains events long enough.

Cluster Cache is not:

  • a value-replication system or shared cache;

  • a replacement for transactions, a database, sessions, or distributed locks;

  • a guarantee that all nodes observe a change at the same instant;

  • appropriate for payment idempotency, global counters, authorization state that needs immediate revocation, or other authoritative/security-critical state.

Cluster identity, nodes, and namespaces#

ClusterCacheConfig has four settings:

use Infocyph\CacheLayer\Cluster\ClusterCacheConfig;

$clusterConfig = new ClusterCacheConfig(
    cluster: 'production',
    nodeId: 'catalog-web-01',
    consumerBatchSize: 1_000,
    invalidateLocallyFirst: true,
);
cluster

A non-empty logical stream name. production, staging, and tenant-a can use the same transport without consuming each other’s events. There is no fixed CacheLayer limit on cluster names; transport capacity and retention determine the practical limit.

nodeId

A non-empty identity for this running node. It must be unique among nodes in the same cluster. The producer’s own events are not applied a second time, but their cursor still advances. A stable hostname is suitable for long-lived hosts; use a unique instance/pod identity for ephemeral infrastructure.

consumerBatchSize

Maximum events fetched by consume() when no explicit limit is supplied. It must be greater than zero. Start with 1,000 and tune from observed event volume and consumer runtime.

invalidateLocallyFirst

Defaults to true. With the default, the initiating node clears its local cache first and then publishes the event. With false, it publishes first and clears locally afterward. This selects failure order; it does not make a two-system operation atomic. See Failure ordering and transactional writes.

The Node Cache namespace is distinct from the cluster name. A namespace is stored in every event and remote nodes only apply events for their matching namespace. This allows independent cache domains to share a cluster transport, although all events in that cluster are still read to advance each node’s cursor.

Prerequisites and transport choice#

Every node needs:

  • a local writable SQLite file and optional APCu, as described in Node Cache;

  • the same cluster name and a unique node ID;

  • access to the same durable, replayable event transport;

  • a consumer process or scheduled task that calls consume();

  • retention longer than the largest expected node outage plus an operational safety margin.

InvalidationTransportInterface is the boundary for the shared event store. A production transport must assign sortable IDs, replay events after a cursor, expose its oldest retained ID, and compare its cursor format for recovery. Suitable technologies include PostgreSQL/MySQL tables, Redis Streams, NATS JetStream, RabbitMQ durable queues designed for replay, and Kafka.

Plain publish/subscribe is not sufficient: an offline node would lose the invalidation and retain a stale local entry until its TTL expires.

PDO transport#

PdoInvalidationTransport is included for a supplied PostgreSQL or MySQL PDO connection. It creates a shared cachelayer_invalidation_events table and index when constructed. The connection account therefore needs the required DDL and read/write rights when the transport is initialized.

use Infocyph\CacheLayer\Cluster\Transport\Pdo\PdoInvalidationTransport;

$pdo = new PDO(
    'pgsql:host=db.internal;port=5432;dbname=application',
    'application',
    $password,
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
);

$transport = new PdoInvalidationTransport($pdo);

SQLite support in this class is useful for a local test only. Do not use a SQLite database as a shared multi-node event transport in production. It is rejected by default and requires an explicit test-only opt-in:

$transport = new PdoInvalidationTransport($sqliteConnection, allowSqliteForTesting: true);

Redis and Valkey Streams transport#

RedisStreamInvalidationTransport is included for lower-latency, higher-volume invalidation workloads. It appends events to one durable stream per cluster and each node replays from its own persisted stream cursor; it does not use a consumer group because every node must receive every invalidation.

use Infocyph\CacheLayer\Cluster\Transport\RedisStreamInvalidationTransport;

$transport = new RedisStreamInvalidationTransport(
    $redis,
    prefix: 'application:cache-invalidation:',
    maxLength: 100_000,
);

maxLength bounds retained stream entries using Redis’s approximate trimming. Set it large enough to cover the greatest expected outage and replay backlog; a node that falls behind the retained stream is safely recovered by clearing its local namespace. Redis/Valkey persistence and replication settings are an application operational requirement—the stream must be durable enough for the invalidation guarantee you need.