sqeduler 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+ require "benchmark"
3
+ module Sqeduler
4
+ module Worker
5
+ # Basic callbacks for worker events.
6
+ module Callbacks
7
+ def perform(*args)
8
+ before_start
9
+ duration = Benchmark.realtime { super }
10
+ on_success(duration)
11
+ rescue StandardError => e
12
+ on_failure(e)
13
+ raise
14
+ end
15
+
16
+ private
17
+
18
+ # provides an oppurtunity to log when the job has started (maybe create a
19
+ # stateful db record for this job run?)
20
+ def before_start
21
+ Service.logger.info "Starting #{self.class.name} at #{Time.new.utc} in process ID #{Process.pid}"
22
+ super if defined?(super)
23
+ end
24
+
25
+ # callback for successful run of this job
26
+ def on_success(total_time)
27
+ Service.logger.info "#{self.class.name} completed at #{Time.new.utc}. Total time #{total_time}"
28
+ super if defined?(super)
29
+ end
30
+
31
+ # callback for when failues in this job occur
32
+ def on_failure(e)
33
+ Service.logger.error "#{self.class.name} failed with exception #{e}"
34
+ super if defined?(super)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ module Sqeduler
2
+ module Worker
3
+ # convenience module for including everything
4
+ module Everything
5
+ def self.included(mod)
6
+ mod.prepend Sqeduler::Worker::Synchronization
7
+ mod.prepend Sqeduler::Worker::KillSwitch
8
+ # needs to be the last one
9
+ mod.prepend Sqeduler::Worker::Callbacks
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ module Sqeduler
3
+ module Worker
4
+ # Uses Redis hashes to enabled and disable workers across multiple hosts.
5
+ module KillSwitch
6
+ SIDEKIQ_DISABLED_WORKERS = "sidekiq.disabled-workers"
7
+
8
+ def self.prepended(base)
9
+ if base.ancestors.include?(Sqeduler::Worker::Callbacks)
10
+ fail "Sqeduler::Worker::Callbacks must be the last module that you prepend."
11
+ end
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ # rubocop:disable Style/Documentation
16
+ module ClassMethods
17
+ def enable
18
+ Service.redis_pool.with do |redis|
19
+ redis.hdel(SIDEKIQ_DISABLED_WORKERS, name)
20
+ Service.logger.warn "#{name} has been enabled"
21
+ end
22
+ end
23
+
24
+ def disable
25
+ Service.redis_pool.with do |redis|
26
+ redis.hset(SIDEKIQ_DISABLED_WORKERS, name, Time.now)
27
+ Service.logger.warn "#{name} has been disabled"
28
+ end
29
+ end
30
+
31
+ def disabled?
32
+ Service.redis_pool.with do |redis|
33
+ redis.hexists(SIDEKIQ_DISABLED_WORKERS, name)
34
+ end
35
+ end
36
+
37
+ def enabled?
38
+ !disabled?
39
+ end
40
+ end
41
+ # rubocop:enable Style/Documentation
42
+
43
+ def perform(*args)
44
+ if self.class.disabled?
45
+ Service.logger.warn "#{self.class.name} is currently disabled."
46
+ else
47
+ super
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+ require "benchmark"
3
+ require "active_support/core_ext/class/attribute"
4
+
5
+ module Sqeduler
6
+ module Worker
7
+ # Module that provides common synchronization infrastructure
8
+ # of workers across multiple hosts `Sqeduler::BaseWorker.synchronize_jobs`.
9
+ module Synchronization
10
+ def self.prepended(base)
11
+ if base.ancestors.include?(Sqeduler::Worker::Callbacks)
12
+ fail "Sqeduler::Worker::Callbacks must be the last module that you prepend."
13
+ end
14
+
15
+ base.extend(ClassMethods)
16
+ base.class_attribute :synchronize_jobs_mode
17
+ base.class_attribute :synchronize_jobs_expiration
18
+ base.class_attribute :synchronize_jobs_timeout
19
+ end
20
+
21
+ # rubocop:disable Style/Documentation
22
+ module ClassMethods
23
+ def synchronize(mode, opts = {})
24
+ self.synchronize_jobs_mode = mode
25
+ self.synchronize_jobs_timeout = opts[:timeout] || 5
26
+ self.synchronize_jobs_expiration = opts[:expiration]
27
+ return if synchronize_jobs_expiration
28
+ fail ArgumentError, ":expiration is required!"
29
+ end
30
+ end
31
+ # rubocop:enable Style/Documentation
32
+
33
+ def perform(*args)
34
+ if self.class.synchronize_jobs_mode == :one_at_a_time
35
+ perform_locked(sync_lock_key(*args)) do
36
+ perform_timed do
37
+ super
38
+ end
39
+ end
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def sync_lock_key(*args)
48
+ if args.empty?
49
+ self.class.name
50
+ else
51
+ "#{self.class.name}-#{args.join}"
52
+ end
53
+ end
54
+
55
+ # callback for when a lock cannot be obtained
56
+ def on_lock_timeout(key)
57
+ Service.logger.warn(
58
+ "#{self.class.name} unable to acquire lock '#{key}'. Aborting."
59
+ )
60
+ super if defined?(super)
61
+ end
62
+
63
+ # callback for when the job expiration is too short, less < time it took
64
+ # perform the actual work
65
+ SCHEDULE_COLLISION_MARKER = "%s took %s but has an expiration of %p sec. Beware of race conditions!".freeze
66
+ def on_schedule_collision(duration)
67
+ Service.logger.warn(
68
+ format(
69
+ SCHEDULE_COLLISION_MARKER,
70
+ self.class.name,
71
+ time_duration(duration),
72
+ self.class.synchronize_jobs_expiration
73
+ )
74
+ )
75
+ super if defined?(super)
76
+ end
77
+
78
+ def perform_timed(&block)
79
+ duration = Benchmark.realtime(&block)
80
+ on_schedule_collision(duration) if duration > self.class.synchronize_jobs_expiration
81
+ end
82
+
83
+ def perform_locked(sync_lock_key, &work)
84
+ RedisLock.with_lock(
85
+ sync_lock_key,
86
+ :expiration => self.class.synchronize_jobs_expiration,
87
+ :timeout => self.class.synchronize_jobs_timeout,
88
+ &work
89
+ )
90
+ rescue RedisLock::LockTimeoutError
91
+ on_lock_timeout(sync_lock_key)
92
+ end
93
+
94
+ # rubocop:disable Metrics/AbcSize
95
+ def time_duration(timespan)
96
+ rest, secs = timespan.divmod(60) # self is the time difference t2 - t1
97
+ rest, mins = rest.divmod(60)
98
+ days, hours = rest.divmod(24)
99
+
100
+ result = []
101
+ result << "#{days} Days" if days > 0
102
+ result << "#{hours} Hours" if hours > 0
103
+ result << "#{mins} Minutes" if mins > 0
104
+ result << "#{secs.round(2)} Seconds" if secs > 0
105
+ result.join(" ")
106
+ end
107
+ # rubocop:enable Metrics/AbcSize
108
+ end
109
+ end
110
+ end
data/lib/sqeduler.rb ADDED
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ require "sqeduler/version"
3
+ require "sqeduler/config"
4
+ require "sqeduler/redis_scripts"
5
+ require "sqeduler/lock_value"
6
+ require "sqeduler/redis_lock"
7
+ require "sqeduler/trigger_lock"
8
+ require "sqeduler/service"
9
+ require "sqeduler/worker/callbacks"
10
+ require "sqeduler/worker/synchronization"
11
+ require "sqeduler/worker/kill_switch"
12
+ require "sqeduler/worker/everything"
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ RSpec.describe Sqeduler::Config do
5
+ describe "#initialize" do
6
+ subject do
7
+ described_class.new(options)
8
+ end
9
+
10
+ let(:options) do
11
+ {
12
+ :logger => double,
13
+ :schedule_path => "/tmp/schedule.yaml",
14
+ :redis_hash => {
15
+ :host => "localhost",
16
+ :db => 1
17
+ }
18
+ }.merge(extras)
19
+ end
20
+
21
+ let(:extras) { {} }
22
+
23
+ describe "redis_hash" do
24
+ it "should set the redis_hash" do
25
+ expect(subject.redis_hash).to eq(options[:redis_hash])
26
+ end
27
+ end
28
+
29
+ describe "schedule_path" do
30
+ it "should set the schedule_path" do
31
+ expect(subject.schedule_path).to eq(options[:schedule_path])
32
+ end
33
+ end
34
+
35
+ describe "logger" do
36
+ it "should set the logger" do
37
+ expect(subject.logger).to eq(options[:logger])
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ require "sqeduler"
2
+ require_relative "fake_worker"
3
+
4
+ REDIS_CONFIG = {
5
+ :host => "localhost",
6
+ :db => 1
7
+ }
8
+ Sidekiq.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
9
+
10
+ Sqeduler::Service.config = Sqeduler::Config.new(
11
+ :redis_hash => REDIS_CONFIG,
12
+ :logger => Sidekiq.logger,
13
+ :schedule_path => File.expand_path(File.dirname(__FILE__)) + "/schedule.yaml",
14
+ :on_server_start => proc do |_config|
15
+ Sqeduler::Service.logger.info "Received on_server_start callback"
16
+ end,
17
+ :on_client_start => proc do |_config|
18
+ Sqeduler::Service.logger.info "Received on_client_start callback"
19
+ end
20
+ )
21
+ Sqeduler::Service.start
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ # Sample worker for specs
3
+ class FakeWorker
4
+ JOB_RUN_PATH = "/tmp/job_run"
5
+ JOB_BEFORE_START_PATH = "/tmp/job_before_start"
6
+ JOB_SUCCESS_PATH = "/tmp/job_success"
7
+ JOB_FAILURE_PATH = "/tmp/job_failure"
8
+ JOB_LOCK_FAILURE_PATH = "/tmp/lock_failure"
9
+ SCHEDULE_COLLISION_PATH = "/tmp/schedule_collision"
10
+ include Sidekiq::Worker
11
+ include Sqeduler::Worker::Everything
12
+
13
+ def perform(sleep_time = 0.1)
14
+ long_process(sleep_time)
15
+ end
16
+
17
+ private
18
+
19
+ def long_process(sleep_time)
20
+ sleep sleep_time
21
+ log_event(JOB_RUN_PATH)
22
+ end
23
+
24
+ def log_event(file_path)
25
+ File.open(file_path, "a+") { |f| f.write "1" }
26
+ end
27
+
28
+ def on_success(_total_duration)
29
+ log_event(JOB_SUCCESS_PATH)
30
+ end
31
+
32
+ def on_failure(_e)
33
+ log_event(JOB_FAILURE_PATH)
34
+ end
35
+
36
+ def before_start
37
+ log_event(JOB_BEFORE_START_PATH)
38
+ end
39
+
40
+ def on_lock_timeout(_key)
41
+ log_event(JOB_LOCK_FAILURE_PATH)
42
+ end
43
+
44
+ def on_schedule_collision(_duration)
45
+ log_event(SCHEDULE_COLLISION_PATH)
46
+ end
47
+ end
@@ -0,0 +1,2 @@
1
+ FakeWorker:
2
+ every: 5s
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+ require "./spec/fixtures/fake_worker"
3
+
4
+ def maybe_cleanup_file(file_path)
5
+ File.delete(file_path) if File.exist?(file_path)
6
+ end
7
+
8
+ RSpec.describe "Sidekiq integration" do
9
+ before do
10
+ maybe_cleanup_file(FakeWorker::JOB_RUN_PATH)
11
+ maybe_cleanup_file(FakeWorker::JOB_SUCCESS_PATH)
12
+ maybe_cleanup_file(FakeWorker::JOB_FAILURE_PATH)
13
+ maybe_cleanup_file(FakeWorker::JOB_LOCK_FAILURE_PATH)
14
+ maybe_cleanup_file(FakeWorker::JOB_BEFORE_START_PATH)
15
+ maybe_cleanup_file(FakeWorker::SCHEDULE_COLLISION_PATH)
16
+ end
17
+
18
+ it "should start sidekiq, schedule FakeWorker, and verify that it ran" do
19
+ path = File.expand_path(File.dirname(__FILE__)) + "/fixtures/env.rb"
20
+ pid = Process.spawn "bundle exec sidekiq -r #{path}"
21
+ puts "Spawned process #{pid}"
22
+ timeout = 30
23
+ start = Time.new
24
+ while (Time.new - start) < timeout
25
+ break if File.exist?(FakeWorker::JOB_RUN_PATH)
26
+ sleep 0.5
27
+ end
28
+ Process.kill("INT", pid)
29
+ Process.wait(pid, 0)
30
+ expect(File).to exist(FakeWorker::JOB_RUN_PATH)
31
+ end
32
+ end
@@ -0,0 +1,172 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ RSpec.describe Sqeduler::Service do
5
+ let(:logger) do
6
+ Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
7
+ end
8
+
9
+ describe ".start" do
10
+ subject { described_class.start }
11
+
12
+ context "no config provided" do
13
+ it "should raise" do
14
+ expect { subject }.to raise_error
15
+ end
16
+ end
17
+
18
+ context "config provided" do
19
+ let(:schedule_filepath) { Pathname.new("./spec/fixtures/schedule.yaml") }
20
+ let(:server_receiver) { double }
21
+ let(:client_receiver) { double }
22
+ before do
23
+ allow(server_receiver).to receive(:call)
24
+ allow(client_receiver).to receive(:call)
25
+
26
+ described_class.config = Sqeduler::Config.new(
27
+ :redis_hash => REDIS_CONFIG,
28
+ :logger => logger,
29
+ :schedule_path => schedule_filepath,
30
+ :on_server_start => proc { |config| server_receiver.call(config) },
31
+ :on_client_start => proc { |config| client_receiver.call(config) }
32
+ )
33
+ end
34
+
35
+ it "starts the server" do
36
+ expect(Sidekiq).to receive(:configure_server)
37
+ subject
38
+ end
39
+
40
+ it "starts the client" do
41
+ expect(Sidekiq).to receive(:configure_client)
42
+ subject
43
+ end
44
+
45
+ it "calls the appropriate on_server_start callbacks" do
46
+ allow(Sidekiq).to receive(:server?).and_return(true)
47
+ expect(server_receiver).to receive(:call)
48
+ subject
49
+ end
50
+
51
+ it "calls the appropriate on_client_start callbacks" do
52
+ expect(client_receiver).to receive(:call)
53
+ subject
54
+ end
55
+
56
+ context "a schedule_path is provided" do
57
+ it "starts the scheduler" do
58
+ expect(Sidekiq).to receive(:"schedule=").with(YAML.load_file(schedule_filepath))
59
+ subject
60
+ expect(Sidekiq::Scheduler.rufus_scheduler_options).to have_key(:trigger_lock)
61
+ expect(Sidekiq::Scheduler.rufus_scheduler_options[:trigger_lock]).to be_kind_of(
62
+ Sqeduler::TriggerLock
63
+ )
64
+ end
65
+
66
+ context "a schedule_path is a string" do
67
+ let(:schedule_filepath) { "./spec/fixtures/schedule.yaml" }
68
+
69
+ it "starts the scheduler" do
70
+ expect(Sidekiq).to receive(:"schedule=").with(YAML.load_file(schedule_filepath))
71
+ subject
72
+ expect(Sidekiq::Scheduler.rufus_scheduler_options).to have_key(:trigger_lock)
73
+ expect(Sidekiq::Scheduler.rufus_scheduler_options[:trigger_lock]).to be_kind_of(
74
+ Sqeduler::TriggerLock
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ context "a schedule_path is not provided" do
81
+ let(:schedule_filepath) { nil }
82
+
83
+ it "does not start the scheduler" do
84
+ expect(Sidekiq).to_not receive(:"schedule=")
85
+ subject
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ describe ".redis_pool" do
92
+ subject { described_class.redis_pool }
93
+
94
+ before do
95
+ described_class.config = Sqeduler::Config.new.tap do |config|
96
+ config.redis_hash = REDIS_CONFIG
97
+ config.logger = logger
98
+ end
99
+ end
100
+
101
+ it "creates a connection pool" do
102
+ expect(subject).to be_kind_of(ConnectionPool)
103
+ end
104
+
105
+ it "is memoized" do
106
+ pool_1 = described_class.redis_pool
107
+ pool_2 = described_class.redis_pool
108
+ expect(pool_1.object_id).to eq(pool_2.object_id)
109
+ end
110
+
111
+ it "is not Sidekiq.redis" do
112
+ described_class.start
113
+ expect(Sidekiq.redis_pool.object_id).to_not eq(subject.object_id)
114
+ end
115
+
116
+ context "redis version is too low" do
117
+ before do
118
+ allow_any_instance_of(Redis).to receive(:info).and_return(
119
+ "redis_version" => "2.6.11"
120
+ )
121
+ if described_class.instance_variable_defined?(:@redis_pool)
122
+ described_class.remove_instance_variable(:@redis_pool)
123
+ end
124
+
125
+ if described_class.instance_variable_defined?(:@verified)
126
+ described_class.remove_instance_variable(:@verified)
127
+ end
128
+ end
129
+
130
+ it "should raise" do
131
+ expect { described_class.redis_pool }.to raise_error
132
+ end
133
+ end
134
+ end
135
+
136
+ describe ".logger" do
137
+ subject { described_class.logger }
138
+
139
+ before do
140
+ described_class.config = Sqeduler::Config.new.tap do |config|
141
+ config.logger = logger
142
+ end
143
+ end
144
+
145
+ context "provided in config" do
146
+ it "return the config value" do
147
+ expect(subject).to eq(logger)
148
+ end
149
+ end
150
+
151
+ context "no config provided" do
152
+ let(:logger) { nil }
153
+
154
+ it "should raise ArgumentError" do
155
+ expect { subject }.to raise_error(ArgumentError)
156
+ end
157
+
158
+ context "in a Rails app" do
159
+ let(:logger) { double }
160
+ before do
161
+ rails = double
162
+ stub_const("Rails", rails)
163
+ allow(rails).to receive(:logger).and_return(logger)
164
+ end
165
+
166
+ it "should use the Rails logger" do
167
+ expect(subject).to eq(logger)
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+ require "pry"
3
+ require "rspec"
4
+ require "sqeduler"
5
+ require "timecop"
6
+
7
+ REDIS_CONFIG = {
8
+ :host => "localhost",
9
+ :db => 1
10
+ }
11
+ TEST_REDIS = Redis.new(REDIS_CONFIG)
12
+
13
+ Timecop.safe_mode = true
14
+
15
+ RSpec.configure do |config|
16
+ config.before(:each) do
17
+ TEST_REDIS.flushdb
18
+ end
19
+
20
+ config.disable_monkey_patching!
21
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+ require "sqeduler"
5
+
6
+ RSpec.describe Sqeduler do
7
+ it "should have a VERSION constant" do
8
+ expect(subject.const_get("VERSION")).not_to be_empty
9
+ end
10
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ RSpec.describe Sqeduler::TriggerLock do
5
+ context "#lock" do
6
+ subject { described_class.new.lock }
7
+
8
+ before do
9
+ Sqeduler::Service.config = Sqeduler::Config.new(
10
+ :redis_hash => REDIS_CONFIG,
11
+ :logger => Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
12
+ )
13
+ end
14
+
15
+ let(:trigger_lock_1) { described_class.new }
16
+ let(:trigger_lock_2) { described_class.new }
17
+
18
+ it "should get the lock" do
19
+ lock_successes = [trigger_lock_1, trigger_lock_2].map do |trigger_lock|
20
+ Thread.new { trigger_lock.lock }
21
+ end.map(&:value)
22
+
23
+ expect(lock_successes).to match_array([true, false])
24
+ end
25
+
26
+ it "should not be the owner if the lock has expired" do
27
+ allow(trigger_lock_1).to receive(:expiration_milliseconds).and_return(1000)
28
+ expect(trigger_lock_1.lock).to be true
29
+ expect(trigger_lock_1.locked?).to be true
30
+ sleep 1
31
+ expect(trigger_lock_1.locked?).to be false
32
+ end
33
+
34
+ it "should refresh the lock expiration time when it is the owner" do
35
+ allow(trigger_lock_1).to receive(:expiration_milliseconds).and_return(1000)
36
+ expect(trigger_lock_1.lock).to be true
37
+ sleep 1.1
38
+ expect(trigger_lock_1.locked?).to be false
39
+ expect(trigger_lock_1.refresh).to be true
40
+ end
41
+
42
+ it "should not refresh the lock when it is not owner" do
43
+ threads = []
44
+ threads << Thread.new do
45
+ allow(trigger_lock_1).to receive(:expiration_milliseconds).and_return(1000)
46
+ trigger_lock_1.lock
47
+ sleep 1
48
+ end
49
+ threads << Thread.new do
50
+ sleep 1.1
51
+ trigger_lock_2.lock
52
+ end
53
+ threads.each(&:join)
54
+ expect(trigger_lock_2.locked?).to be(true)
55
+ expect(trigger_lock_1.refresh).to be(false)
56
+ end
57
+
58
+ it "should release the lock when it is the owner" do
59
+ expect(trigger_lock_1.lock).to be true
60
+ expect(trigger_lock_1.unlock).to be true
61
+ expect(trigger_lock_1.locked?).to be false
62
+ end
63
+
64
+ it "should not release the lock when it is not the owner" do
65
+ threads = []
66
+ threads << Thread.new do
67
+ allow(trigger_lock_1).to receive(:expiration_milliseconds).and_return(1000)
68
+ trigger_lock_1.lock
69
+ sleep 1
70
+ end
71
+ threads << Thread.new do
72
+ sleep 1.1
73
+ trigger_lock_2.lock
74
+ end
75
+ threads.each(&:join)
76
+ expect(trigger_lock_2.locked?).to be(true)
77
+ expect(trigger_lock_1.unlock).to be(false)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Sqeduler::Worker::Synchronization do
4
+ describe ".synchronize" do
5
+ before do
6
+ stub_const(
7
+ "ParentWorker",
8
+ Class.new do
9
+ prepend Sqeduler::Worker::Synchronization
10
+ synchronize :one_at_a_time, :expiration => 10, :timeout => 1
11
+ end
12
+ )
13
+
14
+ stub_const("ChildWorker", Class.new(ParentWorker))
15
+ end
16
+
17
+ it "should preserve the synchronize attributes" do
18
+ expect(ChildWorker.synchronize_jobs_mode).to eq(:one_at_a_time)
19
+ expect(ChildWorker.synchronize_jobs_expiration).to eq(10)
20
+ expect(ChildWorker.synchronize_jobs_timeout).to eq(1)
21
+ end
22
+
23
+ it "should allow the child class to update the synchronize attributes" do
24
+ ChildWorker.synchronize :one_at_a_time, :expiration => 20, :timeout => 2
25
+ expect(ChildWorker.synchronize_jobs_mode).to eq(:one_at_a_time)
26
+ expect(ChildWorker.synchronize_jobs_expiration).to eq(20)
27
+ expect(ChildWorker.synchronize_jobs_timeout).to eq(2)
28
+ expect(ParentWorker.synchronize_jobs_mode).to eq(:one_at_a_time)
29
+ expect(ParentWorker.synchronize_jobs_expiration).to eq(10)
30
+ expect(ParentWorker.synchronize_jobs_timeout).to eq(1)
31
+ end
32
+ end
33
+ end