sqeduler 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 510d5754df3875e36f052b971668cf58ccdcdde1
4
- data.tar.gz: dd0b216fe36b27a4df8f76dbd243e4f088b45fe8
3
+ metadata.gz: 9331eead88bcd85bcd8e564ce22b5de81b3a43ee
4
+ data.tar.gz: b2ae466592349be888d1ff930f344e1b32702587
5
5
  SHA512:
6
- metadata.gz: 64eb353667433b507b725db2b9b92adc5dde0ec71a140ddc640a5c74a52f42dcbfa1dc63ddeba27b4c8ebd0b188711e5256cac9f89d2edd45cff0f5d635d1e7c
7
- data.tar.gz: 1e7cde947da623040ee8f07218fc7b02be92b0c4bec6f54b9688068fc662b090eaadd8f6a3e8466493244aa43ed9e395f552be5443f3dee36bb13a19857c43e0
6
+ metadata.gz: 0b7af459f6a789931ad6a078650449e3efe51641f3b863206a9c99283740bb636010ecc581abd946027773f14fa16f820f1fb6df2cfb90b9476b8f9c2a913109
7
+ data.tar.gz: 416c4d0f6c7059fbd40e5aeb8495b46c25f67f919e314196c1df4a8313dd0475d6ea159c229dede39015b282f9ce987c7078bdbf9894494704096412caf47019
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### 0.3.0 / 2016-01-25
2
+
3
+ * Added lock refresh to maintain exclusive locks until long running jobs finish
4
+
1
5
  ### 0.2.2 / 2015-11-11
2
6
 
3
7
  * Support ERB in job schedules
data/README.md CHANGED
@@ -19,11 +19,10 @@ Provides loosely-coupled helpers for Sidekiq workers. Provides highly available
19
19
 
20
20
  ## Examples
21
21
 
22
- To install this gem with necessary forks:
22
+ To install this gem, add it to your Gemfile:
23
23
 
24
24
  ```ruby
25
25
  gem 'sqeduler'
26
- gem 'sidekiq-scheduler', :github => 'ecin/sidekiq-scheduler', :branch => 'ecin/redis-lock' # https://github.com/Moove-it/sidekiq-scheduler/pull/38
27
26
  ```
28
27
 
29
28
  ### Scheduling
@@ -48,6 +47,8 @@ config.on_server_start = proc {|config| ... }
48
47
  config.on_client_start = proc {|config| ... }
49
48
  # required if you want to start the Sidekiq::Scheduler
50
49
  config.schedule_path = Rails.root.join('config').join('sidekiq_schedule.yml')
50
+ # optional to maintain locks for exclusive jobs, see "Lock Maintainer" below
51
+ config.maintain_locks = true
51
52
 
52
53
  Sqeduler::Service.config = config
53
54
  # Starts Sidekiq and Sidekiq::Scheduler
