NAME

DBIO::Storage::PoolBase - Shared connection pool mechanics for async DBIO drivers

VERSION

version 0.900004

SYNOPSIS

package DBIO::PostgreSQL::EV::Pool;
use base 'DBIO::Storage::PoolBase';

# An event-loop transport: EV::Pg->new returns BEFORE the connect
# finishes, so build the connection through the readiness wiring and
# inherit the gate (no _connection_ready_future override needed).
sub _create_connection {
  my ($self, $conninfo) = @_;
  return $self->_create_ready_connection(conninfo => $conninfo);
}

sub _new_connection { my $self = shift; require EV::Pg; EV::Pg->new(@_) }

sub _shutdown_connection { $_[1]->finish }

sub _transform_conninfo { conninfo_string($_[1]) }

See t/storage/pool_base.t and t/storage/pool_ready_wiring.t for runnable examples.

DESCRIPTION

Concrete implementation of the DBIO::Storage::Pool contract hosting the pool mechanics shared by all async drivers: idle-pool handling, capacity-bounded connection creation, the waiter queue and shutdown.

Drivers subclass this and supply only the engine seam:

In the other direction, a driver reports a connection it found dead through "_mark_connection_dead", so the pool retires it instead of handing it out again, and takes a queued caller that stopped waiting out of the queue through "_withdraw_waiter".

METHODS

new

my $pool = Driver::Pool->new(
    conninfo => 'dbname=myapp',
    size     => 10,
    on_error => sub { warn $_[0] },
);

Requires conninfo or conninfo_provider (a coderef returning fresh connect info per connection). The provider is called for every connection the pool opens, as it opens it; what holds when it comes back into the pool from there is said under "acquire". size caps the pool: a positive integer, default 5 -- also for a size that is false. Any other size throws: a pool with a negative or non-numeric size would spawn nothing and queue every "acquire" for good. future_class overrides the Future implementation per instance.

storage (optional) is the owning DBIO::Storage::Async, held weakly. When given, the pool replays that storage's on_connect_do / on_connect_call (and the on_disconnect_* counterparts) against each physical connection at spawn / shutdown.

Two options bound what a caller waits for:

  • acquire_timeout -- seconds a caller may wait in "acquire" for a usable connection, default 30; 0 turns the timeout off. It spans the whole wait: the time in the queue and the time a new connection takes to come up.

  • max_waiters -- the number of callers that may be queued at once, a positive integer. Without it the queue has no limit.

A third option bounds the reset of a released connection ("release"): reset_timeout -- seconds the storage's _reset_pool_connection may take, default 10; 0 turns the timeout off.

All three are given to the constructor, or else read from the owning storage ("async_option" in DBIO::Storage::Async), which takes them as connect options. A value that is no number of seconds, or no positive integer, throws; the message names the option and never its value. The timeout is measured by the owning storage's timer ("_delay" in DBIO::Storage::Async). A pool with no owning storage has no timer: it works with the timeout off, and a positive acquire_timeout given to it throws. The default of 30 seconds applies wherever a timer exists.

assert_limit_option

DBIO::Storage::PoolBase->assert_limit_option(acquire_timeout => $value);

Throws unless $value is a valid setting of the named limit option: acquire_timeout and reset_timeout a number of seconds, 0 or more; txn_timeout a number of seconds above 0; max_waiters a positive integer. The message names the option and what it takes, never the value. It is the one check behind the constructor and the connect options of DBIO::Storage::Async.

_delay

my $f = $pool->_delay($seconds);

The timer behind acquire_timeout: a Future that resolves after $seconds, from the owning storage's "_delay" in DBIO::Storage::Async. A pool with no owning storage throws. Override it to give a pool a timer of its own.

future_class

The Future implementation backing "acquire". Defaults to Future; override in a subclass or pass future_class to "new".

acquire

Returns a connection wrapped in a Future. Hands out an idle connection if one is available, otherwise creates a new connection if the pool has capacity; if all connections are busy and the pool is at max size, queues the request and returns a pending Future that resolves on the next "release". Once "shutdown" starts, the same pool instance is terminal: acquire returns a failed Future with Connection pool has been shut down and never creates or queues another connection.

Whichever of those three paths supplies the connection, acquire ALWAYS chains the result through the "_connection_ready_future" seam before it resolves. For synchronous pools that seam is a no-op — the connection is usable the instant it exists — so the returned Future is ready immediately and behaviour is unchanged. For async transports whose connection is not usable until a background connect completes, the seam is the single place where the Future is held pending until the connection is actually ready; see "_connection_ready_future".

