Pools
GuidesA connection is one fiber’s at a time. A program serving requests makes a
pool once, where it starts, and lends its connections to each piece of
work. rowtype-postgres makes connections and pools as
postgres-eio makes them,
every option passed through.
A pool
Section titled “A pool”module S = Rowtypemodule Pg = Rowtype_postgres
let count_users = S.find ~params:S.unit ~row:S.int "select count(*) from users"
let () = Eio_main.run @@ fun env -> Eio.Switch.run @@ fun sw -> match Pg.Conninfo.of_string "postgres://app@localhost/shop" with | Error m -> prerr_endline m | Ok target -> ( let ( let* ) = Result.bind in match let* pool = Pg.Pool.create ~sw ~net:(Eio.Stdenv.net env) ~mono_clock:(Eio.Stdenv.mono_clock env) ~size:16 ~observe:Counting.observe target in let users = Result.join (Pg.Pool.use pool (fun db -> Pg.run db count_users ())) in Pg.Pool.close pool; users with | Ok n -> Printf.printf "%d users, in %d statements\n" n (Atomic.get Counting.statements) | Error e -> prerr_endline (Pg.error_to_string e))Pg.Pool.create opens every connection up front, and answers the first
that could not be made, closing those made before it.
| Option | Default | |
|---|---|---|
~size |
8 | connections, at least one |
~wait |
5 s | how long a borrow waits for a connection |
~reset |
true |
each connection’s session reset before it is lent again |
~max_lifetime |
30 min | a connection older than this is replaced |
~idle_check |
60 s | a connection idle longer is pinged, and replaced if it fails |
~timeout |
30 s | the longest a connection waits on the server for bytes to move |
~observe |
none | an observer of every statement its connections run |
The durations are Mtime.Span.ts, written Mtime.Span.(2 * s). A
connection’s reset ends what a borrower left in its session – an open
transaction, a LISTEN, its settings – and keeps the prepared statements
borrowers share; ~reset:false keeps the settings.
Pg.Pool.use pool f borrows a connection, runs f on the calling fiber,
and gives it back, even if f raises. It is `Pool `Busy if no
connection came free within the wait, and `Pool `Closed at once if the
pool is closed, or closes while the borrow waits. Pg.Pool.stats says how
many connections it keeps, how many are idle, how many borrows wait and how
many it replaced, for a health endpoint. Pg.Pool.close closes it.
A transaction in a borrowed connection is one result:
Result.join (Pg.Pool.use pool (fun db -> Pg.Transaction.within db work))One connection
Section titled “One connection”Pg.connect ~sw ~net ~mono_clock target makes one connection, with the
same ?timeout and ?observe, ?parameters for the session’s settings,
sent as it starts (("statement_timeout", "10s")), and ?statement_cache
for how many statements it keeps parsed, 256 unless given; 0 turns the
cache off, for a pooler without named statements.
Pg.close dbcloses it for good, telling the server; its next statement says so.Pg.revive dbmakes again, in place, a connection a failure closed – the server restarted, say – and isOkat once for an open one. Onecloseclosed stays closed.Pg.timeout dbandPg.set_timeout db ~timeoutread and set the bound on every read and write, lifted around work that runs longer, a migration, and put back after it.
A statement runs on the calling fiber. A fiber cancelled with a statement
in flight closes its connection and asks the server to cancel the
statement, so nothing runs on for nobody. A raise from Pg.fold’s function
passes, and closes the connection, whose answer was read only in part: its
next statement is `Connection `Closed until Pg.revive.
Watching statements
Section titled “Watching statements”An observer is handed each statement’s text as it runs, and runs it – a span begun and ended around it, a count:
module Pg = Rowtype_postgres
let statements = Atomic.make 0
let observe : Pg.observer = { around = (fun _sql run -> Atomic.incr statements; run ()); }The text has placeholders where the values are, and the values are never
handed over, since a code, a digest or a token is one. It watches a
transaction’s begin and commit too. Each statement is also a debug
line on the rowtype-postgres log source, with how long it took: its first
line of text, never its parameters.
Notifications
Section titled “Notifications”A session that ran LISTEN keeps what it hears, so a listener is a
connection of its own, never a pooled one:
module Pg = Rowtype_postgres
let ( let* ) = Result.bind
let rec hear listener = let* event = Pg.Listener.next listener in (match (event : Pg.Listener.event) with | Notification { channel; payload } -> Printf.printf "%s: %s\n%!" channel payload | Reconnected -> print_endline "reconnected: what was sent meanwhile is gone"); hear listener
let () = Eio_main.run @@ fun env -> Eio.Switch.run @@ fun sw -> match Pg.Conninfo.of_string "postgres://app@localhost/shop" with | Error m -> prerr_endline m | Ok target -> ( match let* listener = Pg.Listener.connect ~sw ~net:(Eio.Stdenv.net env) ~mono_clock:(Eio.Stdenv.mono_clock env) target in let* () = Pg.Listener.listen listener "orders" in hear listener with | Ok () -> () | Error e -> prerr_endline (Pg.error_to_string e))Pg.Listener.next answers the next notification, or waits for one. A lost
connection is made again, every channel listened to again, and said so:
Reconnected, since whatever was sent in between is gone. ~keep_alive is
how long next waits in silence before it pings the server, 10 s unless
given. Pg.Listener.unlisten stops hearing a channel, and
Pg.Listener.close closes the listener for good.
