From 0e5561b4c710d81a7bcdf826a4c47120b882af0d Mon Sep 17 00:00:00 2001 From: "Hal M. Spitz" Date: Sat, 16 Nov 2024 16:11:04 -0800 Subject: [PATCH 1/4] Minor housekeeping * Adds to RVM .ruby-gemset to .gitignore for RVM and gemset holdouts. * Updates Gemfile.lock to include arm64-darwin-24 (weird that it's missing) * Adds 4 development dependencies to quiet deprecation warnings * Updates rubocop to allow Ruby 3.3 syntax, matching .ruby-version --- .gitignore | 3 +++ .rubocop.yml | 2 +- Gemfile.lock | 7 +++++++ solid_queue.gemspec | 3 +++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 147c05f2..3cbf5eae 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,8 @@ # Folder for Visual Studio Code /.vscode/ +# Files for RVM holdouts +.ruby-gemset + # misc .DS_Store diff --git a/.rubocop.yml b/.rubocop.yml index 0269c78b..999ad5f5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,7 +4,7 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml } AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.3 Exclude: - "test/dummy/db/schema.rb" - "test/dummy/db/queue_schema.rb" diff --git a/Gemfile.lock b/Gemfile.lock index 8c030422..1a296ef2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,6 +61,7 @@ GEM erubi (1.13.0) et-orbi (1.2.11) tzinfo + fiddle (1.1.2) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -74,6 +75,7 @@ GEM reline (>= 0.4.2) json (2.8.2) language_server-protocol (3.17.0.3) + logger (1.6.0) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -90,6 +92,7 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + ostruct (0.6.0) parallel (1.26.3) parser (3.3.6.0) ast (~> 2.4.1) @@ -175,14 +178,18 @@ GEM PLATFORMS arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-darwin-21 x86_64-darwin-23 x86_64-linux DEPENDENCIES debug + fiddle + logger mocha mysql2 + ostruct pg puma rubocop-rails-omakase diff --git a/solid_queue.gemspec b/solid_queue.gemspec index 7faeabfa..ed3f5c4e 100644 --- a/solid_queue.gemspec +++ b/solid_queue.gemspec @@ -44,4 +44,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "pg" spec.add_development_dependency "sqlite3" spec.add_development_dependency "rubocop-rails-omakase" + spec.add_development_dependency "ostruct" + spec.add_development_dependency "logger" + spec.add_development_dependency "fiddle" end From 6cc550edadd9c497bfddd257c86627bf2af4894e Mon Sep 17 00:00:00 2001 From: "Hal M. Spitz" Date: Sun, 17 Nov 2024 15:20:07 -0800 Subject: [PATCH 2/4] Silence test output for expected exceptions SolidQueue has excellent built-in error reporting. While this is fantastic for SQ users, it is less than ideal for testing SolidQueue because any test that deliberately uses or triggers an exception produces voluminous error reporting. This error reporting is hugely valuable when the exception is not expected, but distracting and of limited value for expected use-cases, especially when the test confirms the correct outcomes via assertions. This commit adds: * A generic test-specific Exception class: ExpectedTestError This allows testing for specific exceptions while retaining all error reporting infrastructure for unexpected exceptions. * Two helper methods for silencing on_thread_error output These methods accept an Exception or Array(Exception) and simply does not call the output mechanism if the exception passed to on_thread_error matches. This way, any unexpected error during test still reports in a highly visible manner while the exceptions being tested are validated via assertions. * Replaces the stock on_thread_error with one that ignores ExpectedTextError. Updated several tests from using the ruby stock RuntimeError to ExpectedTestError. * Configures tests to run with YJIT enabled This is to test under likely production deployment configuration, not for performance reasons. Note: With the very recent reporting on M4's crashing on Mac's with YJIT enabled, we might want to either defer this change or add a conditional to opt in until the problem is resolved. --- Gemfile.lock | 3 ++ solid_queue.gemspec | 1 + test/dummy/config/initializers/enable_yjit.rb | 12 +++++++ test/integration/concurrency_controls_test.rb | 2 +- test/integration/instrumentation_test.rb | 8 ++--- test/integration/jobs_lifecycle_test.rb | 8 +++-- test/integration/processes_lifecycle_test.rb | 6 ++-- .../solid_queue/failed_execution_test.rb | 16 +++++---- test/test_helper.rb | 33 +++++++++++++++++++ test/unit/worker_test.rb | 10 +++--- 10 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 test/dummy/config/initializers/enable_yjit.rb diff --git a/Gemfile.lock b/Gemfile.lock index 1a296ef2..a19f86f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -130,6 +130,8 @@ GEM rake (13.2.1) rdoc (6.8.1) psych (>= 4.0.0) + rdoc (6.6.3.1) + psych (>= 4.0.0) regexp_parser (2.9.2) reline (0.5.12) io-console (~> 0.5) @@ -192,6 +194,7 @@ DEPENDENCIES ostruct pg puma + rdoc rubocop-rails-omakase solid_queue! sqlite3 diff --git a/solid_queue.gemspec b/solid_queue.gemspec index ed3f5c4e..227ad5b6 100644 --- a/solid_queue.gemspec +++ b/solid_queue.gemspec @@ -47,4 +47,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "ostruct" spec.add_development_dependency "logger" spec.add_development_dependency "fiddle" + spec.add_development_dependency "rdoc" end diff --git a/test/dummy/config/initializers/enable_yjit.rb b/test/dummy/config/initializers/enable_yjit.rb new file mode 100644 index 00000000..5367a5de --- /dev/null +++ b/test/dummy/config/initializers/enable_yjit.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Ideally, tests should be configured as close to production settings as +# possible and YJIT is likely to be enabled. While it's highly unlikely +# YJIT would cause issues, enabling it confirms this assertion. +# +# Configured via initializer to align with Rails 7.1 default in gemspec +if defined?(RubyVM::YJIT.enable) + Rails.application.config.after_initialize do + RubyVM::YJIT.enable + end +end diff --git a/test/integration/concurrency_controls_test.rb b/test/integration/concurrency_controls_test.rb index e181e4ca..dbce706d 100644 --- a/test/integration/concurrency_controls_test.rb +++ b/test/integration/concurrency_controls_test.rb @@ -85,7 +85,7 @@ class ConcurrencyControlsTest < ActiveSupport::TestCase test "run several jobs over the same record sequentially, with some of them failing" do ("A".."F").each_with_index do |name, i| # A, C, E will fail, for i= 0, 2, 4 - SequentialUpdateResultJob.perform_later(@result, name: name, pause: 0.2.seconds, exception: (RuntimeError if i.even?)) + SequentialUpdateResultJob.perform_later(@result, name: name, pause: 0.2.seconds, exception: (ExpectedTestError if i.even?)) end ("G".."K").each do |name| diff --git a/test/integration/instrumentation_test.rb b/test/integration/instrumentation_test.rb index c90d161a..59443ccf 100644 --- a/test/integration/instrumentation_test.rb +++ b/test/integration/instrumentation_test.rb @@ -162,7 +162,7 @@ class InstrumentationTest < ActiveSupport::TestCase test "errors when deregistering processes are included in deregister_process events" do previous_thread_report_on_exception, Thread.report_on_exception = Thread.report_on_exception, false - error = RuntimeError.new("everything is broken") + error = ExpectedTestError.new("everything is broken") SolidQueue::Process.any_instance.expects(:destroy!).raises(error).at_least_once events = subscribed("deregister_process.solid_queue") do @@ -182,7 +182,7 @@ class InstrumentationTest < ActiveSupport::TestCase end test "retrying failed job emits retry event" do - RaisingJob.perform_later(RuntimeError, "A") + RaisingJob.perform_later(ExpectedTestError, "A") job = SolidQueue::Job.last worker = SolidQueue::Worker.new.tap(&:start) @@ -198,7 +198,7 @@ class InstrumentationTest < ActiveSupport::TestCase end test "retrying failed jobs in bulk emits retry_all" do - 3.times { RaisingJob.perform_later(RuntimeError, "A") } + 3.times { RaisingJob.perform_later(ExpectedTestError, "A") } AddToBufferJob.perform_later("A") jobs = SolidQueue::Job.last(4) @@ -392,7 +392,7 @@ class InstrumentationTest < ActiveSupport::TestCase test "thread errors emit thread_error events" do previous_thread_report_on_exception, Thread.report_on_exception = Thread.report_on_exception, false - error = RuntimeError.new("everything is broken") + error = ExpectedTestError.new("everything is broken") SolidQueue::ClaimedExecution::Result.expects(:new).raises(error).at_least_once AddToBufferJob.perform_later "hey!" diff --git a/test/integration/jobs_lifecycle_test.rb b/test/integration/jobs_lifecycle_test.rb index e1b713ee..1740f760 100644 --- a/test/integration/jobs_lifecycle_test.rb +++ b/test/integration/jobs_lifecycle_test.rb @@ -4,11 +4,13 @@ class JobsLifecycleTest < ActiveSupport::TestCase setup do + SolidQueue.on_thread_error = silent_on_thread_error_for([ ExpectedTestError, RaisingJob::DefaultError ]) @worker = SolidQueue::Worker.new(queues: "background", threads: 3) @dispatcher = SolidQueue::Dispatcher.new(batch_size: 10, polling_interval: 0.2) end teardown do + SolidQueue.on_thread_error = @on_thread_error @worker.stop @dispatcher.stop @@ -29,8 +31,8 @@ class JobsLifecycleTest < ActiveSupport::TestCase end test "enqueue and run jobs that fail without retries" do - RaisingJob.perform_later(RuntimeError, "A") - RaisingJob.perform_later(RuntimeError, "B") + RaisingJob.perform_later(ExpectedTestError, "A") + RaisingJob.perform_later(ExpectedTestError, "B") jobs = SolidQueue::Job.last(2) @dispatcher.start @@ -38,7 +40,7 @@ class JobsLifecycleTest < ActiveSupport::TestCase wait_for_jobs_to_finish_for(3.seconds) - message = "raised RuntimeError for the 1st time" + message = "raised ExpectedTestError for the 1st time" assert_equal [ "A: #{message}", "B: #{message}" ], JobBuffer.values.sort assert_empty SolidQueue::Job.finished diff --git a/test/integration/processes_lifecycle_test.rb b/test/integration/processes_lifecycle_test.rb index 5d5c2072..b96c452d 100644 --- a/test/integration/processes_lifecycle_test.rb +++ b/test/integration/processes_lifecycle_test.rb @@ -144,11 +144,11 @@ class ProcessesLifecycleTest < ActiveSupport::TestCase test "process some jobs that raise errors" do 2.times { enqueue_store_result_job("no error", :background) } 2.times { enqueue_store_result_job("no error", :default) } - error1 = enqueue_store_result_job("error", :background, exception: RuntimeError) + error1 = enqueue_store_result_job("error", :background, exception: ExpectedTestError) enqueue_store_result_job("no error", :background, pause: 0.03) - error2 = enqueue_store_result_job("error", :background, exception: RuntimeError, pause: 0.05) + error2 = enqueue_store_result_job("error", :background, exception: ExpectedTestError, pause: 0.05) 2.times { enqueue_store_result_job("no error", :default, pause: 0.01) } - error3 = enqueue_store_result_job("error", :default, exception: RuntimeError) + error3 = enqueue_store_result_job("error", :default, exception: ExpectedTestError) wait_for_jobs_to_finish_for(2.second, except: [ error1, error2, error3 ]) diff --git a/test/models/solid_queue/failed_execution_test.rb b/test/models/solid_queue/failed_execution_test.rb index 7b142991..c2299b8a 100644 --- a/test/models/solid_queue/failed_execution_test.rb +++ b/test/models/solid_queue/failed_execution_test.rb @@ -7,7 +7,7 @@ class SolidQueue::FailedExecutionTest < ActiveSupport::TestCase end test "run job that fails" do - RaisingJob.perform_later(RuntimeError, "A") + RaisingJob.perform_later(ExpectedTestError, "A") @worker.start assert_equal 1, SolidQueue::FailedExecution.count @@ -15,15 +15,17 @@ class SolidQueue::FailedExecutionTest < ActiveSupport::TestCase end test "run job that fails with a SystemStackError (stack level too deep)" do - InfiniteRecursionJob.perform_later - @worker.start + silence_on_thread_error_for(SystemStackError) do + InfiniteRecursionJob.perform_later + @worker.start - assert_equal 1, SolidQueue::FailedExecution.count - assert SolidQueue::Job.last.failed? + assert_equal 1, SolidQueue::FailedExecution.count + assert SolidQueue::Job.last.failed? + end end test "retry failed job" do - RaisingJob.perform_later(RuntimeError, "A") + RaisingJob.perform_later(ExpectedTestError, "A") @worker.start assert_difference -> { SolidQueue::FailedExecution.count }, -1 do @@ -34,7 +36,7 @@ class SolidQueue::FailedExecutionTest < ActiveSupport::TestCase end test "retry failed jobs in bulk" do - 1.upto(5) { |i| RaisingJob.perform_later(RuntimeError, i) } + 1.upto(5) { |i| RaisingJob.perform_later(ExpectedTestError, i) } 1.upto(3) { |i| AddToBufferJob.perform_later(i) } @worker.start diff --git a/test/test_helper.rb b/test/test_helper.rb index 176cb6e1..5f1bebbb 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -24,11 +24,20 @@ def write(...) end Logger::LogDevice.prepend(BlockLogDeviceTimeoutExceptions) +class ExpectedTestError < RuntimeError; end + class ActiveSupport::TestCase include ProcessesTestHelper, JobsTestHelper + setup do + # Could be cleaner with one several minitest gems, but didn't want to add new dependency + @_on_thread_error = SolidQueue.on_thread_error + SolidQueue.on_thread_error = silent_on_thread_error_for(ExpectedTestError) + end + teardown do + SolidQueue.on_thread_error = @_on_thread_error JobBuffer.clear if SolidQueue.supervisor_pidfile && File.exist?(SolidQueue.supervisor_pidfile) @@ -69,4 +78,28 @@ def wait_while_with_timeout!(timeout, &block) def skip_active_record_query_cache(&block) SolidQueue::Record.uncached(&block) end + + # Silences specified exceptions during the execution of a block + # + # @param [Exception, Array] expected an Exception or an array of Exceptions to ignore + # @yield Executes the provided block with specified exception(s) silenced + def silence_on_thread_error_for(expected, &block) + SolidQueue.with(on_thread_error: silent_on_thread_error_for(expected)) do + block.call + end + end + + # Does not call on_thread_error for expected exceptions + # @param [Exception, Array] expected an Exception or an array of Exceptions to ignore + def silent_on_thread_error_for(expected) + current_proc = SolidQueue.on_thread_error + + ->(exception) do + expected_exceptions = Array(expected) + + unless expected_exceptions.any? { exception.instance_of?(_1) } + current_proc.call(exception) + end + end + end end diff --git a/test/unit/worker_test.rb b/test/unit/worker_test.rb index 09999808..d511cf74 100644 --- a/test/unit/worker_test.rb +++ b/test/unit/worker_test.rb @@ -28,14 +28,14 @@ class WorkerTest < ActiveSupport::TestCase original_on_thread_error, SolidQueue.on_thread_error = SolidQueue.on_thread_error, ->(error) { errors << error.message } previous_thread_report_on_exception, Thread.report_on_exception = Thread.report_on_exception, false - SolidQueue::ReadyExecution.expects(:claim).raises(RuntimeError.new("everything is broken")).at_least_once + SolidQueue::ReadyExecution.expects(:claim).raises(ExpectedTestError.new("everything is broken")).at_least_once AddToBufferJob.perform_later "hey!" worker = SolidQueue::Worker.new(queues: "background", threads: 3, polling_interval: 0.2).tap(&:start) sleep(1) - assert_raises RuntimeError do + assert_raises ExpectedTestError do worker.stop end @@ -51,7 +51,7 @@ class WorkerTest < ActiveSupport::TestCase subscriber = ErrorBuffer.new Rails.error.subscribe(subscriber) - SolidQueue::ClaimedExecution::Result.expects(:new).raises(RuntimeError.new("everything is broken")).at_least_once + SolidQueue::ClaimedExecution::Result.expects(:new).raises(ExpectedTestError.new("everything is broken")).at_least_once AddToBufferJob.perform_later "hey!" @@ -71,7 +71,7 @@ class WorkerTest < ActiveSupport::TestCase subscriber = ErrorBuffer.new Rails.error.subscribe(subscriber) - RaisingJob.perform_later(RuntimeError, "B") + RaisingJob.perform_later(ExpectedTestError, "B") @worker.start @@ -79,7 +79,7 @@ class WorkerTest < ActiveSupport::TestCase @worker.wake_up assert_equal 1, subscriber.errors.count - assert_equal "This is a RuntimeError exception", subscriber.messages.first + assert_equal "This is a ExpectedTestError exception", subscriber.messages.first ensure Rails.error.unsubscribe(subscriber) if Rails.error.respond_to?(:unsubscribe) end From a152f2637e69675612737e7da533ae7f2d4f092e Mon Sep 17 00:00:00 2001 From: "Hal M. Spitz" Date: Thu, 14 Nov 2024 16:20:25 -0800 Subject: [PATCH 3/4] Reimplement Interruptible using Thread#queue * Replaces a little Unix cleverness with a standard Ruby class. This pushes the responsibiity for meeting the SQ requirements from SQ to stock Ruby * Delivers equivelent performance, identical API, and API behaviors with the original implementation (see note below on Futures) * Mostly fixes a *platform / version dependent* issue with MySQL (see below) * Meets 100% of SQ's functional requirements: * interruptible_sleep: a potentially blocking operation interruptible via either a "wake_event" (possibly requested prior to entering interruptible_sleep) or blocking until a timeout. * wake_up / interrupt: a Signal#trap and thread-safe method that does not require user-level synchronization (with the risk of not fully understanding all of the complexities required) code that either interrupts an inflight-interruptible_sleep or enqueues the event to processed in the invocation of interruptible_sleep * Interruptible's API is trivially reproduceable via Thread::Queue * interruptible_sleep => Queue.pop(timeout:) where pushing anything into the queue acts as the interrupt event and timeout is reliable without any extra code or exception handling. * wake_up / interrupt => Queue.push(Anything) is thread, fiber, and Signal.trap safe (can be called from anywhere) and captures all wake_up events whenever requested, automaticall caching any "event" not processed by a currently executing interruptible_sleep matching existing functionality exactly. Why the Future in #interruptible_sleep? While Thread::Queue micro benchmarks as having the same performance on the main thread Vs. any form of a sub-thread (or Fiber) and self-pipe, when running the SQ test suite we see a 35% slow down Vs. the original self-pipe implenentation. One assumes this slowdown would manifest in production. By moving the just the Queue#pop into a separate thread via Concurrent::Promises.future we get +/- identical performance to the original self-pipe implementation. I'm assuming this root causes to Ruby main-thread only housekeeping and/or possibly triggering a fast/slow path issue. Why a Future Vs. Thread#new for each interruptible_sleep call? Every other threaded operation in SQ is implemented using Concurrent Ruby. Using a Future is for code and architectual consistency. There is no difference in performance or functionality between the two. MySQL *only* issues: There seems to be a *platform specific* or *version specific* problem with MySQL database connectivity and/or broken self-pipes leading to randomly failing tests and a stream of distracting backtraces *even with successful* tests. Adding to the complexity sometimes, the lost database connection can self-heal -- HOWEVER -- this takes time and given how much of the test suite has time based assertions, leads to additional random test failures. These, or similar, issues have been observed in the past when changes to the MySQL client library forced changes in the mysql2 gem. With the Thread::Queue based implementation of the Interruptible concern, the random failures and amount of spurious output are dramatically improved (but not eliminated). --- Gemfile.lock | 6 ++--- lib/solid_queue/processes/interruptible.rb | 30 +++++++++------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a19f86f9..0ed3e912 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,7 @@ GEM concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) + date (3.4.1) debug (1.7.1) irb (>= 1.5.0) reline (>= 0.3.1) @@ -98,7 +99,8 @@ GEM ast (~> 2.4.1) racc pg (1.5.4) - psych (5.2.0) + psych (5.2.1) + date stringio puma (6.4.3) nio4r (~> 2.0) @@ -130,8 +132,6 @@ GEM rake (13.2.1) rdoc (6.8.1) psych (>= 4.0.0) - rdoc (6.6.3.1) - psych (>= 4.0.0) regexp_parser (2.9.2) reline (0.5.12) io-console (~> 0.5) diff --git a/lib/solid_queue/processes/interruptible.rb b/lib/solid_queue/processes/interruptible.rb index 67173aeb..11c54a42 100644 --- a/lib/solid_queue/processes/interruptible.rb +++ b/lib/solid_queue/processes/interruptible.rb @@ -7,31 +7,25 @@ def wake_up end private - SELF_PIPE_BLOCK_SIZE = 11 def interrupt - self_pipe[:writer].write_nonblock(".") - rescue Errno::EAGAIN, Errno::EINTR - # Ignore writes that would block and retry - # if another signal arrived while writing - retry + queue << true end def interruptible_sleep(time) - if time > 0 && self_pipe[:reader].wait_readable(time) - loop { self_pipe[:reader].read_nonblock(SELF_PIPE_BLOCK_SIZE) } - end - rescue Errno::EAGAIN, Errno::EINTR + # Since this is invoked on the main thread, using some form of Async + # avoids a 35% slowdown (at least when running the test suite). + # + # Using Futures for architectural consistency with all the other Async in SolidQueue. + Concurrent::Promises.future(time) do |timeout| + if timeout > 0 && queue.pop(timeout:) + queue.clear # exiting the poll wait guarantees testing for SHUTDOWN before next poll + end + end.value end - # Self-pipe for signal-handling (http://cr.yp.to/docs/selfpipe.html) - def self_pipe - @self_pipe ||= create_self_pipe - end - - def create_self_pipe - reader, writer = IO.pipe - { reader: reader, writer: writer } + def queue + @queue ||= Queue.new end end end From 17c4cd5aa9dce02d42cbb60dd08fa3c572af62ac Mon Sep 17 00:00:00 2001 From: "Hal M. Spitz" Date: Tue, 3 Dec 2024 13:11:56 -0800 Subject: [PATCH 4/4] Addresses PR feedback * Streamlined a comment in the code * Removed unneeded gemspec developer_dependencies that we get handled via the upgrade to Rails 7.1.4.1 * Added a gemspec version dependency required for keyword argument support * Rebase to main --- Gemfile.lock | 14 +++++--------- lib/solid_queue/processes/interruptible.rb | 8 +++----- solid_queue.gemspec | 6 ++---- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0ed3e912..2df57956 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,14 +55,13 @@ GEM connection_pool (2.4.1) crass (1.0.6) date (3.4.1) - debug (1.7.1) - irb (>= 1.5.0) - reline (>= 0.3.1) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) drb (2.2.1) erubi (1.13.0) et-orbi (1.2.11) tzinfo - fiddle (1.1.2) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -76,7 +75,7 @@ GEM reline (>= 0.4.2) json (2.8.2) language_server-protocol (3.17.0.3) - logger (1.6.0) + logger (1.6.2) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -93,7 +92,6 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) - ostruct (0.6.0) parallel (1.26.3) parser (3.3.6.0) ast (~> 2.4.1) @@ -186,12 +184,10 @@ PLATFORMS x86_64-linux DEPENDENCIES - debug - fiddle + debug (~> 1.9) logger mocha mysql2 - ostruct pg puma rdoc diff --git a/lib/solid_queue/processes/interruptible.rb b/lib/solid_queue/processes/interruptible.rb index 11c54a42..09c027b6 100644 --- a/lib/solid_queue/processes/interruptible.rb +++ b/lib/solid_queue/processes/interruptible.rb @@ -13,13 +13,11 @@ def interrupt end def interruptible_sleep(time) - # Since this is invoked on the main thread, using some form of Async - # avoids a 35% slowdown (at least when running the test suite). - # - # Using Futures for architectural consistency with all the other Async in SolidQueue. + # Invoking from the main thread can result in a 35% slowdown (at least when running the test suite). + # Using some form of Async (Futures) addresses this performance issue. Concurrent::Promises.future(time) do |timeout| if timeout > 0 && queue.pop(timeout:) - queue.clear # exiting the poll wait guarantees testing for SHUTDOWN before next poll + queue.clear end end.value end diff --git a/solid_queue.gemspec b/solid_queue.gemspec index 227ad5b6..5a4b0de4 100644 --- a/solid_queue.gemspec +++ b/solid_queue.gemspec @@ -37,15 +37,13 @@ Gem::Specification.new do |spec| spec.add_dependency "fugit", "~> 1.11.0" spec.add_dependency "thor", "~> 1.3.1" - spec.add_development_dependency "debug" + spec.add_development_dependency "debug", "~> 1.9" spec.add_development_dependency "mocha" spec.add_development_dependency "puma" spec.add_development_dependency "mysql2" spec.add_development_dependency "pg" spec.add_development_dependency "sqlite3" spec.add_development_dependency "rubocop-rails-omakase" - spec.add_development_dependency "ostruct" - spec.add_development_dependency "logger" - spec.add_development_dependency "fiddle" spec.add_development_dependency "rdoc" + spec.add_development_dependency "logger" end