acquire never throws -- a spawn that fails, including a connect action the server refuses, comes back as a failed Future like every other failure on the async path. Spawning is synchronous and runs outside any Future, so acquire catches what it throws and hands it back as the failed Future -- the ->on_fail a Future-based API teaches you to write is where every error arrives. A connection whose connect actions failed is retired ("_retire_connection") rather than left counting against "max_size".

A connection that does not become ready -- its "_connection_ready_future" fails, or the seam dies -- is retired too, and the caller gets that error. Its slot goes on the way "_retire_in_use_connection" hands one on: the oldest caller queued behind it gets a fresh connection, or that spawn's error, instead of waiting for a release nobody is left to make.

In a forked child that inherited the pool, acquire never hands out one of the parent's connections: the first call there drops the inherited state and the child spawns connections of its own ("_verify_pid").

An idle connection is handed out only if "_idle_connection_alive" vouches for it; one it does not is retired and the next one tried, then a fresh one spawned. By default it vouches for every connection.

A queued caller waits on a Future made by "_new_waiter", and the Future acquire returns is chained off it. Cancelling that Future -- with a Future class that can be cancelled, such as Future -- takes the caller out of the queue ("_withdraw_waiter").

Cancelling it while the connection is still coming up -- the "_connection_ready_future" seam still pending -- does not lose the connection: the caller waits on a Future of its own, not on one chained off the readiness Future, so the cancel stops there. When the connection comes up it goes to the next queued caller or to the idle list, and when it fails to it is retired. A callback on the caller's Future that dies as the connection is handed over is reported through the pool's on_error.

Letting go of that Future without cancelling it is not giving up, as far as the pool can tell: the connection comes up, is handed to a Future nobody reads, and stays checked out -- counted, never idle, never handed out again. Cancel the Future of an acquire you stop waiting for.

Between reading the pool's books and writing into them, acquire calls code that is not the pool's: the conninfo_provider, "_transform_conninfo", "_create_connection" and the owning storage's connect actions when it spawns; "_idle_connection_alive" and on_error before it hands out an idle connection; "_new_waiter" before it queues the caller; "_connection_ready_future" before it hands a connection over. Any of it may come back into the pool -- an AccessBroker whose client runs the event loop while it fetches, a transport that reports an error from its constructor -- and two things hold then:

  • A spawn holds its slot from before the first of them. An acquire made from inside a spawn finds that slot taken: in a pool of one it is queued behind the spawn and gets the connection once it is released, and the pool never counts more connections than "max_size". A spawn that fails gives the slot up, and the oldest caller queued behind it gets it, on a spawn of its own. A caller that waits for its Future, blocking, from inside the spawn it is queued behind waits for a spawn that cannot finish first.

  • A shutdown from inside any of them is as final as one before. The acquire that was in it fails with Connection pool has been shut down, and nothing is made, counted, queued or handed out after it. A connection that was being made is closed, once, without on_disconnect_*, like every connection that did not come up. A spawn that died fails its acquire with its own error, shut down or not -- masked by the core: a client library may quote the connection string it was given, password included, so the error of the provider, "_transform_conninfo" and "_create_connection" goes through "mask_connect_secrets" in DBIO::Util before it is passed on, with the pool's conninfo as what is known (a string as the DSN, a hash's password / Password as the password). A transport does not have to mask its errors itself; an exception object is passed on as it is. The error a transport raises later, through on_connect/on_error of "_create_ready_connection", is masked the same way before it settles the readiness Future, reaches the pool's on_error or becomes the reason a connection is retired.

A caller waits for a bounded time, and the queue is bounded if you say so. acquire_timeout ("new", default 30 seconds, 0 for no timeout) covers the caller's whole wait for a usable connection: the time in the queue and the time a new connection takes to come up, counted from the call. When it runs out the caller's waiter is taken out of the queue ("_withdraw_waiter") and fails with no pool connection within N seconds; a connection that comes up after that goes to the next queued caller or to the idle list, and nobody orphaned is handed one. A caller that gets its connection without waiting arms no timer. max_waiters bounds the queue: with that many callers queued, the next "acquire" returns a failed Future at once, pool exhausted: N callers waiting, and queues nothing. Neither message carries the SQL or the binds of the refused caller.

_new_waiter

sub _new_waiter {
  my ($self) = @_;
  my $waiter = My::Transport::Future->new;   # knows the transport's loop
  return $waiter;
}

