From b3eb65886e70ecfcb96c06b3eeb8269d06ce7f4e Mon Sep 17 00:00:00 2001 From: felipe stival Date: Tue, 17 Mar 2026 21:10:24 -0300 Subject: [PATCH 1/3] Don't wait for ReadyForQuery after FATAL errors When PostgreSQL sends a FATAL or PANIC ErrorResponse, it closes the connection immediately without sending ReadyForQuery. Postgrex unconditionally waited for ReadyForQuery, which would hit tcp_closed and return a generic disconnect error, discarding the original FATAL error. --- lib/postgrex/protocol.ex | 6 ++++++ test/query_test.exs | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/postgrex/protocol.ex b/lib/postgrex/protocol.ex index 773be678..c80716b5 100644 --- a/lib/postgrex/protocol.ex +++ b/lib/postgrex/protocol.ex @@ -3068,6 +3068,12 @@ defmodule Postgrex.Protocol do end end + defp error_ready(s, _status, %Postgrex.Error{postgres: %{severity: severity}} = err, buffer) + when severity in ["FATAL", "PANIC"] do + %{connection_id: connection_id} = s + {:disconnect, %{err | connection_id: connection_id}, %{s | buffer: buffer}} + end + defp error_ready(s, status, %Postgrex.Error{} = err, buffer) do case recv_ready(s, status, buffer) do {:ok, s} -> diff --git a/test/query_test.exs b/test/query_test.exs index 2820627a..761bfb1f 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -1940,6 +1940,26 @@ defmodule QueryTest do end) =~ "** (Postgrex.Error) FATAL 57P01 (admin_shutdown)" end + test "terminate backend during query returns FATAL error", context do + assert {:ok, pid} = P.start_link([idle_interval: 10] ++ context[:options]) + + %Postgrex.Result{connection_id: connection_id} = Postgrex.query!(pid, "SELECT 42", []) + + # Start a long-running query in a separate process so we can terminate the + # backend while it's executing. + task = + Task.async(fn -> + Postgrex.query(pid, "SELECT pg_sleep(10)", []) + end) + + Process.sleep(100) + + assert [[true]] = query("SELECT pg_terminate_backend($1)", [connection_id]) + + assert {:error, %Postgrex.Error{postgres: %{code: :admin_shutdown, severity: "FATAL"}}} = + Task.await(task, 5000) + end + test "terminate backend with socket", context do Process.flag(:trap_exit, true) socket = System.get_env("PG_SOCKET_DIR") || "/tmp" From d744d33176287813aeee5fc3b964f4bf1bea2a9b Mon Sep 17 00:00:00 2001 From: felipe stival Date: Wed, 18 Mar 2026 09:22:38 -0300 Subject: [PATCH 2/3] refactor test to use tracing instead of sleep --- test/query_test.exs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/query_test.exs b/test/query_test.exs index 761bfb1f..d1db0c0d 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -1945,19 +1945,29 @@ defmodule QueryTest do %Postgrex.Result{connection_id: connection_id} = Postgrex.query!(pid, "SELECT 42", []) - # Start a long-running query in a separate process so we can terminate the - # backend while it's executing. task = Task.async(fn -> + receive do + :go -> :ok + end + Postgrex.query(pid, "SELECT pg_sleep(10)", []) end) - Process.sleep(100) + :erlang.trace(task.pid, true, [:call]) + :erlang.trace_pattern({Postgrex.Protocol, :recv_bind, :_}, [], [:local]) + + send(task.pid, :go) + + assert_receive {:trace, _, :call, {Postgrex.Protocol, :recv_bind, _}}, 200 assert [[true]] = query("SELECT pg_terminate_backend($1)", [connection_id]) assert {:error, %Postgrex.Error{postgres: %{code: :admin_shutdown, severity: "FATAL"}}} = Task.await(task, 5000) + after + :erlang.trace_pattern({Postgrex.Protocol, :recv_bind, :_}, false, []) + :erlang.trace(:all, false, [:call]) end test "terminate backend with socket", context do From 2fd60cd2e45b33da76a88c9ce7b2f5a9cdad0024 Mon Sep 17 00:00:00 2001 From: felipe stival Date: Wed, 18 Mar 2026 09:23:09 -0300 Subject: [PATCH 3/3] wait for disconnect or ready for query --- lib/postgrex/protocol.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/postgrex/protocol.ex b/lib/postgrex/protocol.ex index c80716b5..0256f14a 100644 --- a/lib/postgrex/protocol.ex +++ b/lib/postgrex/protocol.ex @@ -3068,20 +3068,19 @@ defmodule Postgrex.Protocol do end end - defp error_ready(s, _status, %Postgrex.Error{postgres: %{severity: severity}} = err, buffer) - when severity in ["FATAL", "PANIC"] do + defp error_ready(s, status, %Postgrex.Error{} = err, buffer) do %{connection_id: connection_id} = s - {:disconnect, %{err | connection_id: connection_id}, %{s | buffer: buffer}} - end - defp error_ready(s, status, %Postgrex.Error{} = err, buffer) do case recv_ready(s, status, buffer) do {:ok, s} -> - %{connection_id: connection_id} = s {:error, %{err | connection_id: connection_id}, s} {:disconnect, _, _} = disconnect -> - disconnect + if err.postgres.severity in ["FATAL", "PANIC"] do + {:disconnect, %{err | connection_id: connection_id}, %{s | buffer: buffer}} + else + disconnect + end end end