mini_scheduler 0.10.0 → 0.12.3

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
  SHA256:
3
- metadata.gz: 61de19d47b5090c36a179869a01800506ee364629db64f47df963426c1d9a8b6
4
- data.tar.gz: faa1b86a2d625fa570b26dd8ad8a2ccdb15fcd6029f1d93ad67ddefb0301342a
3
+ metadata.gz: def18630479a6ca0133f7131ba4aa59c779f66bd7d979c84e43d90b9c7303927
4
+ data.tar.gz: 0bb8b699e224d2e3b47f9237fc9e9c68d71c6bb4c1ae82e67d55b7c442276997
5
5
  SHA512:
6
- metadata.gz: 931ae1731d9c1b49b3c861ec5b401d317e436f9548494c6fc9cd7b242c5d9bd932459bdf5598f9e7ca24abe8c46d585a43d75cac8a2772f7a04bc77fcf6ad27d
7
- data.tar.gz: 82ef45be2249b8c5d6834aeeb13f3f72e4b23604436a68246bba46350ad49255d0f9adf6ac262f685c189d9fc6670c8247776ab3b2d4eaf1f9b7e55b87a63515
6
+ metadata.gz: 9ebcc59b92f99824c503682fd33fc4ca11dfa47920ad7fb2b74d2c123534104e0cb3fcc44c1848c2ddac91a80f187a5800dff4536f245e0eac07ad6db01bb0c7
7
+ data.tar.gz: 06074fb819bdce384abee949ddb27b81361a2a42823a8c1a4c25b8e3f5b8854557f7222ed791fa0b4b343aa1467e97eb8eaac98a4c03ae98330a0245bc8cc09f
@@ -1,8 +1,29 @@
1
- # 0.9.2 - 26-04-2019
1
+ # 0.12.3 - 2020-10-15
2
+
3
+ - Fixes a problem where scheduler didn't recover from Redis flush
4
+
5
+ # 0.12.2 - 2019-09-11
6
+
7
+ - Allow sorting schedule history by schedule name
8
+
9
+ # 0.12.1 - 2019-08-30
10
+
11
+ - Jobs that change family from per host to non per host can cause a tight loop
12
+
13
+ # 0.12.0 - 2019-08-29
14
+
15
+ - Add support for multiple workers which allows avoiding queue starvation
16
+
17
+ # 0.11.0 - 2019-06-24
18
+
19
+ - Correct situation where distributed mutex could end in a tight loop when
20
+ redis could not be contacted
21
+
22
+ # 0.9.2 - 2019-04-26
2
23
 
3
24
  - Correct UI so it displays durations that are longer than a minute
4
25
 
5
- # 0.9.1 - 21-01-2019
26
+ # 0.9.1 - 2019-01-21
6
27
 
7
28
  - Remove dependency on ActiveSupport and add proper dependency for Sidekiq
8
29
  - Remove Discourse specific bits from Sidekiq web scheduler tab.
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  source 'https://rubygems.org'
2
3
 
3
4
  git_source(:github) { 'https://github.com/discourse/mini_scheduler' }
data/Guardfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # A sample Guardfile
2
3
  # More info at https://github.com/guard/guard#readme