@@ -57,6 +58,12 @@ Sqeduler::Service.start
57
58
  See documentation for [Sidekiq::Scheduler](https://github.com/Moove-it/sidekiq-scheduler#scheduled-jobs-recurring-jobs)
58
59
  for specifics on how to construct your schedule YAML file.
59
60
 
61
+ ### Lock Maintainer
62
+
63
+ Exclusive locks only last for the expiration you set. If your expiration is 30 seconds and the job runs for 60 seconds, you can have multiple jobs running at once. Rather than having to set absurdly high lock expirations, you can enable the `maintain_locks` option which handles this for you.
64
+
65
+ Every 30 seconds, Sqeduler will look for any exclusive Sidekiq jobs that have been running for more than 30 seconds, and have a lock expiration of more than 30 seconds and refresh the lock.
66
+
60
67
  ### Worker Helpers
61
68
 
62
69
  To use `Sqeduler::Worker` modules:
@@ -3,7 +3,7 @@ module Sqeduler
3
3
  # Simple config for Sqeduler::Service
4
4
  class Config
5
5
  attr_accessor :logger, :redis_hash, :schedule_path,
6
- :on_server_start, :on_client_start
6
+ :on_server_start, :on_client_start, :maintain_locks
7
7
 
8
8
  def initialize(opts = {})
9
9
  self.redis_hash = opts[:redis_hash]
@@ -11,6 +11,7 @@ module Sqeduler
11
11
  self.on_server_start = opts[:on_server_start]
12
12
  self.on_client_start = opts[:on_client_start]
13
13
  self.logger = opts[:logger]
14
+ self.maintain_locks = opts[:maintain_locks]
14
15
  end
15
16
  end
16
17
  end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+ module Sqeduler
3
+ # This is to ensure that if you set your jobs to run one a time and something goes wrong
4
+ # causing a job to run for a long time, your lock won't expire.
5
+ # This doesn't stop long running jobs, it just ensures you only end up with one long running job
6
+ # rather than 20 of them.
7
+ class LockMaintainer
8
+ RUN_INTERVAL = 30
9
+ RUN_JITTER = 1..5
10
+
11
+ def initialize
12
+ @class_with_locks = {}
13
+ end
14
+
15
+ # This is only done when we initialize Sqeduler, don't need to worry about threading
16
+ def run
17
+ @maintainer_thread ||= Thread.new do
18
+ loop do
19
+ begin
20
+ synchronize
21
+ rescue => ex
22
+ Service.logger.error "[SQEDULER LOCK MAINTAINER] #{ex.class}, #{ex.message}"
23
+ end
24
+
25
+ sleep RUN_INTERVAL + rand(RUN_JITTER)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def synchronize
33
+ # Not great, but finding our identity in Sidekiq is a pain, and we already have locks in Sqeduler.
34
+ # Easier to just try and grab a lock each time and whichever server wins gets to do it.
35
+ return unless redis_lock.send(:take_lock)
36
+
37
+ now = Time.now.to_i
38
+
39
+ Service.redis_pool do |redis|
40
+ redis.pipelined do
41
+ workers.each do |_worker, _tid, args|
42
+ # No sense in pinging if it's not been running long enough to matter
43
+ next if (now - args["run_at"]) < RUN_INTERVAL
44
+
45
+ klass = str_to_class(args["payload"]["class"])
46
+ next unless klass
47
+
48
+ lock_key = klass.sync_lock_key(*args["payload"]["args"])
49
+
50
+ # This works because EXPIRE does not recreate the key, it only resets the expiration.
51
+ # We don't have to worry about atomic operations or anything like that.
52
+ # If the job finishes in the interim and deletes the key nothing will happen.
53
+ redis.expire(lock_key, klass.synchronize_jobs_expiration)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # Not all classes will use exclusive locks
60
+ def str_to_class(class_name)
61
+ return @class_with_locks[class_name] unless @class_with_locks[class_name].nil?
62
+
63
+ klass = class_name.constantize
64
+ if klass.respond_to?(:synchronize_jobs_mode)
65
+ # We only care about exclusive jobs that are long running
66
+ if klass.synchronize_jobs_mode == :one_at_a_time && klass.synchronize_jobs_timeout >= RUN_INTERVAL
67
+ return @class_with_locks[class_name] = klass
68
+ end
69
+ end
70
+
71
+ @class_with_locks[class_name] = false
72
+ end
73
+
74
+ def redis_lock
75
+ @redis_lock ||= RedisLock.new("sqeduler-lock-maintainer", :expiration => 60, :timeout => 0)
76
+ end
77
+
78
+ def workers
79
+ @workers ||= Sidekiq::Workers.new
80
+ end
81
+ end
82
+ end
@@ -23,12 +23,13 @@ module Sqeduler
23
23
  def load_sha(redis, script_name)
24
24
  @redis_sha_cache ||= {}
25
25
  @redis_sha_cache[script_name] ||= begin
26
- script = if script_name == :refresh
27
- refresh_lock_script
28
- elsif script_name == :release
29
- release_lock_script
30
- else
31
- fail "No script for #{script_name}"
26
+ script = case script_name
27
+ when :refresh
28
+ refresh_lock_script
29
+ when :release
30
+ release_lock_script
31
+ else
32
+ fail "No script for #{script_name}"
32
33
  end
33
34
  # strip leading whitespace of 8 characters
34
35
  redis.script(:load, script.gsub(/^ {8}/, ""))
@@ -5,7 +5,7 @@ module Sqeduler
5
5
  # Singleton class for configuring this Gem.
6
6
  class Service
7
7
  SCHEDULER_TIMEOUT = 60
8
- MINIMUM_REDIS_VERSION = "2.6.12"
8
+ MINIMUM_REDIS_VERSION = "2.6.12".freeze
9
9
 
10
10
  class << self
11
11
  attr_accessor :config
@@ -44,6 +44,7 @@ module Sqeduler
44
44
  chain.add(Sqeduler::Middleware::KillSwitch)
45
45
  end
46
46
 
47
+ LockMaintainer.new.run if Service.config.maintain_locks
47
48
  Service.config.on_server_start.call(config) if Service.config.on_server_start
48
49
  end
49
50
  end
@@ -104,11 +105,8 @@ module Sqeduler
104
105
 
105
106
  def logger
106
107
  return config.logger if config.logger
107
- if defined?(Rails)
108
- Rails.logger
109
- else
110
- fail ArgumentError, "No logger provided and Rails.logger cannot be inferred"
111
- end
108
+ return Rails.logger if defined?(Rails)
109
+ fail ArgumentError, "No logger provided and Rails.logger cannot be inferred"
112
110
  end
113
111
  end
114
112
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Sqeduler
3
- VERSION = "0.2.2"
3
+ VERSION = "0.3.0".freeze
4
4
  end
@@ -3,7 +3,7 @@ module Sqeduler
3
3
  module Worker
4
4
  # Uses Redis hashes to enabled and disable workers across multiple hosts.
5
5
  module KillSwitch
6
- SIDEKIQ_DISABLED_WORKERS = "sidekiq.disabled-workers"
6
+ SIDEKIQ_DISABLED_WORKERS = "sidekiq.disabled-workers".freeze
7
7
 
8
8
  def self.prepended(base)
9
9
  if base.ancestors.include?(Sqeduler::Worker::Callbacks)
@@ -27,12 +27,20 @@ module Sqeduler
27
27
  return if synchronize_jobs_expiration
28
28
  fail ArgumentError, ":expiration is required!"
29
29
  end
30
+
31
+ def sync_lock_key(*args)
32
+ if args.empty?
33
+ name
34
+ else
35
+ "#{name}-#{args.join}"
36
+ end
37
+ end
30
38
  end
31
39
  # rubocop:enable Style/Documentation
32
40
 
33
41
  def perform(*args)
34
42
  if self.class.synchronize_jobs_mode == :one_at_a_time
35
- perform_locked(sync_lock_key(*args)) do
43
+ perform_locked(self.class.sync_lock_key(*args)) do
36
44
  perform_timed do
37
45
  super
38
46
  end
@@ -44,14 +52,6 @@ module Sqeduler
44
52
 
45
53
  private
46
54
 
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
55
  # callback for when a lock cannot be obtained
56
56
  def on_lock_timeout(key)
57
57
  Service.logger.warn(
data/lib/sqeduler.rb CHANGED
@@ -6,6 +6,7 @@ require "sqeduler/redis_scripts"
6
6
  require "sqeduler/lock_value"
7
7
  require "sqeduler/redis_lock"
8
8
  require "sqeduler/trigger_lock"
9
+ require "sqeduler/lock_maintainer"
9
10
  require "sqeduler/middleware/kill_switch"
10
11
  require "sqeduler/service"
11
12
  require "sqeduler/worker/callbacks"
@@ -0,0 +1 @@
1
+ {}
data/spec/fixtures/env.rb CHANGED
@@ -1,15 +1,14 @@
1
1
  require "sqeduler"
2
2
  require_relative "fake_worker"
3
3
 
4
- REDIS_CONFIG = {
5
- :host => "localhost",
6
- :db => 1,
7
- :namespace => "sqeduler-tests"
8
- }
9
4
  Sidekiq.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
10
5
 
11
6
  Sqeduler::Service.config = Sqeduler::Config.new(
12
- :redis_hash => REDIS_CONFIG,
7
+ :redis_hash => {
8
+ :host => "localhost",
9
+ :db => 1,
10
+ :namespace => "sqeduler-tests"
11
+ },
13
12
  :logger => Sidekiq.logger,
14
13
  :schedule_path => File.expand_path(File.dirname(__FILE__)) + "/schedule.yaml",
15
14
  :on_server_start => proc do |_config|
@@ -1,12 +1,12 @@
1
1
  # encoding: utf-8
2
2
  # Sample worker for specs
3
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"
4
+ JOB_RUN_PATH = "/tmp/job_run".freeze
5
+ JOB_BEFORE_START_PATH = "/tmp/job_before_start".freeze
6
+ JOB_SUCCESS_PATH = "/tmp/job_success".freeze
7
+ JOB_FAILURE_PATH = "/tmp/job_failure".freeze
8
+ JOB_LOCK_FAILURE_PATH = "/tmp/lock_failure".freeze
9
+ SCHEDULE_COLLISION_PATH = "/tmp/schedule_collision".freeze
10
10
  include Sidekiq::Worker
11
11
  include Sqeduler::Worker::Everything
12
12
 
@@ -0,0 +1,140 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ RSpec.describe Sqeduler::LockMaintainer do
5
+ let(:instance) { described_class.new }
6
+
7
+ before do
8
+ stub_const(
9
+ "SyncExclusiveWorker",
10
+ Class.new do
11
+ include Sidekiq::Worker
12
+ prepend Sqeduler::Worker::Synchronization
13
+ synchronize :one_at_a_time, :expiration => 300, :timeout => 30
14
+
15
+ def perform(*_args)
16
+ yield
17
+ end
18
+ end
19
+ )
20
+
21
+ stub_const(
22
+ "SyncWhateverWorker",
23
+ Class.new do
24
+ include Sidekiq::Worker
25
+ prepend Sqeduler::Worker::Synchronization
26
+
27
+ def perform
28
+ fail "This shouldn't be called"
29
+ end
30
+ end
31
+ )
32
+
33
+ Sqeduler::Service.config = Sqeduler::Config.new(
34
+ :redis_hash => REDIS_CONFIG,
35
+ :logger => Logger.new("/dev/null"),
36
+ :schedule_path => Pathname.new("./spec/fixtures/empty_schedule.yaml")
37
+ )
38
+ end
39
+
40
+ context "#run" do
41
+ let(:run) { instance.run }
42
+
43
+ it "calls into the synchronizer" do
44
+ expect(instance).to receive(:synchronize).at_least(1)
45
+
46
+ run.join(1)
47
+ run.terminate
48
+ end
49
+
50
+ it "doesn't die on errors" do
51
+ expect(instance).to receive(:synchronize).and_raise(StandardError, "Boom")
52
+
53
+ run.join(1)
54
+ expect(run.status).to_not be_falsy
55
+ run.terminate
56
+ end
57
+ end
58
+
59
+ context "#synchronize" do
60
+ subject(:sync) { instance.send(:synchronize) }
61
+
62
+ let(:run_at) { Time.now.to_i }
63
+ let(:job_args) { [1, { "a" => "b" }] }
64
+
65
+ let(:workers) do
66
+ [
67
+ [
68
+ "process-key",
69
+ "worker-tid-1234",
70
+ {
71
+ "run_at" => run_at,
72
+ "payload" => {
73
+ "class" => "SyncExclusiveWorker",
74
+ "args" => job_args
75
+ }
76
+ }
77
+ ],
78
+ [
79
+ "process-key",
80
+ "worker-tid-4321",
81
+ {
82
+ "run_at" => run_at,
83
+ "payload" => {
84
+ "class" => "SyncWhateverWorker",
85
+ "args" => []
86
+ }
87
+ }
88
+ ]
89
+ ]
90
+ end
91
+
92
+ before { allow(instance).to receive(:workers).and_return(workers) }
93
+
94
+ it "does nothing if the jobs just started" do
95
+ expect(instance).to_not receive(:str_to_class)
96
+ sync
97
+ end
98
+
99
+ context "when outside the run threshold" do
100
+ let(:run_at) { Time.now - described_class::RUN_INTERVAL - 5 }
101
+
102
+ let(:lock_key) { SyncExclusiveWorker.sync_lock_key(job_args) }
103
+
104
+ it "refresh the lock" do
105
+ SyncExclusiveWorker.new.perform(job_args) do
106
+ Sqeduler::Service.redis_pool do |redis|
107
+ # Change the lock timing to make sure ours works
108
+ redis.expire(lock_key, 10)
109
+ expect(redis.ttl(lock_key)).to eq(10)
110
+
111
+ # Run the re-locker
112
+ sync
113
+
114
+ # Confirm it reset
115
+ expect(redis.ttl(lock_key)).to eq(300)
116
+ end
117
+ end
118
+
119
+ # Shouldn't be around once the job finished
120
+ Sqeduler::Service.redis_pool do |redis|
121
+ expect(redis.exists(lock_key)).to eq(false)
122
+ end
123
+ end
124
+
125
+ it "obeys the exclusive lock" do
126
+ instance.send(:redis_lock).send(:take_lock)
127
+ expect(instance).to_not receive(:str_to_class)
128
+
129
+ sync
130
+ end
131
+ end
132
+ end
133
+
134
+ context "#str_to_class" do
135
+ it "only returns exclusive lock classes" do
136
+ expect(instance.send(:str_to_class, "SyncExclusiveWorker")).to eq(SyncExclusiveWorker)
137
+ expect(instance.send(:str_to_class, "SyncWhateverWorker")).to eq(false)
138
+ end
139
+ end
140
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,11 +4,7 @@ require "rspec"
4
4
  require "sqeduler"
5
5
  require "timecop"
6
6
 
7
- REDIS_CONFIG = {
8
- :host => "localhost",
9
- :db => 1
10
- }
11
- TEST_REDIS = Redis.new(REDIS_CONFIG)
7
+ TEST_REDIS = Redis.new(:host => "localhost", :db => 1)
12
8
 
13
9
  Timecop.safe_mode = true
14
10
 
@@ -16,6 +12,7 @@ RSpec.configure do |config|
16
12
  config.before(:each) do
17
13
  TEST_REDIS.flushdb
18
14
  Sqeduler::Service.config = nil
15
+ REDIS_CONFIG = { :host => "localhost", :db => 1 }.clone
19
16
  end
20
17
  config.disable_monkey_patching!
21
18
  end
data/sqeduler.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |gem|
27
27
  gem.add_development_dependency "pry", "~> 0"
28
28
  gem.add_development_dependency "rake", "~> 10"
29
29
  gem.add_development_dependency "rspec", "~> 3.3"
30
- gem.add_development_dependency "rubocop", "~> 0.24"
30
+ gem.add_development_dependency "rubocop", "~> 0.36.0"
31
31
  gem.add_development_dependency "timecop", "~> 0"
32
32
  gem.add_development_dependency "yard", "~> 0"
33
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqeduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jared Jenkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-11 00:00:00.000000000 Z
11
+ date: 2016-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.24'
117
+ version: 0.36.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0.24'
124
+ version: 0.36.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: timecop
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -172,6 +172,7 @@ files:
172
172
  - Rakefile
173
173
  - lib/sqeduler.rb
174
174
  - lib/sqeduler/config.rb
175
+ - lib/sqeduler/lock_maintainer.rb
175
176
  - lib/sqeduler/lock_value.rb
176
177
  - lib/sqeduler/middleware/kill_switch.rb
177
178
  - lib/sqeduler/redis_lock.rb
@@ -184,10 +185,12 @@ files:
184
185
  - lib/sqeduler/worker/kill_switch.rb
185
186
  - lib/sqeduler/worker/synchronization.rb
186
187
  - spec/config_spec.rb
188
+ - spec/fixtures/empty_schedule.yaml
187
189
  - spec/fixtures/env.rb
188
190
  - spec/fixtures/fake_worker.rb
189
191
  - spec/fixtures/schedule.yaml
190
192
  - spec/integration_spec.rb
193
+ - spec/lock_maintainer_spec.rb
191
194
  - spec/middleware/kill_switch_spec.rb
192
195
  - spec/service_spec.rb
193
196
  - spec/spec_helper.rb
@@ -216,16 +219,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
219
  version: '0'
217
220
  requirements: []
218
221
  rubyforge_project:
219
- rubygems_version: 2.5.0
222
+ rubygems_version: 2.4.6
220
223
  signing_key:
221
224
  specification_version: 4
222
225
  summary: Common Sidekiq infrastructure for multi-host applications.
223
226
  test_files:
224
227
  - spec/config_spec.rb
228
+ - spec/fixtures/empty_schedule.yaml
225
229
  - spec/fixtures/env.rb
226
230
  - spec/fixtures/fake_worker.rb
227
231
  - spec/fixtures/schedule.yaml
228
232
  - spec/integration_spec.rb
233
+ - spec/lock_maintainer_spec.rb
229
234
  - spec/middleware/kill_switch_spec.rb
230
235
  - spec/service_spec.rb
231
236
  - spec/spec_helper.rb