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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/CHANGES.md +4 -0
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +54 -0
- data/README.md +161 -0
- data/Rakefile +12 -0
- data/lib/sqeduler/config.rb +16 -0
- data/lib/sqeduler/lock_value.rb +23 -0
- data/lib/sqeduler/redis_lock.rb +112 -0
- data/lib/sqeduler/redis_scripts.rb +63 -0
- data/lib/sqeduler/service.rb +100 -0
- data/lib/sqeduler/trigger_lock.rb +20 -0
- data/lib/sqeduler/version.rb +4 -0
- data/lib/sqeduler/worker/callbacks.rb +38 -0
- data/lib/sqeduler/worker/everything.rb +13 -0
- data/lib/sqeduler/worker/kill_switch.rb +52 -0
- data/lib/sqeduler/worker/synchronization.rb +110 -0
- data/lib/sqeduler.rb +12 -0
- data/spec/config_spec.rb +41 -0
- data/spec/fixtures/env.rb +21 -0
- data/spec/fixtures/fake_worker.rb +47 -0
- data/spec/fixtures/schedule.yaml +2 -0
- data/spec/integration_spec.rb +32 -0
- data/spec/service_spec.rb +172 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/sqeduler_spec.rb +10 -0
- data/spec/trigger_lock_spec.rb +80 -0
- data/spec/worker/synchronization_spec.rb +33 -0
- data/spec/worker_spec.rb +275 -0
- data/sqeduler.gemspec +33 -0
- metadata +233 -0
@@ -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"
|
data/spec/config_spec.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|