Waiter seam (ADR 0047 addendum 2026-09-28). "acquire" (and with it "acquire_txn") calls it when it has to queue the caller -- no idle connection it can hand out, the pool at "max_size" -- once per queued caller, and never otherwise. It returns the new, pending Future the caller waits on: the pool queues it, and "release" (or "_retire_in_use_connection") settles it with a connection, "shutdown" fails it.

The default is $self->future_class->new. Override it when the queued caller's Future has to be of a class of the transport's own. Every ->then off a pending Future is made with that Future's own new -- in Future as in DBIO::Test::Future -- so the waiter's class is the class of everything the caller builds on the wait: the Future "acquire" returns, the query chained off it, the Future a blocking call finally ->gets. A plain Future knows no event loop, and get on it while pending croaks is not yet complete and does not provide ->await; a waiter whose class drives the transport's loop in await lets a blocking call wait for its turn instead. Only the waiter needs that class: a Future the pool hands out ready never waits.

What the seam must and must not do:

  • Return a new, pending Future answering is_ready, then, done and fail. It may carry marks of the transport's own -- a back-reference to the pool, held weakly, since the pool holds the waiter.

  • Not queue, register or settle it. The pool queues it after the seam returns, and wires the cancel hook ("_withdraw_waiter") whatever the override does, so an override never has to call the default.

  • A seam that dies, or returns anything but a pending Future, fails the "acquire" -- with its error, or naming the class and _new_waiter -- and nothing is queued. "acquire" never throws. A pool shut down from inside the seam queues nothing either: the "acquire" fails with the shutdown.

_withdraw_waiter

my $was_queued = $pool->_withdraw_waiter($waiter);
$waiter->fail("gave up waiting\n") if $was_queued;

Take a queued caller's waiter -- a Future made by "_new_waiter", the one "acquire" chains the caller's Future off -- out of the queue, so no later "release" or "_retire_in_use_connection" hands it a connection (ADR 0047 addendum 2026-09-28). Returns true if $waiter was queued, false if it was not: already handed a connection, already withdrawn, dropped by "shutdown" or as inherited after a fork, or never the pool's.

It does not settle $waiter: whoever withdraws it does, typically failing it with the reason it stopped waiting, which reaches the caller through the chain. A withdrawn waiter left pending waits for good. No connection is lost: a waiter is handed a connection only as it leaves the queue, so one still in the queue holds none.

A waiter whose class can be cancelled (Future and its subclasses answer on_cancel) is withdrawn by the pool itself when it is cancelled -- also when the caller cancels its own Future, since Future carries a cancel down the ->then chain to the waiter. A waiter settled by somebody else without being withdrawn is not lost either: "release" passes over a waiter that is no longer pending, but it stays queued until then.

_collect_queued

my ($future, @waiters) = $pool->_collect_queued(sub { $storage->some_async(...) });

Runs the coderef and returns what it returned followed by the waiters "acquire" queued while it ran -- the callers the call itself left waiting for a connection. Used by DBIO::Storage::Async around the Futures it builds for the calls on the storage itself, which is how the pool knows which Futures wait on its own queue ("_note_queued_for").

_note_queued_for

$pool->_note_queued_for($future, @waiters);

Records that $future waits on the queued @waiters of this pool: a Future derived from them, as the Futures are that the *_async methods of the storage return. The pool holds the Future weakly and forgets it once it is decided. See "_refuse_queued_for".

_queued_waiters_for

my @waiters = $pool->_queued_waiters_for($future);

The waiters of this pool, still queued, that $future waits on: those noted for it ("_note_queued_for"), and for an aggregate such as Future->needs_all those of the Futures it is made of, found through pending_futures where the Future class has it. Empty for a Future that waits on none of this pool's queue.

_refuse_queued_for

my $n = $pool->_refuse_queued_for($future, $message);

Takes every waiter $future waits on ("_queued_waiters_for") out of the queue and fails it with $message, so the statement behind it never runs. Returns how many it took out. Asked by DBIO::Storage::Async for the Future a transaction's or a pipeline's coderef returns: a Future that waits on this pool's own queue cannot settle before the transaction or pipeline that holds its connection ends ("_txn_inner_future" in DBIO::Storage::Async).

_idle_connection_alive

sub _idle_connection_alive {
  my ($self, $conn) = @_;
  ...   # local state, or a non-blocking read of what the server already sent
  return $alive;
}

