Postgres over the ring
ioxide.pg is a hand-rolled Postgres driver where everything -
TCP connect, startup handshake, every query - is an io_uring op on the host reactor's ring,
resumed inline. No Npgsql, no socket engine, no thread pool.
The pool
var options = new PgOptions
{
Host = "127.0.0.1", // IPv4 literal - resolve DNS up front
User = "bench",
Database = "bench",
PoolSize = 8, // connections per reactor
};
// At startup, on the reactor thread.
reactor.OnStart = r => PgPool.Start(r, options);
// In the handler: rent → query → return, in one call.
var pool = reactor.GetService<PgPool>();
var result = await pool.QueryAsync("SELECT 42");
// result.Value: "42" · result.Rows: 1 · result.CommandTag: "SELECT 1"
One PgConnection is one conversation - one query in flight. The pool is the
concurrency: QueryAsync rents a connection, runs the query, returns it; when all
are busy, requests queue and resume inline as connections free up. For multi-statement work
(transactions), RentAsync/Return directly.
Warmup is concurrent and non-blocking: Start fires PoolSize
ring-native connects; a request that arrives first simply waits in the rent queue until the
first connection lands. Total server-side connections = PoolSize × ReactorCount.
Failure semantics
| Failure | Behavior |
|---|---|
| Server error (bad SQL, missing table) | PgException with severity + SQLSTATE, thrown after the stream resyncs at ReadyForQuery - the connection stays usable |
| Transport failure (reset, EOF, malformed framing) | connection marked IsBroken; the pool discards it on return and opens a replacement in the background |
| Database restart | broken connections get replaced as they're returned - the server heals without a restart |
try
{
var result = await pool.QueryAsync(sql);
}
catch (PgException e) when (e.SqlState == "42P01")
{
// Relation does not exist - the connection is already back in rotation.
}
Wire details
- Simple query protocol, UTF-8 throughout; the parser walks messages incrementally and compacts/grows its native buffer (64KB → 1MB) for large responses.
PgResultcarries the first column of the first row as text, the row count, and the command tag - enough for the common case; a typed row reader is roadmap.- Trust authentication only, today. SCRAM, the extended protocol (prepared statements), and pipelining are the driver's roadmap, in that order.
/sleep
route and a few concurrent curls.