DBIO
view release on metacpan or search on metacpan
docs/adr/0030-async-explicit-per-connection-mode.md view on Meta::CPAN
# ADR 0030 â Async is an explicit, per-connection mode over one core orchestration
- Status: accepted
- Date: 2026-06-30
- Tags: async, future, storage, drivers, pluggable, per-instance-mode, supersedes-0014, replaces-0028, replaces-0029
## Context
ADRs 0014, 0028 and 0029 grew the async story in three layers that, taken
together, became hard to reason about:
- **0014** made `*_async` universal: every storage answers it, degrading on sync
storage to an immediately-resolved `DBIO::Future::Immediate`. The degrade is *silent*
and *always on*.
- **0028** made a real non-blocking driver a pluggable **embedded backend**,
selected by an `async_backend` **class-data** accessor declared per driver.
- **0029** added a universal **auto-fallback**: `async_fallback`, defaulting to
`DBIO::Forked::Storage`, so any driver gains fork-based async automatically once
`dbio-forked` is installed.
Three problems surfaced once the real drivers (`dbio-async` via Future::IO,
`dbio-postgresql-ev`/`dbio-mysql-ev` via EV) existed side by side:
1. **Async is not a transparent feature â it is an execution model.** A
connection-based backend (Future::IO, EV) requires the application to run an
event loop; `forked` requires none (fork + blocking `waitpid`); the degrade
requires none and isn't async at all. These are three different runtime
contracts with the application. Whichever one answers is a property of the
*application's architecture*, not of the set of installed packages. Under 0029,
`cpanm DBIO::Forked` silently turns `select_async` from "immediate" into "forks
a process" â spooky action at a distance, exactly the footgun an execution-model
choice must not be.
2. **Three selection mechanisms for one decision.** `async_backend` (class
default, native), `async_fallback` (auto chain, forked) and the silent degrade
overlap and interact. A schema is *either* sync *or* a single class-chosen async
flavour; you cannot hold a forked schema and an EV schema of the same class at
once.
3. **The Model-B orchestration is written three times.** `dbio-async`,
`dbio-postgresql-ev` and `dbio-mysql-ev` each re-implement the same
loop-agnostic machinery â connect-info normalisation, pool wiring, `_run_crud`
and its runners, `txn_do_async`, the pipeline scaffold, the TransactionContext.
The only genuinely different part is the **query transport** (`_query_async`:
Future::IO vs EV::Pg vs EV::MariaDB) plus a little DB-specific SQL. `dbio-async`
already generalised that machinery behind seam hooks, but no driver inherits it.
The shaping requirement: async must be a thing the application **declares
explicitly, per connection**, and the same schema class must be runnable in
several async modes at once (one forked instance, one Future::IO instance, one EV
instance â all alive together). And the shared orchestration must live in exactly
one place.
## Decision
### 1. Async is an explicit, per-connection mode
The async backend is chosen **at `connect` time, per instance**, by a named
string and is fixed for the life of that instance:
```perl
my $sync = MyApp::Schema->connect($dsn, $u, $p); # sync
my $forked = MyApp::Schema->connect($dsn, $u, $p, { async => 'forked' });
my $futio = MyApp::Schema->connect($dsn, $u, $p, { async => 'future_io' });
my $pgev = MyApp::Schema->connect($dsn, $u, $p, { async => 'ev' });
```
All four instances live in parallel, each with its own connection/pool. The
**schema class carries no async declaration** â the choice is a runtime/deployment
concern, where it belongs. There is no runtime mode-switch on a live instance
(thin value, fat complexity around in-flight transactions and open pools); to run
another mode you `connect` another instance.
### 2. Modes are named, registered against the driver
A small **mode registry** maps a name to a backend class. Generic modes register
themselves globally; native modes are registered by the driver:
| mode | provider | scope | loop |
|---|---|---|---|
| `forked` | `dbio-forked` (`DBIO::Forked::Storage`) | any driver | none |
| `future_io` | `dbio-async`: abstract `DBIO::Async::Storage` base + a per-driver `DBIO::X::Storage::Async` adapter (convention-resolved) | any driver that exposes its own async binding (`DBD::Pg`, `DBD::mysql`) | yes |
| `ev` | the per-driver EV add-on, **driver-resolved** | one DB | yes |
| `immediate` | core (`DBIO::Future::Immediate`) | any | none |
( run in 2.153 seconds using v1.01-cache-2.11-cpan-5c0b1e786e0 )