Package · cpool

Writing an adapter

The adapter is the only place that knows what is in the pool. Three methods, and nearly all of the difficulty sits in one of them.

The contract

php
interface ConnectionFactory
{
  public function create(): object;
  public function validate(object $connection): bool;
  public function close(object $connection): void;
}

create()

Opens one connection and returns it. It may throw — the pool wraps any exception in PoolException::connectFailed(), so the caller sees a single type regardless of driver.

This is where the full handshake happens: connect, authenticate, select the database, set session parameters. A half-configured connection handed to the pool becomes a half-configured connection in some random borrower’s hands later.

validate() — where it usually goes wrong

It answers one question: would a query work on this connection right now?

php
public function validate(object $connection): bool
{
  try {
      $connection->query('SELECT 1');   // Redis: $connection->ping()
      return true;
  } catch (Throwable) {
      return false;
  }
}

Three rules:

Return false, do not throw. The pool treats a thrown exception as death too, so nothing breaks — but the intent blurs and the trace fills with noise.

One round trip, no more. The probe runs on most borrows, and anything expensive here is paid for by every request.

Check the connection, not the schema. SELECT 1 and PING answer “the socket is alive”. A probe that touches a table will also fail when that table is locked — and the pool will enthusiastically evict perfectly healthy connections.

A fresh connection is not probed

A just-created connection carries its creation moment as its last-used timestamp, so it falls inside the aliveBypassWindow and is handed out unprobed. Probing a socket you opened a second ago means paying for the handshake twice.

close()

Releases the resource. The pool ignores errors here: the connection is going away anyway, and an exception would only hide the reason it is being closed.

Some drivers have nothing to do — PDO closes when dereferenced, and an empty body is more honest than an invented call. Others need an explicit close().

A full example: Redis

src/RedisFactory.php
<?php

use Flytachi\Winter\CPool\ConnectionFactory;

final readonly class RedisFactory implements ConnectionFactory
{
  public function __construct(
      private string $host,
      private int    $port = 6379,
      private string $password = '',
      private int    $database = 0,
  ) {}

  public function create(): object
  {
      $redis = new \Redis();
      $redis->connect($this->host, $this->port, 2.0);

      if ($this->password !== '') {
          $redis->auth($this->password);
      }
      $redis->select($this->database);

      return $redis;
  }

  public function validate(object $connection): bool
  {
      try {
          return $connection->ping() !== false;
      } catch (\Throwable) {
          return false;
      }
  }

  public function close(object $connection): void
  {
      try {
          $connection->close();
      } catch (\Throwable) {
          // already closed — nothing to do
      }
  }
}

What an adapter must not do

Cache the connection. The adapter creates, the pool owns. An adapter that remembers its own result turns a pool of ten connections into one connection shared by ten — the exact bug the pool exists to prevent.

Assume it is called once. create() runs every time the pool grows and after every evicted connection. Anything one-off — reading configuration, resolving a host — belongs in the constructor.

Hold request state. The adapter outlives every borrower. A tenant id or a user context captured into it will leak into connections belonging to other requests.

Next

  • PolicyaliveBypassWindow decides how often validate() is called
  • API reference — signatures and exceptions