diff --git a/lib/puma/plugin/solid_queue.rb b/lib/puma/plugin/solid_queue.rb index 8a7aea28b..a0acfdbba 100644 --- a/lib/puma/plugin/solid_queue.rb +++ b/lib/puma/plugin/solid_queue.rb @@ -33,6 +33,11 @@ def start_forked(launcher) @solid_queue_pid = fork do Thread.new { monitor_puma } SolidQueue::Supervisor.start(mode: :fork) + + # Same as Processes::Supervised#create_fork: skip at-exit + # finalization, which can deadlock on database handles when a + # thread was killed while inside a query + exit!(0) end end @@ -43,6 +48,11 @@ def start_forked(launcher) @solid_queue_pid = fork do Thread.new { monitor_puma } start_solid_queue(mode: :fork) + + # Same as Processes::Supervised#create_fork: skip at-exit + # finalization, which can deadlock on database handles when a + # thread was killed while inside a query + exit!(0) end end diff --git a/lib/solid_queue/processes/supervised.rb b/lib/solid_queue/processes/supervised.rb index 5638026d2..9ad02be66 100644 --- a/lib/solid_queue/processes/supervised.rb +++ b/lib/solid_queue/processes/supervised.rb @@ -29,6 +29,14 @@ def create_fork(&block) fork do register_signal_handlers block.call + + # Exit skipping at-exit hooks and finalizers, like Puma's cluster + # workers do: Ruby would finalize every object still alive in the + # fork, including SQLite database handles whose mutex can be left + # locked when a thread is killed while waiting in SQLite's busy + # handler, deadlocking the exit. Everything the process needs to do + # on shutdown has already run by now. + exit!(0) end end diff --git a/test/dummy/config/puma_async.rb b/test/dummy/config/puma_async.rb index beb652595..925bde385 100644 --- a/test/dummy/config/puma_async.rb +++ b/test/dummy/config/puma_async.rb @@ -13,10 +13,6 @@ # worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port ENV.fetch("PORT") { 3000 } - # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } diff --git a/test/dummy/config/puma_fork.rb b/test/dummy/config/puma_fork.rb index 4cdbbfd1d..08422974f 100644 --- a/test/dummy/config/puma_fork.rb +++ b/test/dummy/config/puma_fork.rb @@ -13,10 +13,6 @@ # worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port ENV.fetch("PORT") { 3000 } - # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } diff --git a/test/unit/supervised_test.rb b/test/unit/supervised_test.rb new file mode 100644 index 000000000..d79ccd1fc --- /dev/null +++ b/test/unit/supervised_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "test_helper" + +class SupervisedTest < ActiveSupport::TestCase + class FakeProcess + include SolidQueue::Processes::Supervised + + def stop + end + end + + test "forked processes exit immediately, without running inherited at-exit hooks" do + reader, writer = IO.pipe + + pid = FakeProcess.new.send(:create_fork) do + at_exit { writer.write("at_exit ran") } + writer.write("block ran") + end + + writer.close + _, status = Process.waitpid2(pid) + + assert_equal 0, status.exitstatus + assert_equal "block ran", reader.read + ensure + reader.close + end +end