Borrow-time liveness seam (ADR 0047 addendum 2026-09-27). "acquire" asks it about an idle connection before handing that connection out. One it does not vouch for -- false, or a throw, which is also passed to the pool's on_error (an on_error that dies in turn is warned, never thrown out of "acquire") -- is retired through "_retire_connection", and the next idle connection is tried, then a fresh one spawned (or the caller queued, at "max_size"). So a connection whose server session ended while it sat idle -- a server restart, an idle timeout, an administrator's kill -- does not cost a caller a failed query first. Without the seam the only report of such a death is "_mark_connection_dead" from that failed query, and after a server restart every idle connection is dead: each is handed out, and fails, once.

The default answers true without looking: no check and no cost, the behaviour from before the seam. Override it only if the transport can answer without a round trip -- from local state, or from a non-blocking read of what the server has already sent (the close or the error a server sends when it ends an idle session). It runs on every borrow of an idle connection, so it must never block and never send anything to the server.

It narrows the window; it does not close it. A connection can still die between the check and its first statement, or without the server saying so (a network partition); that statement fails, and the transport reports the connection through "_mark_connection_dead" as before. It is asked about idle connections only: a freshly spawned one has just come up, and one handed straight to a queued caller was in use a moment ago.

_connection_ready_future

sub _connection_ready_future { my ($self, $conn) = @_; ... }

Readiness seam, chained by "acquire" around every connection it hands out — freshly spawned, reused-idle, or handed to a queued waiter. Returns a Future that resolves to $conn once the connection is actually ready to run queries.

For a synchronous pool the default is a safe no-op: with no readiness Future registered for the connection it answers $self->future_class->done($conn) (the whole default is shown below).

A DBI / synchronous connection is usable the instant its constructor returns, so an immediately-done Future is correct and "acquire" stays ready-at-once. Behaviour for every existing synchronous pool is therefore unchanged.

Async transports MUST gate readiness here -- by building their connections through "_create_ready_connection", which registers the readiness Future the default honours, or by overriding this seam when readiness is not callback-shaped. A backend whose connection is not usable when its constructor returns — every event-loop transport: EV::Pg, EV::MariaDB, future_io, ... — is still connecting in the background when the handle first exists. If such a driver registers no readiness Future and leaves this seam at its default, "acquire" hands out a live-looking but unconnected handle and the very first bound query on a cold pool dies not connected. This is not a theoretical edge: dbio-mysql-ev shipped exactly this bug — its _create_connection wired on_connect => sub {}, a pure no-op, with no readiness tracking, and the symptom was papered over with test-harness pre-warming rather than fixed in the driver. The correct gate is a per-connection Future that resolves from the transport's on_connect callback and fails from its on_error -- what "_create_ready_connection" wires.

The default deliberately does NOT auto-detect the async case — it cannot know whether a given transport's constructor connects synchronously — so forgetting the gate stays possible by design. What the base does provide is that the responsibility now lives at ONE documented seam, and the bookkeeping for the common "resolve a per-connection Future from a connect callback" pattern is supplied by "_register_connection_ready", "_connection_ready_lookup" and "_clear_connection_ready".

The default honours a registered readiness Future (ADR 0030 §4)

The default is not an unconditional immediately-done: it consults the readiness side table first, so a transport that registers a per-connection Future — which "_create_ready_connection" does for you — is gated correctly without overriding this seam at all. The default is exactly:

return $self->future_class->done($conn)
  if blessed($conn) && $conn->can('is_connected') && $conn->is_connected;
return $self->_connection_ready_lookup($conn)
    || $self->future_class->done($conn);

which is the override dbio-postgresql-ev and dbio-mysql-ev each wrote by hand (identically), lifted here so they can inherit it instead.

Nothing that worked before changes. A synchronous pool registers no readiness Future, so the lookup misses and the result is the same immediately-done as before; the is_connected probe is only ever a fast path to that same answer, never away from it, and it is guarded by can() because core cannot require an arbitrary transport handle to implement it. A driver that keeps its own override is unaffected — the default is not consulted at all.

What the pool does with a pending readiness Future

A Future the seam returns ready is consumed at once. Onto one it returns pending the pool chains itself exactly once per "acquire" -- one ->then -- and it makes the caller's Future with new called on that instance, so a pending readiness Future has to answer both. The pool holds the chain; the caller holds only its own Future. For an override that returns a Future of the transport's own, one never registered through "_register_connection_ready", that means:

  • The caller's cancel does not reach it. The caller's Future is not chained off the readiness Future, so cancelling an "acquire" no longer cancels the connect behind it: the connect runs on, and the connection goes to the next queued caller or to the idle list. A transport that wants a connect stopped has to stop it itself.

  • "shutdown" cancels the chain. With Future, cancelling a ->then cancels the Future it was chained off, so the readiness Future itself is cancelled: its on_cancel callbacks run, and a done or fail that arrives later is ignored. A Future class that cannot be cancelled is only let go of, and settling it later changes nothing in the shut down pool. In global destruction nothing is cancelled, as nothing is settled there. A registered readiness Future of a connection the pool holds is failed by "shutdown" instead, before any of this.

  • It has to settle. Resolve or fail it for every connection it was returned pending for. Until then the caller waits, and the pool holds the chain and through it the connection -- until "shutdown", or until the connection is retired, at the latest. After a retirement the pool still keeps the chain itself, and the Future with it, until the Future settles or the pool is gone.

