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 ordered conversation, and commands pipeline
onto it - many in flight at once, answered in order. QueryAsync picks the next
connection round-robin; the pool is parallelism, not exclusive checkout, and there is no
rent/return surface. That also means a transaction must not be split across
QueryAsync calls - other handlers' queries interleave on the same connection.
Keep it in one SQL string (BEGIN; …; COMMIT), which travels as one simple-query
message and runs as one unit.
Warmup is concurrent and non-blocking: Start fires PoolSize
ring-native connects; a request that arrives first simply queues 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 and extended (parameterised, auto-prepared) query protocols, UTF-8 throughout; the
parser walks messages incrementally and compacts then grows its native buffer up to
PgOptions.MaxReceiveBytes(default 64 MB) for large responses. PgResultcarries the first column of the first row as text, the row count, and the command tag for the common case; the streaming overload (QueryAsync(sql, args, onRow)) hands each row to the callback as aPgRowoverPgColumn[]. Typed column conversion is roadmap.- Trust and SCRAM-SHA-256 authentication; parameterised queries auto-prepare, and commands pipeline onto a connection - many in flight, answered in order.
/sleep
route and a few concurrent curls.