3
4
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.org/discourse/mini_scheduler.svg?branch=master)](https://travis-ci.org/discourse/mini_scheduler)
1
+ [![Build Status](https://github.com/discourse/mini_scheduler/workflows/CI/badge.svg)](https://github.com/discourse/mini_scheduler/actions)
2
2
  [![Gem Version](https://badge.fury.io/rb/mini_scheduler.svg)](https://rubygems.org/gems/mini_scheduler)
3
3
 
4
4
  # mini_scheduler
@@ -30,6 +30,20 @@ In a Rails application, create files needed in your application to configure min
30
30
 
31
31
  An initializer is created named `config/initializers/mini_scheduler.rb` which lists all the configuration options.
32
32
 
33
+ ## Configuring MiniScheduler
34
+
35
+ By default each instance of MiniScheduler will run with a single worker. To amend this behavior:
36
+
37
+ ```
38
+ if Sidekiq.server? && defined?(Rails)
39
+ Rails.application.config.after_initialize do
40
+ MiniScheduler.start(workers: 5)
41
+ end
42
+ end
43
+ ```
44
+
45
+ This is useful for cases where you have extremely long running tasks that you would prefer did not starve.
46
+
33
47
  ## Usage
34
48
 
35
49
  Create jobs with a recurring schedule like this:
data/Rakefile CHANGED
@@ -1,6 +1,19 @@
1
- require "bundler/gem_tasks"
1
+ #!/usr/bin/env rake
2
+ # frozen_string_literal: true
3
+
2
4
  require "rspec/core/rake_task"
5
+ require 'bundler'
6
+
7
+ begin
8
+ Bundler.setup :default, :development
9
+ Bundler::GemHelper.install_tasks
10
+ rescue Bundler::BundlerError => error
11
+ $stderr.puts error.message
12
+ $stderr.puts "Run `bundle install` to install missing gems"
13
+ exit error.status_code
14
+ end
3
15
 
4
16
  RSpec::Core::RakeTask.new(:spec)
5
17
 
6
- task default: :spec
18
+ desc "Default: run tests"
19
+ task default: [ :spec ]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if defined?(ActiveRecord::Base)
2
4
  module MiniScheduler
3
5
  class Stat < ActiveRecord::Base
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails/generators'
2
4
  require 'rails/generators/migration'
3
5
  require 'active_record'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreateMiniSchedulerStats < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  create_table :scheduler_stats do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  MiniScheduler.configure do |config|
2
4
  # An instance of Redis. See https://github.com/redis/redis-rb
3
5
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "mini_scheduler/engine"
2
3
  require 'mini_scheduler/schedule'
3
4
  require 'mini_scheduler/schedule_info'
@@ -51,11 +52,11 @@ module MiniScheduler
51
52
  @skip_schedule
52
53
  end
53
54
 
54
- def self.start
55
+ def self.start(workers: 1)
55
56
  schedules = Manager.discover_schedules
56
57
 
57
58
  Manager.discover_queues.each do |queue|
58
- manager = Manager.new(queue: queue)
59
+ manager = Manager.new(queue: queue, workers: workers)
59
60
 
60
61
  schedules.each do |schedule|
61
62
  if schedule.queue == queue
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniScheduler
2
4
  class DistributedMutex
5
+ class Timeout < StandardError; end
3
6
 
4
7
  @default_redis = nil
5
8
 
@@ -18,11 +21,26 @@ module MiniScheduler
18
21
  @mutex = Mutex.new
19
22
  end
20
23
 
24
+ MAX_POLLING_ATTEMPTS ||= 60
25
+ BASE_SLEEP_DURATION ||= 0.001
26
+ MAX_SLEEP_DURATION ||= 1
27
+
21
28
  # NOTE wrapped in mutex to maintain its semantics
22
29
  def synchronize
23
30
  @mutex.lock
31
+
32
+ attempts = 0
33
+ sleep_duration = BASE_SLEEP_DURATION
24
34
  while !try_to_get_lock
25
- sleep 0.001
35
+
36
+ sleep(sleep_duration)
37
+
38
+ if sleep_duration < MAX_SLEEP_DURATION
39
+ sleep_duration = [sleep_duration * 2, MAX_SLEEP_DURATION].min
40
+ end
41
+
42
+ attempts += 1
43
+ raise Timeout if attempts >= MAX_POLLING_ATTEMPTS
26
44
  end
27
45
 
28
46
  yield
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if defined?(::Rails)
2
4
  module MiniScheduler
3
5
  class Engine < ::Rails::Engine
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniScheduler
2
4
  class Manager
3
- attr_accessor :random_ratio, :redis, :enable_stats, :queue
5
+ attr_accessor :random_ratio, :redis, :enable_stats, :queue, :workers
4
6
 
5
7
  class Runner
6
8
  def initialize(manager)
@@ -8,11 +10,14 @@ module MiniScheduler
8
10
  @mutex = Mutex.new
9
11
  @queue = Queue.new
10
12
  @manager = manager
11
- @reschedule_orphans_thread = Thread.new do
13
+ @hostname = manager.hostname
14
+
15
+ @recovery_thread = Thread.new do
12
16
  while !@stopped
13
17
  sleep 60
14
18
 
15
19
  @mutex.synchronize do
20
+ repair_queue
16
21
  reschedule_orphans
17
22
  end
18
23
  end
@@ -25,9 +30,12 @@ module MiniScheduler
25
30
  sleep (@manager.keep_alive_duration / 2)
26
31
  end
27
32
  end
28
- @thread = Thread.new do
29
- while !@stopped
30
- process_queue
33
+ @threads = []
34
+ manager.workers.times do
35
+ @threads << Thread.new do
36
+ while !@stopped
37
+ process_queue
38
+ end
31
39
  end
32
40
  end
33
41
  end
@@ -38,6 +46,12 @@ module MiniScheduler
38
46
  MiniScheduler.handle_job_exception(ex, message: "Scheduling manager keep-alive")
39
47
  end
40
48
 
49
+ def repair_queue
50
+ @manager.repair_queue
51
+ rescue => ex
52
+ MiniScheduler.handle_job_exception(ex, message: "Scheduling manager queue repair")
53
+ end
54
+
41
55
  def reschedule_orphans
42
56
  @manager.reschedule_orphans!
43
57
  rescue => ex
@@ -45,20 +59,17 @@ module MiniScheduler
45
59
  end
46
60
 
47
61
  def hostname
48
- @hostname ||= begin
49
- `hostname`
50
- rescue
51
- "unknown"
52
- end
62
+ @hostname
53
63
  end
54
64
 
55
65
  def process_queue
56
66
 
57
67
  klass = @queue.deq
58
- return unless klass
59
-
60
68
  # hack alert, I need to both deq and set @running atomically.
61
69
  @running = true
70
+
71
+ return if !klass
72
+
62
73
  failed = false
63
74
  start = Time.now.to_f
64
75
  info = @mutex.synchronize { @manager.schedule_info(klass) }
@@ -118,19 +129,19 @@ module MiniScheduler
118
129
  @stopped = true
119
130
 
120
131
  @keep_alive_thread.kill
121
- @reschedule_orphans_thread.kill
132
+ @recovery_thread.kill
122
133
 
123
134
  @keep_alive_thread.join
124
- @reschedule_orphans_thread.join
135
+ @recovery_thread.join
125
136
 
126
137
  enq(nil)
127
138
 
128
139
  kill_thread = Thread.new do
129
140
  sleep 0.5
130
- @thread.kill
141
+ @threads.each(&:kill)
131
142
  end
132
143
 
133
- @thread.join
144
+ @threads.each(&:join)
134
145
  kill_thread.kill
135
146
  kill_thread.join
136
147
  end
@@ -145,7 +156,8 @@ module MiniScheduler
145
156
  sleep 0.001
146
157
  end
147
158
  # this is a hack, but is only used for test anyway
148
- sleep 0.001
159
+ # if tests fail that depend on this we are forced to increase it.
160
+ sleep 0.010
149
161
  while @running
150
162
  sleep 0.001
151
163
  end
@@ -169,7 +181,7 @@ module MiniScheduler
169
181
 
170
182
  def initialize(options = nil)
171
183
  @queue = options && options[:queue] || "default"
172
-
184
+ @workers = options && options[:workers] || 1
173
185
  @redis = MiniScheduler.redis
174
186
  @random_ratio = 0.1
175
187
  unless options && options[:skip_runner]
@@ -192,7 +204,11 @@ module MiniScheduler
192
204
  end
193
205
 
194
206
  def hostname
195
- @hostname ||= `hostname`.strip
207
+ @hostname ||= begin
208
+ `hostname`.strip
209
+ rescue
210
+ "unknown"
211
+ end
196
212
  end
197
213
 
198
214
  def schedule_info(klass)
@@ -243,6 +259,15 @@ module MiniScheduler
243
259
  nil
244
260
  end
245
261
 
262
+ def repair_queue
263
+ return if redis.exists?(self.class.queue_key(queue)) ||
264
+ redis.exists?(self.class.queue_key(queue, hostname))
265
+
266
+ self.class.discover_schedules
267
+ .select { |schedule| schedule.queue == queue }
268
+ .each { |schedule| ensure_schedule!(schedule) }
269
+ end
270
+
246
271
  def tick
247
272
  lock do
248
273
  schedule_next_job
@@ -256,11 +281,14 @@ module MiniScheduler
256
281
 
257
282
  if due.to_i <= Time.now.to_i
258
283
  klass = get_klass(key)
259
- unless klass
284
+ if !klass || (
285
+ (klass.is_per_host && !hostname) || (hostname && !klass.is_per_host)
286
+ )
260
287
  # corrupt key, nuke it (renamed job or something)
261
288
  redis.zrem Manager.queue_key(queue, hostname), key
262
289
  return
263
290
  end
291
+
264
292
  info = schedule_info(klass)
265
293
  info.prev_run = Time.now.to_i
266
294
  info.prev_result = "QUEUED"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniScheduler::Schedule
2
4
 
3
5
  def queue(value = nil)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniScheduler
2
4
  class ScheduleInfo
3
5
  attr_accessor :next_run,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniScheduler
2
- VERSION = "0.10.0"
4
+ VERSION = "0.12.3"
3
5
  end
@@ -2,7 +2,20 @@
2
2
  <div class="col-sm-12">
3
3
  <h3>Scheduler History</h3>
4
4
  </div>
5
+
6
+ <div class="col-sm-12">
7
+ <form>
8
+ <select name="filter">
9
+ <option value="All" <%= "selected" if !@filter %>>All</option>
10
+ <% @schedules.each do |schedule| %>
11
+ <option value="<%=schedule%>" <%= "selected" if @filter == schedule.to_s%>><%=schedule%></option>
12
+ <% end %>
13
+ </select>
14
+ <input type="submit" value="Filter">
15
+ </form>
16
+ </div>
5
17
  </header>
18
+ <br>
6
19
 
7
20
  <div class="container">
8
21
  <div class="row">
@@ -39,7 +39,7 @@
39
39
  <td>
40
40
  <%= sane_duration @info.prev_duration %>
41
41
  </td>
42
- <td>
42
+ <td style="word-wrap: break-word">
43
43
  <%= @info.current_owner %>
44
44
  </td>
45
45
  <td>
@@ -1,8 +1,23 @@
1
+ # frozen_string_literal: true
1
2
  # Based off sidetiq https://github.com/tobiassvn/sidetiq/blob/master/lib/sidetiq/web.rb
2
3
  module MiniScheduler
3
4
  module Web
4
5
  VIEWS = File.expand_path('views', File.dirname(__FILE__)) unless defined? VIEWS
5
6
 
7
+ def self.find_schedules_by_time
8
+ Manager.discover_schedules.sort do |a, b|
9
+ a_next = a.schedule_info.next_run
10
+ b_next = b.schedule_info.next_run
11
+ if a_next && b_next
12
+ a_next <=> b_next
13
+ elsif a_next
14
+ -1
15
+ else
16
+ 1
17
+ end
18
+ end
19
+ end
20
+
6
21
  def self.registered(app)
7
22
 
8
23
  app.helpers do
@@ -23,23 +38,24 @@ module MiniScheduler
23
38
 
24
39
  app.get "/scheduler" do
25
40
  MiniScheduler.before_sidekiq_web_request&.call
26
- @schedules = Manager.discover_schedules.sort do |a, b|
27
- a_next = a.schedule_info.next_run
28
- b_next = b.schedule_info.next_run
29
- if a_next && b_next
30
- a_next <=> b_next
31
- elsif a_next
32
- -1
33
- else
34
- 1
35
- end
36
- end
41
+ @schedules = Web.find_schedules_by_time
37
42
  erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: { view_path: VIEWS }
38
43
  end
39
44
 
40
45
  app.get "/scheduler/history" do
41
46
  MiniScheduler.before_sidekiq_web_request&.call
42
- @scheduler_stats = Stat.order('started_at desc').limit(200)
47
+ @schedules = Manager.discover_schedules
48
+ @schedules.sort_by!(&:to_s)
49
+ @scheduler_stats = Stat.order('started_at desc')
50
+
51
+ @filter = params[:filter]
52
+ names = @schedules.map(&:to_s)
53
+ @filter = nil if !names.include?(@filter)
54
+ if @filter
55
+ @scheduler_stats = @scheduler_stats.where(name: @filter)
56
+ end
57
+
58
+ @scheduler_stats = @scheduler_stats.limit(200)
43
59
  erb File.read(File.join(VIEWS, 'history.erb')), locals: { view_path: VIEWS }
44
60
  end
45
61
 
@@ -15,11 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = "https://github.com/discourse/mini_scheduler"
16
16
  spec.license = "MIT"
17
17
 
18
- # Specify which files should be added to the gem when it is released.
19
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
- end
18
+ spec.files = `git ls-files`.split($/).reject { |s| s =~ /^(spec|\.)/ }
23
19
  spec.require_paths = ["lib"]
24
20
 
25
21
  spec.add_dependency "sidekiq"
@@ -32,4 +28,5 @@ Gem::Specification.new do |spec|
32
28
  spec.add_development_dependency "guard-rspec"
33
29
  spec.add_development_dependency "mock_redis"
34
30
  spec.add_development_dependency "rake"
31
+ spec.add_development_dependency 'rubocop-discourse'
35
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-05-23 00:00:00.000000000 Z
12
+ date: 2020-10-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -137,6 +137,20 @@ dependencies:
137
137
  - - ">="
138
138
  - !ruby/object:Gem::Version
139
139
  version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rubocop-discourse
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
140
154
  description: Adds recurring jobs for Sidekiq
141
155
  email:
142
156
  - neil.lalonde@discourse.org
@@ -144,10 +158,6 @@ executables: []
144
158
  extensions: []
145
159
  extra_rdoc_files: []
146
160
  files:
147
- - ".gitignore"
148
- - ".rspec"
149
- - ".rubocop.yml"
150
- - ".travis.yml"
151
161
  - CHANGELOG.md
152
162
  - Gemfile
153
163
  - Guardfile
data/.gitignore DELETED
@@ -1,13 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
- Gemfile.lock
10
- .DS_Store
11
- *.swp
12
-
13
- .rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper --color
@@ -1 +0,0 @@
1
- inherit_from: https://raw.githubusercontent.com/discourse/discourse/master/.rubocop.yml
@@ -1,18 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - ruby-head
4
- - 2.5
5
- - 2.6
6
-
7
- before_install:
8
- - gem install bundler
9
-
10
- cache: bundler
11
- sudo: false
12
-
13
- services:
14
- - redis-server
15
-
16
- matrix:
17
- allow_failures:
18
- - rvm: ruby-head