From f50decafc33b2f5aba9bca15e5077617f47c8ede Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 13 Mar 2026 09:32:24 +0100 Subject: [PATCH] fix: return query_timeout instead of closed on deadline When a pool deadline fires during query execution, the socket is closed and recv returns {error, closed}. This is indistinguishable from actual connection failures. Now detect deadline-triggered disconnects by checking if the holder ETS table was deleted, and return {error, query_timeout} instead. Closes #96 --- src/pgo.erl | 15 ++++++++++++++- test/pgo_SUITE.erl | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/pgo.erl b/src/pgo.erl index d08a259..267edf0 100644 --- a/src/pgo.erl +++ b/src/pgo.erl @@ -101,9 +101,16 @@ query(Query, Params, Options) -> Pool = maps:get(pool, Options, default), PoolOptions = maps:get(pool_options, Options, []), case checkout(Pool, PoolOptions) of - {ok, Ref, Conn} -> + {ok, Ref={_, _, _, Holder}, Conn} -> try query(Query, Params, Options, Conn) + of + {error, closed} -> + maybe_timeout_error(Holder); + {error, einval} -> + maybe_timeout_error(Holder); + Result -> + Result after checkin(Ref, Conn) end; @@ -230,6 +237,12 @@ checkin(Ref, Conn) -> break(Conn) -> pgo_connection:break(Conn). +maybe_timeout_error(Holder) -> + case ets:info(Holder, size) of + undefined -> {error, query_timeout}; + _ -> {error, closed} + end. + format_error(Error=#{module := Module}) -> Module:format_error(Error); format_error(Error) -> diff --git a/test/pgo_SUITE.erl b/test/pgo_SUITE.erl index e12c634..2bfe863 100644 --- a/test/pgo_SUITE.erl +++ b/test/pgo_SUITE.erl @@ -15,7 +15,8 @@ Until(I) -> case X of true -> ok; false -> timer:sleep(10), Until(I+1) end end)(0)). all() -> [checkout_checkin, checkout_break, recheckout, kill_socket, kill_pid, - checkout_kill, checkout_disconnect, checkout_query_crash]. + checkout_kill, checkout_disconnect, checkout_query_crash, + query_timeout]. init_per_suite(Config) -> Config. @@ -27,7 +28,8 @@ init_per_testcase(T, Config) when T =:= checkout_break ; T =:= checkout_query_crash ; T =:= recheckout ; T =:= kill_socket ; - T =:= kill_pid -> + T =:= kill_pid ; + T =:= query_timeout -> Pool = list_to_atom("pool_" ++ atom_to_list(T)), application:ensure_all_started(pgo), pgo_sup:start_child(Pool, #{pool_size => 1, @@ -242,3 +244,12 @@ checkout_query_crash(Config) -> end), ok. + +query_timeout(Config) -> + Name = ?config(pool_name, Config), + %% Use a very short deadline so the query times out + Result = pgo:query("SELECT pg_sleep(5)", [], + #{pool => Name, + pool_options => [{timeout, 500}]}), + ?assertEqual({error, query_timeout}, Result), + ok.