_create_ready_connection

sub _create_connection {
  my ($self, $conninfo) = @_;
  return $self->_create_ready_connection(conninfo => $conninfo);
}

Builds one connection through "_new_connection" with its readiness Future already wired and registered — the whole of the "async pool" boilerplate an event-loop transport would otherwise hand-roll (ADR 0030 §4). Takes the named arguments the transport's constructor expects and returns the connection; "_connection_ready_future" then finds the Future by connection, so a driver that builds its connections this way needs no readiness code of its own.

The wiring owns on_connect and on_error: they are appended after the caller's arguments, so passing your own is not a way to extend them. on_connect resolves the Future with the connection; an error before connect fails it — so a dependent "acquire" fails instead of hanging — and after connect the Future is already ready, so errors fall through to the pool's own on_error, which is called in either case.

The transport keeps both callbacks as long as the connection lives, so they hold the connection and its readiness Future only weakly; the pool's connection list and readiness table are what keep them alive. Once the pool lets go of a connection -- retired, shut down, dropped as inherited after a fork -- it is freed as soon as no caller holds it. A callback that fires once its Future is gone settles nothing; on_error still reaches the pool's on_error.

That holds for a connection retired through "_retire_connection" while an "acquire" still waits for it to come up as well: the pool lets go of that acquire's hand-over with the connection. What it keeps is the readiness Future and its own chain on it, until that Future settles or the pool is gone -- the Future may be settled after all, by a callback of a connection its transport still holds.

A callback somebody hung on the readiness Future that dies as on_connect or on_error settles it is reported through the pool's on_error and does not reach the transport; the error the transport reported is passed on all the same.

This is the mechanism, not a policy: a pool whose connections are usable the moment the constructor returns has no reason to call it, and "_create_connection" stays a required hook precisely so that choice is made explicitly per driver rather than guessed by the base.

_new_connection

sub _new_connection {
  my ($self, %args) = @_;
  require EV::Pg;
  return EV::Pg->new(%args);
}

Raw-handle seam used by "_create_ready_connection": construct the transport's connection object from named arguments (the caller's, plus the on_connect / on_error the readiness wiring supplies) and return it. Nothing else — do not register or track the connection here.

It exists as a named method rather than a coderef so a test can subclass the pool and return a fake connection, exercising the readiness wiring with no event loop and no database. Both EV pools had already isolated exactly this seam by hand (_new_ev_pg, _new_ev_mariadb) for that reason.

Croaks unless overridden; only "_create_ready_connection" calls it, so a pool that builds its connections in its own "_create_connection" never needs it.

_register_connection_ready

$self->_register_connection_ready($conn, $ready_future);

Bookkeeping primitive for the "_connection_ready_future" pattern. Stores a per-connection readiness Future keyed by Scalar::Util::refaddr($conn), so an async "_create_connection" can hand the Future's done / fail to the transport's on_connect / on_error callbacks and have "_connection_ready_future" find it again by connection. Returns the Future.

_connection_ready_lookup

my $ready = $self->_connection_ready_lookup($conn);

Return the readiness Future registered for $conn via "_register_connection_ready", or undef if none (e.g. a synchronous pool that never registered one). An async "_connection_ready_future" override typically returns $self->_connection_ready_lookup($conn) || $self->future_class->done($conn).

_clear_connection_ready

$self->_clear_connection_ready($conn);

Drop $conn's readiness Future from the side table. Called for you by "shutdown" for every pooled connection, so an async "_shutdown_connection" override never has to clean the table up itself; also exposed for drivers that retire a single connection outside shutdown. A no-op when no readiness table exists.

acquire_txn

Acquire a connection pinned for exclusive transaction use. Same as "acquire" but the connection will not be released back to the idle pool until explicitly released. Being "acquire", it carries the same fork guard.

release

$pool->release($conn);
$pool->release($conn, $lease);

Return a connection to the idle pool. $lease is optional, from "_lease"; see below. If waiters are queued, hands the connection straight to the oldest waiter instead. After "shutdown", release is a no-op: a late callback cannot repopulate the idle list or settle a waiter.

