diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c1726e..998106b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ruby uses: ruby/setup-ruby@v1 @@ -28,21 +28,27 @@ jobs: bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake') $(git ls-files '*.thor') test: + name: Ruby ${{ matrix.ruby }}, Sidekiq ${{ matrix.sidekiq }}, ${{ matrix.redis }} runs-on: ubuntu-latest timeout-minutes: 5 services: redis: - image: redis + image: ${{ matrix.redis }} ports: - 6379:6379 strategy: matrix: - ruby: ["3.1", "3.2", "3.3"] + ruby: ["3.0", "3.1", "3.2", "3.3"] + redis: ["redis", "valkey/valkey"] + sidekiq: ["6.5", "7.0", "7.1", "7.2", "7.3", "8.0", "8.1"] + + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/sidekiq-${{ matrix.sidekiq }}.gemfile steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ruby uses: ruby/setup-ruby@v1 @@ -59,7 +65,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Release Gem uses: discourse/publish-rubygems-action@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b2667e..69b31b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 0.19.0 - 2025-12-04 + +- Fix race condition in `MiniScheduler::Manager` + +# 0.18.0 - 2024-12-20 + +- Add support for Sidekiq 7+ +- Remove support for Sidekiq < 6.5 +- Update minimum Ruby version to 3.0 + # 0.17.0 - 2024-08-06 - Add `MiniScheduler::Manager.discover_running_scheduled_jobs` API to allow running scheduled jobs to easily be discovered on the diff --git a/gemfiles/sidekiq-6.5.gemfile b/gemfiles/sidekiq-6.5.gemfile new file mode 100644 index 0000000..74a077b --- /dev/null +++ b/gemfiles/sidekiq-6.5.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 6.5.0" diff --git a/gemfiles/sidekiq-7.0.gemfile b/gemfiles/sidekiq-7.0.gemfile new file mode 100644 index 0000000..2e43d33 --- /dev/null +++ b/gemfiles/sidekiq-7.0.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 7.0.0" diff --git a/gemfiles/sidekiq-7.1.gemfile b/gemfiles/sidekiq-7.1.gemfile new file mode 100644 index 0000000..1bf9eca --- /dev/null +++ b/gemfiles/sidekiq-7.1.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 7.1.0" diff --git a/gemfiles/sidekiq-7.2.gemfile b/gemfiles/sidekiq-7.2.gemfile new file mode 100644 index 0000000..a93c3fb --- /dev/null +++ b/gemfiles/sidekiq-7.2.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 7.2.0" diff --git a/gemfiles/sidekiq-7.3.gemfile b/gemfiles/sidekiq-7.3.gemfile new file mode 100644 index 0000000..0e9dc69 --- /dev/null +++ b/gemfiles/sidekiq-7.3.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 7.3.0" diff --git a/gemfiles/sidekiq-8.0.gemfile b/gemfiles/sidekiq-8.0.gemfile new file mode 100644 index 0000000..3cc8105 --- /dev/null +++ b/gemfiles/sidekiq-8.0.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 8.0.0" diff --git a/gemfiles/sidekiq-8.1.gemfile b/gemfiles/sidekiq-8.1.gemfile new file mode 100644 index 0000000..6140cd1 --- /dev/null +++ b/gemfiles/sidekiq-8.1.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec path: ".." + +gem "sidekiq", "~> 8.1.0" diff --git a/lib/mini_scheduler.rb b/lib/mini_scheduler.rb index fbf217b..962a6ca 100644 --- a/lib/mini_scheduler.rb +++ b/lib/mini_scheduler.rb @@ -5,6 +5,7 @@ require "mini_scheduler/manager" require "mini_scheduler/distributed_mutex" require "sidekiq" +require "redis" begin require "sidekiq/exception_handler" @@ -16,26 +17,23 @@ def self.configure yield self end - class SidekiqExceptionHandler - if defined?(Sidekiq::ExceptionHandler) - extend Sidekiq::ExceptionHandler - else - def self.handle_exception(exception, context) - Sidekiq.handle_exception(exception, context) - end + SidekiqExceptionHandler = + if defined?(Sidekiq.default_configuration) # Sidekiq 7+ + ->(ex, ctx, _config = nil) { Sidekiq.default_configuration.handle_exception(ex, ctx) } + else # Sidekiq 6.5 + ->(ex, ctx, _config = nil) { Sidekiq.handle_exception(ex, ctx) } end - end def self.job_exception_handler(&blk) @job_exception_handler = blk if blk @job_exception_handler end - def self.handle_job_exception(ex, context = {}) + def self.handle_job_exception(ex, context = {}, _config = nil) if job_exception_handler job_exception_handler.call(ex, context) else - SidekiqExceptionHandler.handle_exception(ex, context) + SidekiqExceptionHandler.call(ex, context) end end diff --git a/lib/mini_scheduler/manager.rb b/lib/mini_scheduler/manager.rb index 1b1cd4a..1295858 100644 --- a/lib/mini_scheduler/manager.rb +++ b/lib/mini_scheduler/manager.rb @@ -28,8 +28,8 @@ def initialize(manager) @keep_alive_thread = Thread.new do while !@stopped - @mutex.synchronize { keep_alive } sleep(@manager.keep_alive_duration / 2) + @mutex.synchronize { keep_alive } end end @@ -348,7 +348,7 @@ def blocking_tick end def stop! - @runner.stop! + @runner&.stop! self.class.current.delete(@queue) end @@ -357,7 +357,7 @@ def keep_alive_duration end def keep_alive(*ids) - ids = [identity_key, *@runner.worker_thread_ids] if ids.size == 0 + ids = [identity_key, *@runner&.worker_thread_ids] if ids.size == 0 ids.each { |identity_key| redis.setex identity_key, keep_alive_duration, "" } end @@ -394,10 +394,10 @@ def self.hostname begin require "socket" Socket.gethostname - rescue => e + rescue StandardError begin `hostname`.strip - rescue => e + rescue StandardError "unknown_host" end end diff --git a/lib/mini_scheduler/version.rb b/lib/mini_scheduler/version.rb index bc91acd..216a946 100644 --- a/lib/mini_scheduler/version.rb +++ b/lib/mini_scheduler/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module MiniScheduler - VERSION = "0.17.0" + VERSION = "0.18.0" end diff --git a/lib/mini_scheduler/web.rb b/lib/mini_scheduler/web.rb index 1634ffe..e40ef73 100644 --- a/lib/mini_scheduler/web.rb +++ b/lib/mini_scheduler/web.rb @@ -18,22 +18,24 @@ def self.find_schedules_by_time end end - def self.registered(app) - app.helpers do - def sane_time(time) - return unless time - time - end + module Helpers + def sane_time(time) + return unless time + time + end - def sane_duration(duration) - return unless duration - if duration < 1000 - "#{duration}ms" - else - "#{"%.2f" % (duration / 1000.0)} secs" - end + def sane_duration(duration) + return unless duration + if duration < 1000 + "#{duration}ms" + else + "#{"%.2f" % (duration / 1000.0)} secs" end end + end + + def self.registered(app) + app.helpers Helpers app.get "/scheduler" do MiniScheduler.before_sidekiq_web_request&.call @@ -72,5 +74,5 @@ def sane_duration(duration) end end -Sidekiq::Web.register(MiniScheduler::Web) +Sidekiq::Web.configure {|cfg| cfg.register(MiniScheduler::Web, name: nil, tab: nil, index: nil) } Sidekiq::Web.tabs["Scheduler"] = "scheduler" diff --git a/mini_scheduler.gemspec b/mini_scheduler.gemspec index 67e7423..fdc86a9 100644 --- a/mini_scheduler.gemspec +++ b/mini_scheduler.gemspec @@ -15,17 +15,17 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/discourse/mini_scheduler" spec.license = "MIT" - spec.required_ruby_version = ">= 2.7.0" + spec.required_ruby_version = ">= 3.0.0" spec.files = `git ls-files`.split($/).reject { |s| s =~ /^(spec|\.)/ } spec.require_paths = ["lib"] - spec.add_runtime_dependency "sidekiq", ">= 4.2.3", "< 7.0" + spec.add_runtime_dependency "sidekiq", ">= 6.5", "< 9.0" spec.add_development_dependency "pg", "~> 1.0" spec.add_development_dependency "activesupport", "~> 7.0" spec.add_development_dependency "rspec", "~> 3.0" - spec.add_development_dependency "mocha", "~> 2.0" + spec.add_development_dependency "mocha", "~> 3.0" spec.add_development_dependency "guard", "~> 2.0" spec.add_development_dependency "guard-rspec", "~> 4.0" spec.add_development_dependency "redis", ">= 4.0" diff --git a/spec/mini_scheduler/manager_spec.rb b/spec/mini_scheduler/manager_spec.rb index 5491a99..606ee35 100644 --- a/spec/mini_scheduler/manager_spec.rb +++ b/spec/mini_scheduler/manager_spec.rb @@ -409,16 +409,19 @@ def queued_jobs(manager, with_hostname:) def expect_job_failure(ex, ctx) expect(ex).to be_kind_of ZeroDivisionError - expect(ctx).to match( - { message: "Error while running a scheduled job", job: { "class" => Testing::FailingJob } }, - ) + expect(ctx).to match a_hash_including( + message: "Error while running a scheduled job", + job: { + "class" => Testing::FailingJob, + }, + ) end context "with default handler" do class TempSidekiqLogger attr_accessor :exception, :context - def call(ex, ctx) + def call(ex, ctx, _config = nil) self.exception = ex self.context = ctx end @@ -426,9 +429,17 @@ def call(ex, ctx) let(:logger) { TempSidekiqLogger.new } - before { Sidekiq.error_handlers << logger } + let(:error_handlers) do + if defined?(Sidekiq.default_configuration) + Sidekiq.default_configuration.error_handlers + else + Sidekiq.error_handlers + end + end + + before { error_handlers << logger } - after { Sidekiq.error_handlers.delete(logger) } + after { error_handlers.delete(logger) } it "captures failed jobs" do manager.blocking_tick @@ -438,7 +449,9 @@ def call(ex, ctx) end context "with custom handler" do - before { MiniScheduler.job_exception_handler { |ex, ctx| expect_job_failure(ex, ctx) } } + before do + MiniScheduler.job_exception_handler { |ex, ctx, _config = nil| expect_job_failure(ex, ctx) } + end after { MiniScheduler.instance_variable_set :@job_exception_handler, nil } @@ -447,4 +460,11 @@ def call(ex, ctx) end end end + + describe "#keep_alive" do + it "does not raise an error when `skip_runner` is true" do + manager = MiniScheduler::Manager.new(enable_stats: false, skip_runner: true) + expect { manager.keep_alive }.not_to raise_error + end + end end