sqeduler 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|