The session is reset at the end of every borrow (ADR 0079). Release stays the synchronous return of ownership; what follows is the pool's. With an owning storage that has _reset_pool_connection ("_reset_pool_connection" in DBIO::Storage::Async), the connection goes into the state resetting: the lease ends at once, a second release of it is a no-op, its slot stays taken and neither the idle list nor a queued waiter gets it. The pool holds the Future the seam returned. When it is done, the connection goes to the oldest waiter still waiting, or to the idle list. A seam that fails, dies, returns no Future, or does not answer within reset_timeout retires the connection ("_retire_in_use_connection"): the next borrower gets a fresh one. "shutdown", "_verify_pid" and a retirement void a reset that is running; its Future is cancelled (a forked child's inherited one is left alone) and a late answer changes nothing. "shutdown" sends no disconnect action beside a reset. A connection that was never handed out -- one that came up for a caller who gave up meanwhile -- is not reset. A pool without an owning storage is not a session pool: it hands the connection on as it is.

A waiter that is no longer waiting -- its Future cancelled, or settled by somebody else -- is passed over and dropped from the queue: the connection goes to the oldest waiter still waiting, or back to the idle list when there is none. Handed to a cancelled Future, the connection would be lost -- neither borrowed nor idle, in a pool of one for good -- and a settled one refuses it.

The waiter's callbacks run inside release, as its Future is settled, and they are another caller's. One that dies does not reach whoever released: it is reported through the pool's on_error -- and warned, if on_error dies in turn -- as where a connection is handed over as it comes up ("acquire"). The connection is the waiter's all the same, and not idle as well. This is about a callback a Future runs bare, Future's on_done, on_fail and on_ready. The death of one made with ->then is the failure of the Future ->then returned, in Future as in DBIO::Test::Future, and the pool is told nothing.

A connection the pool does not hold is not taken back. In a forked child that is a connection inherited from the parent ("_verify_pid"): taking it in would hand the parent's session out in the child after all.

A connection reported dead through "_mark_connection_dead" is not taken back either: release retires it ("_retire_in_use_connection"), and a queued waiter gets a fresh connection in its place.

A connection that is already idle is not taken a second time: releasing it again changes nothing, where it would otherwise sit on the idle list twice and be handed to two callers.

A release that arrives after the connection went on -- to a queued caller, or to the next "acquire" -- is told from its current holder's by the lease. The holder takes it with "_lease" right after its acquire and passes it as the second argument; a release whose lease is not the connection's current one is a silent no-op. A release without a lease cannot be told and is taken as its current holder's: the callers that can lose the race pass it, and the others release once.

_lease

my $lease = $pool->_lease($conn);
...
$pool->release($conn, $lease);

The lease of the borrow $conn is in right now. Ask for it right after "acquire" resolved, and hand it back as the second argument of "release". A connection has one lease for as long as it is borrowed: asking twice gives the same one, and the release that ends the borrow -- or the connection's retirement -- ends the lease, so whoever gets the connection next is given another.

A release that carries a lease is taken only when it is the connection's current one. One that is not belongs to an earlier borrow -- the connection was handed to a queued caller, or borrowed again from the idle list, since -- and is a silent no-op: it changes nothing, throws nothing, warns nothing and does not reach on_error, as every release the pool does not take. A release without a lease is taken as the release of the current holder, as it always was.

Returns nothing for a connection the pool does not hold, and after "shutdown". The lease is opaque: compare it for nothing, hand it back only.

size

Total connections (active + idle). A connection still being spawned is not one yet, though its slot is taken ("acquire").

available

Number of idle connections.

max_size

Configured maximum pool size.

is_shutdown

if ($pool->is_shutdown) { ... }

True once "shutdown" has been called on the pool, false before. A pool that is shut down refuses "acquire"; a caller that holds a pool across several steps ("deploy_async" in DBIO::Storage::Async) asks it between the steps.

shutdown

Permanently shut down this pool instance. The transition is idempotent and terminal: queued and readiness-pending acquires fail deterministically with Connection pool has been shut down, later acquire / acquire_txn requests return the same failed-Future diagnostic, and late "release" calls have no effect.

Connections that reached readiness run the owning storage's disconnect actions before "_shutdown_connection". Cold connections whose readiness is still pending run no disconnect actions; they are retired and closed exactly once. Restart belongs to the owning storage lifecycle, which builds a new backend and pool rather than reviving this instance.

