Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
services:
redis:
image: redis:8-alpine
ports:
- 16379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 1s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
bundler-cache: true
- run: bundle exec rake
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Gemfile.lock
*.gem
gemfiles/
coverage/
7 changes: 0 additions & 7 deletions .travis.yml

This file was deleted.

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Check [the code](lib/sidekiq/repeat/mini_ice_cube.rb) for documentation.
# setup
bundle install

# Start Redis in another terminal (or use an existing instance)
docker run --rm -p 127.0.0.1:16379:6379 redis:8-alpine

# Run the tests
bundle exec rake test

Expand Down
2 changes: 2 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ Rake::TestTask.new(:test) do |test|
test.libs << 'test'
test.pattern = 'test/**/*.rb'
end

task default: :test
4 changes: 2 additions & 2 deletions lib/sidekiq/repeat/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def reset_to_default!

def self.with_lock
if instance.redlock_enabled
Redlock::Client.new(instance.redlock_redis_instances).lock('sidekiq-repeat-reschedule-all', 500) do
yield
Redlock::Client.new(instance.redlock_redis_instances).lock('sidekiq-repeat-reschedule-all', 500) do |lock_info|
yield if lock_info
end
else
yield
Expand Down
15 changes: 9 additions & 6 deletions sidekiq-repeat.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ Gem::Specification.new do |spec|
spec.files = Dir['lib/**/*rb']
spec.require_paths = ['lib']

spec.add_dependency 'sidekiq', '>= 6', '< 7.0'
spec.add_dependency 'parse-cron', '~> 0.1'
spec.add_dependency 'redlock', '~> 1'
spec.add_dependency 'sidekiq', '~> 8', '< 9'
spec.add_dependency 'parse-cron', '~> 0.1.4'
spec.add_dependency 'redlock', '~> 2'
# Sidekiq 6.5 requires these libraries without declaring them as gems.
spec.add_dependency 'base64', '~> 0.3'
spec.add_dependency 'logger', '~> 1.7'

spec.add_development_dependency 'minitest', '~> 3'
spec.add_development_dependency 'rake', '>= 12.3.3'
spec.add_development_dependency 'redis-namespace', '~> 1.3'
spec.add_development_dependency 'minitest', '~> 5.25'
spec.add_development_dependency 'rake', '~> 13.3'
spec.add_development_dependency 'simplecov', '~> 1.0'
end
5 changes: 2 additions & 3 deletions test/mini_ice_cube.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'minitest/autorun'
require 'sidekiq/repeat/mini_ice_cube'
require_relative 'test_helper'

class TestMiniIceCube < MiniTest::Unit::TestCase
class TestMiniIceCube < Minitest::Test
def setup
@dsl = Sidekiq::Repeat::MiniIceCube::MainDsl.new
end
Expand Down
30 changes: 17 additions & 13 deletions test/repeat.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
require 'minitest/autorun'
require_relative 'test_helper'

require_relative './test_helper.rb'

class TestRescheduling < MiniTest::Unit::TestCase
class TestRescheduling < Minitest::Test
include TestHelper.assertions('SidekiqRepeatTestJob')
include TestHelper.application_setup

Expand Down Expand Up @@ -53,7 +51,7 @@ def test_reschedules_job_if_in_the_future
end
end

class TestArguments < MiniTest::Unit::TestCase
class TestArguments < Minitest::Test
include TestHelper.assertions('SidekiqRepeatArgumentsTestJob', true)
include TestHelper.application_setup

Expand All @@ -64,16 +62,20 @@ def test_perform_called_with_parameters
end
end

class TestRedlockDefaultConfiguration < MiniTest::Unit::TestCase
class TestRedlockDefaultConfiguration < Minitest::Test
include TestHelper.assertions('SidekiqRepeatTestJob')
include TestHelper.application_setup(false)

def test_startup_scheduling_is_locked
expect_redlock! { startup_sidekiq! }
with_redlock_held { startup_sidekiq! }
assert_not_scheduled

startup_sidekiq!
assert_scheduled
end
end

class TestRedlockDisabled < MiniTest::Unit::TestCase
class TestRedlockDisabled < Minitest::Test
include TestHelper.assertions('SidekiqRepeatTestJob')
include TestHelper.application_setup(false)

Expand All @@ -82,19 +84,21 @@ def configure(config)
end

def test_startup_scheduling_is_not_locked
expect_no_redlock! { startup_sidekiq! }
with_redlock_held { startup_sidekiq! }
assert_scheduled
end
end

class TestRedlockMultipleRedisInstances < MiniTest::Unit::TestCase
class TestRedlockMultipleRedisInstances < Minitest::Test
include TestHelper.assertions('SidekiqRepeatTestJob')
include TestHelper.application_setup(false)

def configure(config)
config.redlock_redis_instances = ['redis://1.2.3.4/', 'redis://5.6.7.8/']
config.redlock_redis_instances = [Sidekiq.redis_pool, TestHelper.second_redis_pool]
end

def test_startup_scheduling_is_not_locked
expect_redlock!(['redis://1.2.3.4/', 'redis://5.6.7.8/']) { startup_sidekiq! }
def test_startup_scheduling_is_locked
with_redlock_held(TestHelper.second_redis_pool) { startup_sidekiq! }
assert_not_scheduled
end
end
102 changes: 60 additions & 42 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
# This test setup was taken from sidekiq-middleware:
# https://github.com/krasnoukhov/sidekiq-middleware/blob/v0.3.0/test/test_unique_jobs.rb

