From c3d2aabd0a19d20bb1f2b2fa2e5b30f03b043aff Mon Sep 17 00:00:00 2001 From: Ivan Dzyzenko Date: Sat, 16 May 2026 13:57:32 +0200 Subject: [PATCH 1/3] Fix incomplete transaction commit when thread is shutdown ungracefully --- lib/pg/connection.rb | 22 +++++++++++++++------- spec/pg/connection_spec.rb | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/lib/pg/connection.rb b/lib/pg/connection.rb index eb607ab82..58cf3646e 100644 --- a/lib/pg/connection.rb +++ b/lib/pg/connection.rb @@ -311,17 +311,25 @@ def transaction yield(self) rescue PG::RollbackTransaction rollback = true - cancel if transaction_status == PG::PQTRANS_ACTIVE - block - exec "ROLLBACK" + perform_rollback rescue Exception rollback = true - cancel if transaction_status == PG::PQTRANS_ACTIVE - block - exec "ROLLBACK" + perform_rollback raise ensure - exec "COMMIT" unless rollback + unless rollback + if Thread.current.status == "aborting" + perform_rollback + else + exec("COMMIT") + end + end + end + + private def perform_rollback + cancel if transaction_status == PG::PQTRANS_ACTIVE + block + exec("ROLLBACK") end ### Returns an array of Hashes with connection defaults. See ::conndefaults diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index 7763493a2..f7f018de4 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -1073,6 +1073,41 @@ end expect( res ).to eq( "transaction result" ) end + + context "when current thread gets halted in the middle of the running transaction" do + + it "rollbacks the transaction" do + # abort the per-example transaction so we can test our own + @conn.exec('ROLLBACK') + @conn.exec("CREATE TABLE pie ( flavor TEXT )") + + begin + q = Thread::Queue.new + thread = Thread.new do + conn = $pg_server.connect + conn.transaction do + conn.exec("INSERT INTO pie VALUES ('rhubarb'), ('cherry'), ('schizophrenia')") + q << 1 + # emulate some load that keeps the transaction open + conn.exec("select pg_sleep(2)") + end + ensure + conn&.close + end + # Wait for the thread to reach the testing point + q.pop + # Terminate the thread ungracefully and wait it to die + thread.exit + thread.join + + res = @conn.exec("SELECT * FROM pie") + expect(res.ntuples).to eq(0) + ensure + thread.exit + @conn.exec("DROP TABLE pie") + end + end + end end describe "large objects" do From 9ca08c473cd2d288260c822f2136a0bbcef38c8e Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Sat, 16 May 2026 15:08:28 +0200 Subject: [PATCH 2/3] Simplify Connection#transaction --- lib/pg/connection.rb | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/pg/connection.rb b/lib/pg/connection.rb index 58cf3646e..e45a851e8 100644 --- a/lib/pg/connection.rb +++ b/lib/pg/connection.rb @@ -306,32 +306,21 @@ class << self # and a +COMMIT+ at the end of the block, or # +ROLLBACK+ if any exception occurs. def transaction - rollback = false exec "BEGIN" yield(self) - rescue PG::RollbackTransaction - rollback = true - perform_rollback - rescue Exception - rollback = true - perform_rollback + rescue PG::RollbackTransaction => error + rescue Exception => error raise ensure - unless rollback - if Thread.current.status == "aborting" - perform_rollback - else - exec("COMMIT") - end + if error || Thread.current.status == "aborting" + cancel if transaction_status == PG::PQTRANS_ACTIVE + block + exec("ROLLBACK") + else + exec("COMMIT") end end - private def perform_rollback - cancel if transaction_status == PG::PQTRANS_ACTIVE - block - exec("ROLLBACK") - end - ### Returns an array of Hashes with connection defaults. See ::conndefaults ### for details. def conndefaults From 2a11006fd9e8ecf1c79759d587064d7ce015bf39 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Sat, 16 May 2026 17:22:41 +0200 Subject: [PATCH 3/3] Fix truffleruby compat in new test case It seems to be a Truffleruby bug, that blocking IO reverts Thread#status back to "run". So avoid blocking IO, but use Kernel.sleep instead. See here for a similar issue on JRuby: https://github.com/jruby/jruby/issues/4705 --- spec/pg/connection_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index f7f018de4..fdae1b2b7 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -1089,7 +1089,7 @@ conn.exec("INSERT INTO pie VALUES ('rhubarb'), ('cherry'), ('schizophrenia')") q << 1 # emulate some load that keeps the transaction open - conn.exec("select pg_sleep(2)") + sleep 2 end ensure conn&.close