Redis · Configuration

Configuration

A config describes one endpoint: where to go, what to authenticate with, which database to select. The pool builds every one of its connections from it, so anything that must be identical on every connection lives here.

The config class

Extend RedisConfig and fill it in inside setUp():

main/Configurations/MainRedisConfig.php
<?php

namespace Main\Configurations;

use Flytachi\Winter\Redis\Config\RedisConfig;

class MainRedisConfig extends RedisConfig
{
  public function setUp(): void
  {
      $this->host     = env('REDIS_HOST', 'localhost');
      $this->port     = (int) env('REDIS_PORT', 6379);
      $this->password = env('REDIS_PASS', '');
  }
}

setUp() runs once per instance created — that is, once per connection in the pool. Reading the environment here is fine; talking to a database or the network is not.

What can be set

Property Default What it is
$host 'localhost' server address; a scheme is allowed (tls://, unix://)
$port 6379 port
$username '' ACL user (Redis 6+); empty means the default user
$password '' password; an empty string means no AUTH is sent
$databaseIndex 0 database index (SELECT)
$timeout 1.5 seconds to open the socket
$readTimeout 2.0 seconds to wait for a reply once connected
$serializer Redis::SERIALIZER_NONE how values are encoded
$context [] stream context: CA, client certificate, peer verification

Two timeouts, and they mean different things

They get confused more than anything else here, and their consequences differ.

$timeout is how long to wait for the connection to be established. Exceeded, the connection never opened, and the pool raises a PoolException that surfaces as RedisPoolException. This one is about an unreachable server.

$readTimeout is how long to wait for a reply to a command already sent. Exceeded, you get RedisException: read error on connection, which reads as a dropped connection rather than a timeout. This one is about a server that took the command and went quiet.

php
$this->timeout     = 1.5;    // cannot reach it — fail fast
$this->readTimeout = 2.0;    // no reply for two seconds — call the link lost

readTimeout and blocking commands

BLPOP, BRPOP and SUBSCRIBE make the server go quiet on purpose — until data arrives. To the connection that is indistinguishable from a hung server, so consume(timeout: 5) on a connection with readTimeout = 2.0 dies on the second second with a read error. Measured.

The list handle accounts for that and raises the read timeout for the wait itself — but if you write a blocking command through raw(), raise it yourself: $redis->setOption(Redis::OPT_READ_TIMEOUT, -1) removes the limit (it must be -1; 0 means “do not wait at all”).

Both values apply to one connection, not to a request: the pool may spend longer waiting for a free connection, and that is the separate $poolWaitTimeout.

Authentication

A password is enough for an ordinary installation — it authenticates the default user:

php
$this->password = env('REDIS_PASS', '');

Redis 6 added ACLs: separate users with their own permissions and their own reachable keys. Managed Redis offerings often hand out such a user rather than the default password:

php
$this->username = env('REDIS_USER', 'app');
$this->password = env('REDIS_PASS');

With $username empty, AUTH <password> is sent; with it set, AUTH <user> <password>. To check who you connected as:

php
$config->connection()->rawCommand('ACL', 'WHOAMI');   // 'app'

The password never reaches the package's logs

getDsn() is deliberately built from host, port and database — neither password nor user appears in it, so the string is safe to log whole. That is what the pool logs.

TLS and unix sockets

The scheme goes into the address itself; the driver parses it:

php
$this->host = 'tls://redis.example.com';
$this->port = 6380;

TLS material — a trusted CA, a client certificate, peer verification requirements — travels in $context:

php
$this->host    = 'tls://redis.example.com';
$this->port    = 6380;
$this->context = [
  'stream' => [
      'cafile'      => '/etc/ssl/certs/redis-ca.pem',
      'verify_peer' => true,
  ],
];

For a local socket:

php
$this->host = '/var/run/redis/redis.sock';
$this->port = 0;                                  // the port is unused

TLS failures arrive as warnings, not exceptions

The driver reports a failed handshake with a PHP warning (Failed to enable crypto), and the connection may still be considered established. Verify the setup with an explicit ping() on the first deploy, not by the absence of exceptions.

Several configs

A config is not “the Redis settings of the application” but one endpoint. Having several is normal:

php
class MainRedisConfig extends RedisConfig    { /* database 0 — sessions, cache */ }
class QueueRedisConfig extends RedisConfig   { /* database 1 — queues            */ }
class MetricsRedisConfig extends RedisConfig { /* a different server entirely    */ }

Each gets its own pool with its own ceiling, and they appear separately in RedisPool::stats(). Stores attach to the one they need through $redisConfigClassName.

Splitting pays off when the parts have different needs: a queue with blocking readers should not compete for connections with a hot cache, and metrics should not take the application down when their server is unreachable.

The database index

It is set in the config and only there:

php
class QueueRedisConfig extends RedisConfig
{
  public function setUp(): void
  {
      $this->host          = env('REDIS_HOST', 'localhost');
      $this->databaseIndex = 3;
  }
}

A second database means a second config class. It gets its own pool with its own connections, so the two cannot be confused by construction.

Never switch databases at runtime

SELECT changes the state of the connection, and the connection goes back to the pool. The next request would receive it along with someone else’s database and write to the wrong place — silently, without a single error. That is why nothing resembling select() exists in the store API: changing database means changing config.

The selected database also survives a dropped connection: ext-redis reconnects transparently and restores AUTH and SELECT itself, so a connection never wakes up on database zero.

Value serialization

The default is SERIALIZER_NONE: exactly the bytes you pass go to the server. That is the only form redis-cli and applications in other languages can read.

The price is that a value must be a string or a number:

php
$store->set('user', ['id' => 1]);   // → the string "Array" in the database, with only a PHP warning
$store->get('user');                 // 'Array'

Nothing throws — which is why the serializer is a property of the config, where the choice is made once and in plain sight:

php
use Redis;

class CacheRedisConfig extends RedisConfig
{
  public function setUp(): void
  {
      $this->host       = env('REDIS_HOST', 'localhost');
      $this->serializer = Redis::SERIALIZER_PHP;    // arrays and objects as they are
  }
}
Value What it gives What it costs
SERIALIZER_NONE bytes as they are, readable by everything strings and numbers only
SERIALIZER_PHP any PHP value, objects included only PHP can read the format
SERIALIZER_JSON any array, readable by other languages objects come back as stdClass

Change the serializer before the database holds data

It applies to the connection, not to the key, so values written earlier stop being readable afterwards: the old bytes are decoded the new way. On a populated database this is a migration, not a setting.

Pool size

By default a config gets 10 connections and waits 3 seconds for a free one. To choose your own, let the config declare itself pool-aware:

main/Configurations/MainRedisConfig.php
<?php

namespace Main\Configurations;

use Flytachi\Winter\Redis\Config\RedisConfig;
use Flytachi\Winter\Redis\Pool\RedisPoolConfigInterface;
use Flytachi\Winter\Redis\Pool\RedisPoolTrait;

class MainRedisConfig extends RedisConfig implements RedisPoolConfigInterface
{
  use RedisPoolTrait;

  public int   $poolMaxConnections = 20;
  public float $poolWaitTimeout    = 5.0;

  public function setUp(): void
  {
      $this->host = env('REDIS_HOST', 'localhost');
  }
}

The trait supplies every method of the interface with a default, so you declare only what you change. Each setting is discussed in the Connection pool.

A one-off connection

For a script, a migration or a test there is no reason to declare a class — take the inline variant:

scripts/warmup.php
<?php
require 'vendor/autoload.php';

use Flytachi\Winter\Redis\Config\Call\RedisCall;

$redis = (new RedisCall(
  host:          '127.0.0.1',
  port:          6379,
  databaseIndex: 3,
))->connection();

$redis->set('warm', '1');

Each such object opens its own connection and closes it when it is garbage-collected. In an application serving requests that is the wrong shape — there it is a config and a pool.

Diagnostics

php
use Flytachi\Winter\Redis\RedisPool;

$config = RedisPool::config(MainRedisConfig::class);

$config->getDsn();        // 'redis://localhost:6379/0' — no password, safe to log
$config->ping();          // true | false, never throws
$config->pingDetail();    // ['status' => true, 'latency' => 0.32, 'error' => null]

pingDetail() is what /actuator/health reports: it answers not only “is it alive” but how long the round trip took, and on failure the error text.

The instance from config() is not the one in the pool

RedisPool::config() returns the registration instance: it exists so settings can be read. The connections are held by other instances — one per pool slot, and those are the ones the pool probes and closes.

Next

  • Stores — how application code uses all this
  • Connection pool — how many connections, and what happens when they run out