A callback that dies as shutdown fails its Future -- that of a queued caller, of a caller waiting for its connection to come up, the readiness Future of such a connection -- or as the wait for a connection is cancelled, is reported through the pool's on_error (and warned, if on_error dies in turn) and does not end the shutdown: everyone else is failed and every connection closed all the same. Thrown out of shutdown, it left the callers behind it unsettled and the connections open, in a pool that takes no second shutdown.

In a forked child that inherited the pool, shutdown -- also the one DESTROY runs when the child exits -- first drops the inherited state through "_verify_pid", like every other entry point: the parent's queued waiters are neither failed nor resolved, so none of the parent's callbacks runs in the child, and none of the parent's connections is closed. The pool is shut down for the child all the same.

In global destruction -- the shutdown DESTROY runs when a process exits with the pool still alive -- it settles no Future: queued waiters and pending readiness Futures are dropped, not failed. Perl frees objects in no particular order there, so a waiter may already be gone, or the Future its caller holds may be; and no event loop runs any more, so nobody is left to be told. The connections are closed as always, as far as they are still there: one that is already gone is skipped. What tells a cold connection from one that came up -- its readiness Future, the hand-over of the acquire waiting for it -- may be gone ahead of the pool as well, so there the disconnect actions can run against a connection still coming up.

_verify_pid

$pool->_verify_pid;

Fork safety on the way in, the counterpart of "shutdown"'s on the way out. The pool's connections belong to the process that opened them, recorded as the pool's owner. "acquire" (and with it "acquire_txn"), "release", "shutdown" and "_retire_connection" call this first. In any other process -- a forked child that inherited the pool -- it drops everything the pool inherited and makes the current process the owner, so the child's first "acquire" spawns a connection of its own instead of handing out one of the parent's, whose socket the parent is still reading.

The inherited state is dropped, not torn down, the way DBIO::Storage::DBI::_verify_pid drops an inherited $dbh: no "_shutdown_connection" and no on_disconnect_* actions, because the connections are the parent's and it is still using them. Queued waiters are dropped unsettled: they are the parent's callers, and settling one in the child would run the parent's callback there. The readiness bookkeeping goes with them, and so do the hand-overs the pool held for acquires still waiting for a connection to come up ("acquire"): dropped unsettled, their callers being the parent's. So does the slot of a spawn that was in flight when the process forked -- a provider that forks while it answers: the spawn is the parent's, and holds nothing in the child's pool.

A hand-over's chain cannot be taken off the readiness Future it was made off, and that Future may still be settled in the child -- one the transport made itself in "_connection_ready_future", say. So the chain is neither cancelled, which would run the parent's on_cancel callbacks here, nor just forgotten: the pool leaves it with nothing to act for and keeps it until its readiness Future settles. Settling that Future in the child then resolves none of the parent's callers, gives the child's pool no connection, and Future has no sequence Future to warn of. The child's pool holds neither the parent's caller nor the inherited connection for it, and a "shutdown" in the child neither cancels such a chain nor fails anybody. The chains go with the child's pool; a readiness Future settled only after that makes Future warn lost a sequence Future.

Dropping a handle must not end the parent's session from the handle's destructor either, and that half belongs to the transport: it opens its connections fork-safe (DBD::Pg, DBD::mysql: AutoInactiveDestroy => 1), as "_listen_open" in DBIO::Storage::Async requires for the listen connection.

A pool with no recorded owner is left alone, as the sync storage leaves a handle with no recorded pid.

set_conninfo_provider

$pool->set_conninfo_provider($provider);

Makes every connection this pool opens from now on come from $provider (a coderef answering the conninfo, see "new") and drops the static conninfo the pool was built with. Connections that are open and leases in flight are not touched; a spawn already running keeps the conninfo it took. Called by "set_access_broker" in DBIO::Storage::Async when a broker is attached after the pool was built.

_retire_connection

$pool->_retire_connection($conn);

Drop $conn from the pool for good: unregister it from the connection list so it stops counting against "max_size", close it through "_shutdown_connection" (best-effort, like "shutdown"), and clear its readiness bookkeeping. For a connection that must never be handed out -- a fresh connection whose connect actions were refused, and available to a driver that has to retire a connection it found dead.

The idle list is also cleared of $conn, so a retired connection cannot be reissued by a concurrent caller. The waiter queue is deliberately untouched: a connection this is called for was never handed to a waiter, and waking or replacing waiters belongs to a separate concern -- for a checked-out connection, "_retire_in_use_connection". The on_disconnect_* actions are not run either -- the connect actions are what failed, so there is no session state to tear down and the teardown would only fail in turn.