require 'simplecov'
SimpleCov.start do
cover 'lib/**/*.rb'
end

require 'minitest/autorun'
require 'sidekiq'
require 'sidekiq/cli'
require 'sidekiq/processor'
require 'sidekiq/redis_connection'
require 'minitest/mock'

Sidekiq.testing!(:disable)
Sidekiq.logger.level = Logger::ERROR
Sidekiq.redis = Sidekiq::RedisConnection.create(:namespace => 'sidekiq-repeat-test')
configure_sidekiq = proc do |config|
config.redis = {
url: ENV.fetch('TEST_REDIS_URL', 'redis://127.0.0.1:16379')
}
end
Sidekiq.configure_client(&configure_sidekiq)
Sidekiq.configure_server(&configure_sidekiq)

require 'sidekiq-repeat'

Sidekiq::Testing.server_middleware do |chain|
chain.add Sidekiq::Repeat::Middleware
end

class SidekiqRepeatTestJob
include Sidekiq::Worker
include Sidekiq::Repeat::Repeatable
Expand All @@ -33,13 +50,17 @@ def perform(last, current)
end
end

UnitOfWork = Struct.new(:queue, :job) do
def acknowledge; end
def queue_name; end
def requeue; end
end

module TestHelper
LOCK_KEY = 'sidekiq-repeat-reschedule-all'

def self.second_redis_pool
@second_redis_pool ||= begin
primary_db = Sidekiq.redis { |redis| redis.config.db }
client = RedisClient.config(url: ENV.fetch('TEST_REDIS_URL', 'redis://127.0.0.1:16379'), db: primary_db == 1 ? 0 : 1)
client.new_pool(size: 1)
end
end

def self.assertions(klass, perform_with_arguments = false)
Module.new do
# NOTE: For some reason, we need to use define_method here, as otherwise `klass`
Expand Down Expand Up @@ -80,64 +101,61 @@ def delete_scheduled!
scheduled_jobs.map(&:delete)
end

# Enqueues a job in memory with +fake!+, then executes it with +perform_one+, the purpose is to only use public
# Sidekiq API. During execution, testing is disabled, so the middleware schedules the next occurrence in Redis.
def perform_scheduled!
msg = Sidekiq.dump_json('class' => klass_name, 'queue' => 'default', 'args' => perform_args)
work = UnitOfWork.new('default', msg)
actor = MiniTest::Mock.new
actor.expect(:processor_done, nil, [@processor])
2.times { @boss.expect(:async, actor, []) }
@processor.send(:process, work)
worker = Object.const_get(klass_name)
Sidekiq::Testing.fake! { worker.perform_async(*perform_args) }
worker.perform_one
end

def expect_redlock!(redis_instances = nil)
redis_instances ||= Sidekiq::Repeat::Configuration.instance.redlock_redis_instances
def with_redlock_held(redis = Sidekiq.redis_pool)
client = Redlock::Client.new([redis], retry_count: 0)
lock = client.lock(TestHelper::LOCK_KEY, 30_000)
raise 'Could not acquire test lock' unless lock

@redlock_client_instance = MiniTest::Mock.new
@redlock_client_instance.expect(:lock, nil, ['sidekiq-repeat-reschedule-all', 500])

@redlock_client_new_method = MiniTest::Mock.new
@redlock_client_new_method.expect(:call, @redlock_client_instance, [redis_instances])

Redlock::Client.stub(:new, @redlock_client_new_method) do
yield # to test case.
end

@redlock_client_new_method.verify
@redlock_client_instance.verify
end

def expect_no_redlock!
@redlock_client_new_method = Proc.new { flunk 'Redlock::Client::new should not be called' }
Redlock::Client.stub(:new, @redlock_client_new_method) do
yield
end
yield
ensure
client.unlock(lock) if lock
end
end

module ApplicationSetup
def run
Time.stub(:now, Time.local(2030, 1, 2, 12, 10, 30)) { super }
end

def configure(config)
# To be overwritten in test class.
end

def setup
clear_test_redis
Sidekiq::Repeat::Repeatable.repeatables.each do |klass|
klass.repeat { hourly }
klass.instance_variable_set(:@cronline, nil)
klass.instance_variable_set(:@ss, nil)
end
Sidekiq::Repeat::Configuration.instance.redlock_redis_instances = [Sidekiq.redis_pool]
# Allow the test to configure Sidekiq::Repeat.
Sidekiq::Repeat.configure { |config| configure(config) }

@boss = MiniTest::Mock.new
2.times { @boss.expect(:options, {:queues => ['default'] }, []) }
@processor = Sidekiq::Processor.new(@boss, queues: ['default'])
startup_sidekiq! if startup_sidekiq
end

def teardown
clear_test_redis
# Reset to defaults for next test case.
Sidekiq::Repeat::Configuration.instance.reset_to_default!
end

def startup_sidekiq!
events = Sidekiq.options[:lifecycle_events][:startup].dup
@processor.fire_event(:startup)
Sidekiq.options[:lifecycle_events][:startup] = events
Sidekiq.default_configuration[:lifecycle_events][:startup].each(&:call)
end

def clear_test_redis
Sidekiq.redis(&:flushdb)
TestHelper.second_redis_pool.with { |redis| redis.call('DEL', TestHelper::LOCK_KEY) }
end
end
end
Loading