Skip to content

Commit 7907e13

Browse files
author
Michael G
committed
Add configurable checkout timeout for transactions
pog.transaction uses pgo:checkout to acquire a dedicated connection, but previously called it with no timeout option, defaulting to pgo_pool's hardcoded 5000ms. This is too short for serverless databases (Neon, Supabase) with cold starts or cross-region connections with high latency. Changes: - Add checkout_timeout field to Config (default: 5000ms for backwards compat) - Add checkout_timeout/2 setter function - Store checkout_timeout in Pool connection variant - Pass timeout to pgo:checkout/2 in transaction path - Update pog_ffi checkout/2 to accept and forward timeout Fixes #82
1 parent 1260e90 commit 7907e13

2 files changed

Lines changed: 29 additions & 9 deletions

File tree

src/pog.gleam

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import gleam/uri.{Uri}
2525
const default_port: Int = 5432
2626

2727
pub opaque type Connection {
28-
Pool(Name(Message))
28+
Pool(name: Name(Message), checkout_timeout: Int)
2929
SingleConnection(SingleConnection)
3030
}
3131

@@ -39,7 +39,7 @@ pub type Message
3939
/// connection will fail.
4040
///
4141
pub fn named_connection(name: Name(Message)) -> Connection {
42-
Pool(name)
42+
Pool(name:, checkout_timeout: 5000)
4343
}
4444

4545
/// The configuration for a pool of connections.
@@ -75,6 +75,11 @@ pub type Config {
7575
/// (default: 1000): The database is pinged every idle_interval when the
7676
/// connection is idle.
7777
idle_interval: Int,
78+
/// (default: 5000): Timeout in milliseconds for checking out a connection
79+
/// from the pool. Used by `transaction` when acquiring a dedicated
80+
/// connection. If the checkout takes longer than this, the transaction
81+
/// fails with `QueryTimeout`.
82+
checkout_timeout: Int,
7883
/// trace (default: False): pgo is instrumented with [OpenCensus][1] and
7984
/// when this option is true a span will be created (if sampled).
8085
///
@@ -189,6 +194,14 @@ pub fn idle_interval(config: Config, idle_interval: Int) -> Config {
189194
Config(..config, idle_interval:)
190195
}
191196

197+
/// Timeout in milliseconds for checking out a connection from the pool.
198+
/// Used by `transaction` when acquiring a dedicated connection.
199+
///
200+
/// default: 5000
201+
pub fn checkout_timeout(config: Config, checkout_timeout: Int) -> Config {
202+
Config(..config, checkout_timeout:)
203+
}
204+
192205
/// Trace pgo is instrumented with [OpenTelemetry][1] and
193206
/// when this option is true a span will be created (if sampled).
194207
///
@@ -235,6 +248,7 @@ pub fn default_config(pool_name pool_name: Name(Message)) -> Config {
235248
queue_target: 50,
236249
queue_interval: 1000,
237250
idle_interval: 1000,
251+
checkout_timeout: 5000,
238252
trace: False,
239253
ip_version: Ipv4,
240254
rows_as_map: False,
@@ -335,7 +349,11 @@ fn extract_ssl_mode(query: option.Option(String)) -> Result(Ssl, Nil) {
335349
///
336350
pub fn start(config: Config) -> actor.StartResult(Connection) {
337351
case start_tree(config) {
338-
Ok(pid) -> Ok(actor.Started(pid, Pool(config.pool_name)))
352+
Ok(pid) ->
353+
Ok(actor.Started(
354+
pid,
355+
Pool(name: config.pool_name, checkout_timeout: config.checkout_timeout),
356+
))
339357
Error(reason) -> Error(actor.InitExited(process.Abnormal(reason)))
340358
}
341359
}
@@ -431,10 +449,11 @@ pub fn transaction(
431449
SingleConnection(conn) -> {
432450
transaction_layer(conn, callback)
433451
}
434-
Pool(name) -> {
452+
Pool(name:, checkout_timeout:) -> {
435453
// Check out a single connection from the pool
436454
use #(ref, conn) <- result.try(
437-
checkout(name) |> result.map_error(TransactionQueryError),
455+
checkout(name, checkout_timeout)
456+
|> result.map_error(TransactionQueryError),
438457
)
439458

440459
// Make a best attempt to check back in the connection, even if this
@@ -481,6 +500,7 @@ fn transaction_layer(
481500
@external(erlang, "pog_ffi", "checkout")
482501
fn checkout(
483502
pool: Name(Message),
503+
timeout: Int,
484504
) -> Result(#(Reference, SingleConnection), QueryError)
485505

486506
@external(erlang, "pgo", "checkin")

src/pog_ffi.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-module(pog_ffi).
22

3-
-export([query/4, query_extended/2, start/1, coerce/1, null/0, checkout/1]).
3+
-export([query/4, query_extended/2, start/1, coerce/1, null/0, checkout/2]).
44

55
-include_lib("pog/include/pog_Config.hrl").
66
-include_lib("pg_types/include/pg_types.hrl").
@@ -86,7 +86,7 @@ query(Pool, Sql, Arguments, Timeout) ->
8686
Res = case Pool of
8787
{single_connection, Conn} ->
8888
pgo_handler:extended_query(Conn, Sql, Arguments, #{});
89-
{pool, Name} ->
89+
{pool, Name, _CheckoutTimeout} ->
9090
Options = #{
9191
pool => Name,
9292
pool_options => [{timeout, Timeout}]
@@ -110,8 +110,8 @@ query_extended(Conn, Sql) ->
110110
{error, convert_error(Error)}
111111
end.
112112

113-
checkout(Name) when is_atom(Name) ->
114-
case pgo:checkout(Name) of
113+
checkout(Name, Timeout) when is_atom(Name) ->
114+
case pgo:checkout(Name, [{timeout, Timeout}]) of
115115
{ok, Ref, Conn} -> {ok, {Ref, Conn}};
116116
{error, Error} -> {error, convert_error(Error)}
117117
end.

0 commit comments

Comments
 (0)