A connection an "acquire" still waits on -- retired while it is coming up -- takes that acquire's hand-over with it. The acquire fails with Connection was retired while it was coming up, unless its caller gave up already; it waited for a connection that was gone, until "shutdown". The pool holds neither the caller's Future nor the connection any more, which is freed as soon as no caller holds it. A readiness Future that settles afterwards hands nobody the connection the pool closed; if it fails, the slot the connection had is offered to the oldest caller still waiting, as after any connect that failed. The queue is left alone here as well: hand the slot on at once with "_retire_in_use_connection". During "shutdown" none of this applies -- it fails every caller itself.

_retire_in_use_connection

$pool->_retire_in_use_connection($conn);

The "release" counterpart for a checked-out connection that must never be handed out again, because the caller left it in a state the next borrower cannot use -- a pipeline whose exit failed, say. The connection is retired through "_retire_connection", and the slot it held goes where "release" would have sent the connection: to the oldest queued waiter, which gets a freshly spawned replacement, or fails with the spawn's error rather than waiting for a release that may never come. A spawn that failed took no slot, so the next waiter gets the next attempt, until one is served or none is left. With no waiter queued nothing is spawned; the next "acquire" spawns on demand. After "shutdown" it is a no-op, like a late "release". A waiter no longer waiting is passed over as "release" passes it over, and nothing is spawned for it.

The replacement is spawned for a waiter already taken off the queue, and the spawn calls code that is not the pool's ("acquire"). A pool shut down from inside it fails that waiter with Connection pool has been shut down, like everyone still queued, and leaves no connection open. A waiter that gave up while its replacement was spawned -- cancelled, or settled by somebody else -- is passed over then: the replacement goes where "release" sends a connection, to the oldest caller still waiting or to the idle list.

A callback of a waiter that dies as the waiter is settled here -- with the replacement, with the spawn's error, with the shutdown -- goes to the pool's on_error, as in "release", not to whoever retired the connection. After a spawn's error the slot goes on to the next waiter all the same.

"_retire_connection" on its own leaves the waiter queue alone, which is right for the connections it was written for: none of them was ever handed out. A checked-out connection's slot is what a waiter is queued for, so retiring it without handing the slot on would leave that waiter pending until some unrelated release came along -- in a pool of one, for good. The same holds for a connection still coming up, which "acquire" therefore retires through this method when it does not become ready.

_mark_connection_dead

$pool->_mark_connection_dead($conn);

Report that $conn is dead -- its server session is gone, its socket closed, the protocol out of step -- so the pool never hands it out again. The pool cannot see this by itself: its only per-connection health signal is the readiness Future of "_connection_ready_future", which resolves once, when the connection first comes up, and stays resolved after the session behind it has died. Without a report, "release" puts a dead connection back on the idle list and the next "acquire" hands it to a caller whose query then fails the same way.

Call it from wherever the death is seen: typically the transport's query path on a connection-level error (server has gone away, connection lost -- not a query error the session survives), or a connection error callback such as EV::Pg's on_error after connect. It may be called any number of times, before or after the connection is released. What happens depends on where the connection is:

  • checked out -- nothing yet: whoever holds it still does, and will hand it back. When it comes back through "release", it is retired through "_retire_in_use_connection" instead of idled, and a caller queued for its slot gets a freshly spawned connection. The report does not replace the hand back: the caller releases as always.

  • idle -- retired at once through "_retire_connection". No one holds it, and no waiter can be queued while a connection is idle.

  • not held by the pool -- already retired, never the pool's, or inherited across a fork -- and after "shutdown": nothing happens.

The mark is a weak reference to the connection. It neither keeps a connection the pool has dropped alive -- one a forked child dropped as inherited, say -- nor outlives it, so it cannot land on a later connection that happens to reuse the address. Retiring the connection also clears it.

_create_connection

sub _create_connection { my ($self, $conninfo) = @_; ... }

Required driver hook: build and return one connection from the (already transformed) connect info. The pool tracks the connection; do not push it anywhere yourself.

_shutdown_connection

sub _shutdown_connection { my ($self, $conn) = @_; $conn->finish }

Optional driver hook: close one connection during "shutdown". Defaults to a no-op; exceptions are swallowed by the caller.

_transform_conninfo

sub _transform_conninfo { my ($self, $conninfo) = @_; ... }

Optional driver hook: adapt stored connect info into the shape the driver's connection constructor expects (e.g. a libpq conninfo string). Defaults to returning it unchanged.

AUTHOR

DBIO & DBIx::Class Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.