mini_scheduler 0.8.0
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/.gitignore +11 -0
- data/.rspec +1 -0
- data/.rubocop.yml +113 -0
- data/Gemfile +5 -0
- data/README.md +50 -0
- data/app/models/mini_scheduler/stat.rb +10 -0
- data/lib/generators/mini_scheduler/install/install_generator.rb +25 -0
- data/lib/generators/mini_scheduler/install/templates/create_mini_scheduler_stats.rb +15 -0
- data/lib/generators/mini_scheduler/install/templates/mini_scheduler_initializer.rb +31 -0
- data/lib/mini_scheduler/distributed_mutex.rb +61 -0
- data/lib/mini_scheduler/engine.rb +7 -0
- data/lib/mini_scheduler/manager.rb +348 -0
- data/lib/mini_scheduler/schedule.rb +37 -0
- data/lib/mini_scheduler/schedule_info.rb +138 -0
- data/lib/mini_scheduler/version.rb +3 -0
- data/lib/mini_scheduler/views/history.erb +47 -0
- data/lib/mini_scheduler/views/scheduler.erb +73 -0
- data/lib/mini_scheduler/web.rb +60 -0
- data/lib/mini_scheduler.rb +63 -0
- data/mini_scheduler.gemspec +36 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8f80dba7428c2a8aa9ab7993aaedc20623997a4710fe13856826361b23d3b2b7
|
4
|
+
data.tar.gz: 42eeeafbf3c3764883560f4fc38cfdf59cf9ea7eea30b95111582ff81b98a452
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 195412471e37e96582ef2ee9b2ee0d7107f5d2113c918aacd4cf85fb9135d751ae189f8a32beca9bd9719cf0acb54c47a008527585eb48265be239cca54e4bb1
|
7
|
+
data.tar.gz: b5ee5701360abf05b16179200dcc56bcbc0bc9203edf2a8cd2038f527dd8dab0513f0563f381ebfbb601d550a0c365ab22ed8f8879434d98caaabf5f6cb1e6fa
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper --color
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.4
|
3
|
+
DisabledByDefault: true
|
4
|
+
Exclude:
|
5
|
+
- 'db/schema.rb'
|
6
|
+
- 'bundle/**/*'
|
7
|
+
- 'vendor/**/*'
|
8
|
+
- 'node_modules/**/*'
|
9
|
+
- 'public/**/*'
|
10
|
+
|
11
|
+
# Prefer &&/|| over and/or.
|
12
|
+
Style/AndOr:
|
13
|
+
Enabled: true
|
14
|
+
|
15
|
+
# Do not use braces for hash literals when they are the last argument of a
|
16
|
+
# method call.
|
17
|
+
Style/BracesAroundHashParameters:
|
18
|
+
Enabled: true
|
19
|
+
|
20
|
+
# Align `when` with `case`.
|
21
|
+
Layout/CaseIndentation:
|
22
|
+
Enabled: true
|
23
|
+
|
24
|
+
# Align comments with method definitions.
|
25
|
+
Layout/CommentIndentation:
|
26
|
+
Enabled: true
|
27
|
+
|
28
|
+
# No extra empty lines.
|
29
|
+
Layout/EmptyLines:
|
30
|
+
Enabled: true
|
31
|
+
|
32
|
+
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
|
33
|
+
Style/HashSyntax:
|
34
|
+
Enabled: true
|
35
|
+
|
36
|
+
# Two spaces, no tabs (for indentation).
|
37
|
+
Layout/IndentationWidth:
|
38
|
+
Enabled: true
|
39
|
+
|
40
|
+
Layout/SpaceAfterColon:
|
41
|
+
Enabled: true
|
42
|
+
|
43
|
+
Layout/SpaceAfterComma:
|
44
|
+
Enabled: true
|
45
|
+
|
46
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
47
|
+
Enabled: true
|
48
|
+
|
49
|
+
Layout/SpaceAroundKeyword:
|
50
|
+
Enabled: true
|
51
|
+
|
52
|
+
Layout/SpaceAroundOperators:
|
53
|
+
Enabled: true
|
54
|
+
|
55
|
+
Layout/SpaceBeforeFirstArg:
|
56
|
+
Enabled: true
|
57
|
+
|
58
|
+
# Defining a method with parameters needs parentheses.
|
59
|
+
Style/MethodDefParentheses:
|
60
|
+
Enabled: true
|
61
|
+
|
62
|
+
# Use `foo {}` not `foo{}`.
|
63
|
+
Layout/SpaceBeforeBlockBraces:
|
64
|
+
Enabled: true
|
65
|
+
|
66
|
+
# Use `foo { bar }` not `foo {bar}`.
|
67
|
+
Layout/SpaceInsideBlockBraces:
|
68
|
+
Enabled: true
|
69
|
+
|
70
|
+
# Use `{ a: 1 }` not `{a:1}`.
|
71
|
+
Layout/SpaceInsideHashLiteralBraces:
|
72
|
+
Enabled: true
|
73
|
+
|
74
|
+
Layout/SpaceInsideParens:
|
75
|
+
Enabled: true
|
76
|
+
|
77
|
+
# Detect hard tabs, no hard tabs.
|
78
|
+
Layout/Tab:
|
79
|
+
Enabled: true
|
80
|
+
|
81
|
+
# Blank lines should not have any spaces.
|
82
|
+
Layout/TrailingBlankLines:
|
83
|
+
Enabled: true
|
84
|
+
|
85
|
+
# No trailing whitespace.
|
86
|
+
Layout/TrailingWhitespace:
|
87
|
+
Enabled: true
|
88
|
+
|
89
|
+
Lint/Debugger:
|
90
|
+
Enabled: true
|
91
|
+
|
92
|
+
Lint/BlockAlignment:
|
93
|
+
Enabled: true
|
94
|
+
|
95
|
+
# Align `end` with the matching keyword or starting expression except for
|
96
|
+
# assignments, where it should be aligned with the LHS.
|
97
|
+
Lint/EndAlignment:
|
98
|
+
Enabled: true
|
99
|
+
EnforcedStyleAlignWith: variable
|
100
|
+
|
101
|
+
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
|
102
|
+
Lint/RequireParentheses:
|
103
|
+
Enabled: true
|
104
|
+
|
105
|
+
Layout/MultilineMethodCallIndentation:
|
106
|
+
Enabled: true
|
107
|
+
EnforcedStyle: indented
|
108
|
+
|
109
|
+
Layout/AlignHash:
|
110
|
+
Enabled: true
|
111
|
+
|
112
|
+
Bundler/OrderedGems:
|
113
|
+
Enabled: false
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# mini_scheduler
|
2
|
+
|
3
|
+
MiniScheduler adds recurring jobs to [Sidekiq](https://sidekiq.org/).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'mini_scheduler'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install mini_scheduler
|
20
|
+
|
21
|
+
In a Rails application, create files needed in your application to configure mini_scheduler:
|
22
|
+
|
23
|
+
bin/rails g mini_scheduler:install
|
24
|
+
rake db:migrate
|
25
|
+
|
26
|
+
An initializer is created named `config/initializers/mini_scheduler.rb` which lists all the configuration options.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Create jobs with a recurring schedule like this:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class MyHourlyJob
|
34
|
+
include Sidekiq::Worker
|
35
|
+
extend MiniScheduler::Schedule
|
36
|
+
|
37
|
+
every 1.hour
|
38
|
+
|
39
|
+
def execute(args)
|
40
|
+
# some tasks
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Options for schedules:
|
46
|
+
|
47
|
+
* **every** followed by a duration in seconds, like "every 1.hour".
|
48
|
+
* **daily at:** followed by a duration since midnight, like "daily at: 12.hours", to run only once per day at a specific time.
|
49
|
+
|
50
|
+
To view the scheduled jobs, their history, and the schedule, go to sidekiq's web UI and look for the "Scheduler" tab at the top.
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module MiniScheduler
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
desc "Generate files for MiniScheduler"
|
10
|
+
|
11
|
+
def self.next_migration_number(path)
|
12
|
+
next_migration_number = current_migration_number(path) + 1
|
13
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
14
|
+
end
|
15
|
+
|
16
|
+
def copy_migrations
|
17
|
+
migration_template("create_mini_scheduler_stats.rb", "db/migrate/create_mini_scheduler_stats.rb")
|
18
|
+
end
|
19
|
+
|
20
|
+
def copy_initializer_file
|
21
|
+
copy_file "mini_scheduler_initializer.rb", "config/initializers/mini_scheduler.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateMiniSchedulerStats < ActiveRecord::Migration[4.2]
|
2
|
+
def change
|
3
|
+
create_table :scheduler_stats do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.string :hostname, null: false
|
6
|
+
t.integer :pid, null: false
|
7
|
+
t.integer :duration_ms
|
8
|
+
t.integer :live_slots_start
|
9
|
+
t.integer :live_slots_finish
|
10
|
+
t.datetime :started_at, null: false
|
11
|
+
t.boolean :success
|
12
|
+
t.text :error
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
MiniScheduler.configure do |config|
|
2
|
+
# An instance of Redis. See https://github.com/redis/redis-rb
|
3
|
+
|
4
|
+
# config.redis = $redis
|
5
|
+
|
6
|
+
# Define a custom exception handler when an exception is raised
|
7
|
+
# by a scheduled job. By default, SidekiqExceptionHandler is used.
|
8
|
+
|
9
|
+
# config.job_exception_handler do |ex, context|
|
10
|
+
# ...
|
11
|
+
# end
|
12
|
+
|
13
|
+
# Add code to be called after a scheduled job runs. An argument
|
14
|
+
# with stats about the execution is passed, including these fields:
|
15
|
+
# name, hostname, pid, started_at, duration_ms, live_slots_start,
|
16
|
+
# live_slots_finish, success, error
|
17
|
+
|
18
|
+
# config.job_ran do |stats|
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
end
|
22
|
+
|
23
|
+
if Sidekiq.server? && defined?(Rails)
|
24
|
+
Rails.application.config.after_initialize do
|
25
|
+
scheduler_hostname = ENV["UNICORN_SCHEDULER_HOSTNAME"]
|
26
|
+
|
27
|
+
if !scheduler_hostname || scheduler_hostname.split(',').include?(`hostname`.strip)
|
28
|
+
MiniScheduler.start
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module MiniScheduler
|
2
|
+
class DistributedMutex
|
3
|
+
|
4
|
+
@default_redis = nil
|
5
|
+
|
6
|
+
def self.redis=(redis)
|
7
|
+
@default_redis = redis
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.synchronize(key, redis = nil, &blk)
|
11
|
+
self.new(key, redis || @default_redis).synchronize(&blk)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(key, redis)
|
15
|
+
raise ArgumentError.new('redis argument is nil') if redis.nil?
|
16
|
+
@key = key
|
17
|
+
@redis = redis
|
18
|
+
@mutex = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# NOTE wrapped in mutex to maintain its semantics
|
22
|
+
def synchronize
|
23
|
+
@mutex.lock
|
24
|
+
while !try_to_get_lock
|
25
|
+
sleep 0.001
|
26
|
+
end
|
27
|
+
|
28
|
+
yield
|
29
|
+
|
30
|
+
ensure
|
31
|
+
@redis.del @key
|
32
|
+
@mutex.unlock
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def try_to_get_lock
|
38
|
+
got_lock = false
|
39
|
+
if @redis.setnx @key, Time.now.to_i + 60
|
40
|
+
@redis.expire @key, 60
|
41
|
+
got_lock = true
|
42
|
+
else
|
43
|
+
begin
|
44
|
+
@redis.watch @key
|
45
|
+
time = @redis.get @key
|
46
|
+
if time && time.to_i < Time.now.to_i
|
47
|
+
got_lock = @redis.multi do
|
48
|
+
@redis.set @key, Time.now.to_i + 60
|
49
|
+
end
|
50
|
+
end
|
51
|
+
ensure
|
52
|
+
@redis.unwatch
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
got_lock
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
module MiniScheduler
|
2
|
+
class Manager
|
3
|
+
attr_accessor :random_ratio, :redis, :enable_stats
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
def initialize(manager)
|
7
|
+
@stopped = false
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@queue = Queue.new
|
10
|
+
@manager = manager
|
11
|
+
@reschedule_orphans_thread = Thread.new do
|
12
|
+
while !@stopped
|
13
|
+
sleep 1.minute
|
14
|
+
@mutex.synchronize do
|
15
|
+
reschedule_orphans
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@keep_alive_thread = Thread.new do
|
20
|
+
while !@stopped
|
21
|
+
@mutex.synchronize do
|
22
|
+
keep_alive
|
23
|
+
end
|
24
|
+
sleep (@manager.keep_alive_duration / 2)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@thread = Thread.new do
|
28
|
+
while !@stopped
|
29
|
+
process_queue
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def keep_alive
|
35
|
+
@manager.keep_alive
|
36
|
+
rescue => ex
|
37
|
+
MiniScheduler.handle_job_exception(ex, message: "Scheduling manager keep-alive")
|
38
|
+
end
|
39
|
+
|
40
|
+
def reschedule_orphans
|
41
|
+
@manager.reschedule_orphans!
|
42
|
+
rescue => ex
|
43
|
+
MiniScheduler.handle_job_exception(ex, message: "Scheduling manager orphan rescheduler")
|
44
|
+
end
|
45
|
+
|
46
|
+
def hostname
|
47
|
+
@hostname ||= begin
|
48
|
+
`hostname`
|
49
|
+
rescue
|
50
|
+
"unknown"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_queue
|
55
|
+
|
56
|
+
klass = @queue.deq
|
57
|
+
return unless klass
|
58
|
+
|
59
|
+
# hack alert, I need to both deq and set @running atomically.
|
60
|
+
@running = true
|
61
|
+
failed = false
|
62
|
+
start = Time.now.to_f
|
63
|
+
info = @mutex.synchronize { @manager.schedule_info(klass) }
|
64
|
+
stat = nil
|
65
|
+
error = nil
|
66
|
+
|
67
|
+
begin
|
68
|
+
info.prev_result = "RUNNING"
|
69
|
+
@mutex.synchronize { info.write! }
|
70
|
+
|
71
|
+
if @manager.enable_stats
|
72
|
+
stat = MiniScheduler::Stat.create!(
|
73
|
+
name: klass.to_s,
|
74
|
+
hostname: hostname,
|
75
|
+
pid: Process.pid,
|
76
|
+
started_at: Time.now,
|
77
|
+
live_slots_start: GC.stat[:heap_live_slots]
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
klass.new.perform
|
82
|
+
rescue => e
|
83
|
+
MiniScheduler.handle_job_exception(e, message: "Running a scheduled job", job: klass)
|
84
|
+
|
85
|
+
error = "#{e.class}: #{e.message} #{e.backtrace.join("\n")}"
|
86
|
+
failed = true
|
87
|
+
end
|
88
|
+
duration = ((Time.now.to_f - start) * 1000).to_i
|
89
|
+
info.prev_duration = duration
|
90
|
+
info.prev_result = failed ? "FAILED" : "OK"
|
91
|
+
info.current_owner = nil
|
92
|
+
if stat
|
93
|
+
stat.update!(
|
94
|
+
duration_ms: duration,
|
95
|
+
live_slots_finish: GC.stat[:heap_live_slots],
|
96
|
+
success: !failed,
|
97
|
+
error: error
|
98
|
+
)
|
99
|
+
MiniScheduler.job_ran&.call(stat)
|
100
|
+
end
|
101
|
+
attempts(3) do
|
102
|
+
@mutex.synchronize { info.write! }
|
103
|
+
end
|
104
|
+
rescue => ex
|
105
|
+
MiniScheduler.handle_job_exception(ex, message: "Processing scheduled job queue")
|
106
|
+
ensure
|
107
|
+
@running = false
|
108
|
+
if defined?(ActiveRecord::Base)
|
109
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def stop!
|
114
|
+
return if @stopped
|
115
|
+
|
116
|
+
@mutex.synchronize do
|
117
|
+
@stopped = true
|
118
|
+
|
119
|
+
@keep_alive_thread.kill
|
120
|
+
@reschedule_orphans_thread.kill
|
121
|
+
|
122
|
+
@keep_alive_thread.join
|
123
|
+
@reschedule_orphans_thread.join
|
124
|
+
|
125
|
+
enq(nil)
|
126
|
+
|
127
|
+
kill_thread = Thread.new do
|
128
|
+
sleep 0.5
|
129
|
+
@thread.kill
|
130
|
+
end
|
131
|
+
|
132
|
+
@thread.join
|
133
|
+
kill_thread.kill
|
134
|
+
kill_thread.join
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def enq(klass)
|
139
|
+
@queue << klass
|
140
|
+
end
|
141
|
+
|
142
|
+
def wait_till_done
|
143
|
+
while !@queue.empty? && !(@queue.num_waiting > 0)
|
144
|
+
sleep 0.001
|
145
|
+
end
|
146
|
+
# this is a hack, but is only used for test anyway
|
147
|
+
sleep 0.001
|
148
|
+
while @running
|
149
|
+
sleep 0.001
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def attempts(n)
|
154
|
+
n.times {
|
155
|
+
begin
|
156
|
+
yield; break
|
157
|
+
rescue
|
158
|
+
sleep Random.rand
|
159
|
+
end
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.without_runner
|
166
|
+
self.new(skip_runner: true)
|
167
|
+
end
|
168
|
+
|
169
|
+
def initialize(options = nil)
|
170
|
+
@redis = MiniScheduler.redis
|
171
|
+
@random_ratio = 0.1
|
172
|
+
unless options && options[:skip_runner]
|
173
|
+
@runner = Runner.new(self)
|
174
|
+
self.class.current = self
|
175
|
+
end
|
176
|
+
|
177
|
+
@hostname = options && options[:hostname]
|
178
|
+
@manager_id = SecureRandom.hex
|
179
|
+
|
180
|
+
if options && options.key?(:enable_stats)
|
181
|
+
@enable_stats = options[:enable_stats]
|
182
|
+
else
|
183
|
+
@enable_stats = true # doesn't work !!defined?(MiniScheduler::Stat)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.current
|
188
|
+
@current
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.current=(manager)
|
192
|
+
@current = manager
|
193
|
+
end
|
194
|
+
|
195
|
+
def hostname
|
196
|
+
@hostname ||= `hostname`.strip
|
197
|
+
end
|
198
|
+
|
199
|
+
def schedule_info(klass)
|
200
|
+
MiniScheduler::ScheduleInfo.new(klass, self)
|
201
|
+
end
|
202
|
+
|
203
|
+
def next_run(klass)
|
204
|
+
schedule_info(klass).next_run
|
205
|
+
end
|
206
|
+
|
207
|
+
def ensure_schedule!(klass)
|
208
|
+
lock do
|
209
|
+
schedule_info(klass).schedule!
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def remove(klass)
|
214
|
+
lock do
|
215
|
+
schedule_info(klass).del!
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def reschedule_orphans!
|
220
|
+
lock do
|
221
|
+
reschedule_orphans_on!
|
222
|
+
reschedule_orphans_on!(hostname)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def reschedule_orphans_on!(hostname = nil)
|
227
|
+
redis.zrange(Manager.queue_key(hostname), 0, -1).each do |key|
|
228
|
+
klass = get_klass(key)
|
229
|
+
next unless klass
|
230
|
+
info = schedule_info(klass)
|
231
|
+
|
232
|
+
if ['QUEUED', 'RUNNING'].include?(info.prev_result) &&
|
233
|
+
(info.current_owner.blank? || !redis.get(info.current_owner))
|
234
|
+
info.prev_result = 'ORPHAN'
|
235
|
+
info.next_run = Time.now.to_i
|
236
|
+
info.write!
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def get_klass(name)
|
242
|
+
name.constantize
|
243
|
+
rescue NameError
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
|
247
|
+
def tick
|
248
|
+
lock do
|
249
|
+
schedule_next_job
|
250
|
+
schedule_next_job(hostname)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def schedule_next_job(hostname = nil)
|
255
|
+
(key, due), _ = redis.zrange Manager.queue_key(hostname), 0, 0, withscores: true
|
256
|
+
return unless key
|
257
|
+
|
258
|
+
if due.to_i <= Time.now.to_i
|
259
|
+
klass = get_klass(key)
|
260
|
+
unless klass
|
261
|
+
# corrupt key, nuke it (renamed job or something)
|
262
|
+
redis.zrem Manager.queue_key(hostname), key
|
263
|
+
return
|
264
|
+
end
|
265
|
+
info = schedule_info(klass)
|
266
|
+
info.prev_run = Time.now.to_i
|
267
|
+
info.prev_result = "QUEUED"
|
268
|
+
info.prev_duration = -1
|
269
|
+
info.next_run = nil
|
270
|
+
info.current_owner = identity_key
|
271
|
+
info.schedule!
|
272
|
+
@runner.enq(klass)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def blocking_tick
|
277
|
+
tick
|
278
|
+
@runner.wait_till_done
|
279
|
+
end
|
280
|
+
|
281
|
+
def stop!
|
282
|
+
@runner.stop!
|
283
|
+
self.class.current = nil
|
284
|
+
end
|
285
|
+
|
286
|
+
def keep_alive_duration
|
287
|
+
60
|
288
|
+
end
|
289
|
+
|
290
|
+
def keep_alive
|
291
|
+
redis.setex identity_key, keep_alive_duration, ""
|
292
|
+
end
|
293
|
+
|
294
|
+
def lock
|
295
|
+
MiniScheduler::DistributedMutex.synchronize(Manager.lock_key, MiniScheduler.redis) do
|
296
|
+
yield
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def self.discover_schedules
|
301
|
+
# hack for developemnt reloader is crazytown
|
302
|
+
# multiple classes with same name can be in
|
303
|
+
# object space
|
304
|
+
unique = Set.new
|
305
|
+
schedules = []
|
306
|
+
ObjectSpace.each_object(MiniScheduler::Schedule) do |schedule|
|
307
|
+
if schedule.scheduled?
|
308
|
+
next if unique.include?(schedule.to_s)
|
309
|
+
schedules << schedule
|
310
|
+
unique << schedule.to_s
|
311
|
+
end
|
312
|
+
end
|
313
|
+
schedules
|
314
|
+
end
|
315
|
+
|
316
|
+
@mutex = Mutex.new
|
317
|
+
def self.seq
|
318
|
+
@mutex.synchronize do
|
319
|
+
@i ||= 0
|
320
|
+
@i += 1
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def identity_key
|
325
|
+
@identity_key ||= "_scheduler_#{hostname}:#{Process.pid}:#{self.class.seq}:#{SecureRandom.hex}"
|
326
|
+
end
|
327
|
+
|
328
|
+
def self.lock_key
|
329
|
+
"_scheduler_lock_"
|
330
|
+
end
|
331
|
+
|
332
|
+
def self.queue_key(hostname = nil)
|
333
|
+
if hostname
|
334
|
+
"_scheduler_queue_#{hostname}_"
|
335
|
+
else
|
336
|
+
"_scheduler_queue_"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.schedule_key(klass, hostname = nil)
|
341
|
+
if hostname
|
342
|
+
"_scheduler_#{klass}_#{hostname}"
|
343
|
+
else
|
344
|
+
"_scheduler_#{klass}"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module MiniScheduler::Schedule
|
2
|
+
|
3
|
+
def daily(options = nil)
|
4
|
+
if options
|
5
|
+
@daily = options
|
6
|
+
end
|
7
|
+
@daily
|
8
|
+
end
|
9
|
+
|
10
|
+
def every(duration = nil)
|
11
|
+
if duration
|
12
|
+
@every = duration
|
13
|
+
if manager = MiniScheduler::Manager.current
|
14
|
+
manager.ensure_schedule!(self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
@every
|
18
|
+
end
|
19
|
+
|
20
|
+
# schedule job independently on each host (looking at hostname)
|
21
|
+
def per_host
|
22
|
+
@per_host = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_per_host
|
26
|
+
@per_host
|
27
|
+
end
|
28
|
+
|
29
|
+
def schedule_info
|
30
|
+
manager = MiniScheduler::Manager.without_runner
|
31
|
+
manager.schedule_info self
|
32
|
+
end
|
33
|
+
|
34
|
+
def scheduled?
|
35
|
+
!!@every || !!@daily
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module MiniScheduler
|
2
|
+
class ScheduleInfo
|
3
|
+
attr_accessor :next_run,
|
4
|
+
:prev_run,
|
5
|
+
:prev_duration,
|
6
|
+
:prev_result,
|
7
|
+
:current_owner
|
8
|
+
|
9
|
+
def initialize(klass, manager)
|
10
|
+
@klass = klass
|
11
|
+
@manager = manager
|
12
|
+
|
13
|
+
data = nil
|
14
|
+
|
15
|
+
if data = @manager.redis.get(key)
|
16
|
+
data = JSON.parse(data)
|
17
|
+
end
|
18
|
+
|
19
|
+
if data
|
20
|
+
@next_run = data["next_run"]
|
21
|
+
@prev_run = data["prev_run"]
|
22
|
+
@prev_result = data["prev_result"]
|
23
|
+
@prev_duration = data["prev_duration"]
|
24
|
+
@current_owner = data["current_owner"]
|
25
|
+
end
|
26
|
+
rescue
|
27
|
+
# corrupt redis
|
28
|
+
@next_run = @prev_run = @prev_result = @prev_duration = @current_owner = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# this means the schedule is going to fire, it is setup correctly.
|
32
|
+
# invalid schedules are fixed by running "schedule!"
|
33
|
+
# this happens automatically after if fire by the manager.
|
34
|
+
def valid?
|
35
|
+
return false unless @next_run
|
36
|
+
(!@prev_run && @next_run < Time.now.to_i + 5.minutes) || valid_every? || valid_daily?
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_every?
|
40
|
+
return false unless @klass.every
|
41
|
+
!!@prev_run &&
|
42
|
+
@prev_run <= Time.now.to_i &&
|
43
|
+
@next_run < @prev_run + @klass.every * (1 + @manager.random_ratio)
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_daily?
|
47
|
+
return false unless @klass.daily
|
48
|
+
return true if !@prev_run && @next_run && @next_run <= (Time.now + 1.day).to_i
|
49
|
+
!!@prev_run &&
|
50
|
+
@prev_run <= Time.now.to_i &&
|
51
|
+
@next_run < @prev_run + 1.day
|
52
|
+
end
|
53
|
+
|
54
|
+
def schedule_every!
|
55
|
+
if !valid? && @prev_run
|
56
|
+
mixup = @klass.every * @manager.random_ratio
|
57
|
+
mixup = (mixup * Random.rand - mixup / 2).to_i
|
58
|
+
@next_run = @prev_run + mixup + @klass.every
|
59
|
+
end
|
60
|
+
|
61
|
+
if !valid?
|
62
|
+
@next_run = Time.now.to_i + 5.minutes * Random.rand
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def schedule_daily!
|
67
|
+
return if valid?
|
68
|
+
|
69
|
+
at = @klass.daily[:at] || 0
|
70
|
+
today_begin = Time.now.midnight.to_i
|
71
|
+
today_offset = DateTime.now.seconds_since_midnight
|
72
|
+
|
73
|
+
# If it's later today
|
74
|
+
if at > today_offset
|
75
|
+
@next_run = today_begin + at
|
76
|
+
else
|
77
|
+
# Otherwise do it tomorrow
|
78
|
+
@next_run = today_begin + 1.day + at
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def schedule!
|
83
|
+
if @klass.every
|
84
|
+
schedule_every!
|
85
|
+
elsif @klass.daily
|
86
|
+
schedule_daily!
|
87
|
+
end
|
88
|
+
|
89
|
+
write!
|
90
|
+
end
|
91
|
+
|
92
|
+
def write!
|
93
|
+
|
94
|
+
clear!
|
95
|
+
redis.set key, {
|
96
|
+
next_run: @next_run,
|
97
|
+
prev_run: @prev_run,
|
98
|
+
prev_duration: @prev_duration,
|
99
|
+
prev_result: @prev_result,
|
100
|
+
current_owner: @current_owner
|
101
|
+
}.to_json
|
102
|
+
|
103
|
+
redis.zadd queue_key, @next_run, @klass if @next_run
|
104
|
+
end
|
105
|
+
|
106
|
+
def del!
|
107
|
+
clear!
|
108
|
+
@next_run = @prev_run = @prev_result = @prev_duration = @current_owner = nil
|
109
|
+
end
|
110
|
+
|
111
|
+
def key
|
112
|
+
if @klass.is_per_host
|
113
|
+
Manager.schedule_key(@klass, @manager.hostname)
|
114
|
+
else
|
115
|
+
Manager.schedule_key(@klass)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def queue_key
|
120
|
+
if @klass.is_per_host
|
121
|
+
Manager.queue_key(@manager.hostname)
|
122
|
+
else
|
123
|
+
Manager.queue_key
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def redis
|
128
|
+
@manager.redis
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
def clear!
|
133
|
+
redis.del key
|
134
|
+
redis.zrem queue_key, @klass
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-12">
|
3
|
+
<h3>Scheduler History</h3>
|
4
|
+
</div>
|
5
|
+
</header>
|
6
|
+
|
7
|
+
<div class="container">
|
8
|
+
<div class="row">
|
9
|
+
<div class="col-md-9">
|
10
|
+
<% if @scheduler_stats.length > 0 %>
|
11
|
+
|
12
|
+
<table class="table table-striped table-bordered table-white" style="width: 100%; margin: 0; table-layout:fixed;">
|
13
|
+
<thead>
|
14
|
+
<th style="width: 30%">Job Name</th>
|
15
|
+
<th style="width: 15%">Hostname:Pid</th>
|
16
|
+
<th style="width: 15%">Live Slots delta</th>
|
17
|
+
<th style="width: 15%">Started At</th>
|
18
|
+
<th style="width: 15%">Duration</th>
|
19
|
+
<th style="width: 15%"></th>
|
20
|
+
</thead>
|
21
|
+
<tbody>
|
22
|
+
<% @scheduler_stats.each do |stat| %>
|
23
|
+
<tr>
|
24
|
+
<td><%= stat.name %></td>
|
25
|
+
<td><%= stat.hostname %>:<%= stat.pid %></td>
|
26
|
+
<td>
|
27
|
+
<% if stat.live_slots_start && stat.live_slots_finish %>
|
28
|
+
<%= stat.live_slots_finish - stat.live_slots_start %>
|
29
|
+
<% end %>
|
30
|
+
</td>
|
31
|
+
<td><%= sane_time stat.started_at %></td>
|
32
|
+
<td><%= sane_duration stat.duration_ms %></td>
|
33
|
+
<td>
|
34
|
+
<% if stat.success.nil? %>
|
35
|
+
RUNNING
|
36
|
+
<% elsif !stat.success %>
|
37
|
+
FAILED
|
38
|
+
<% end %>
|
39
|
+
</td>
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</tbody>
|
43
|
+
</table>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
</div>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<% if Sidekiq.respond_to?(:paused?) && Sidekiq.paused? %>
|
3
|
+
<div class="col-sm-12">
|
4
|
+
<div class="alert alert-danger text-center">
|
5
|
+
<h2>SIDEKIQ IS PAUSED!</h2>
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
<% end %>
|
9
|
+
<div class="col-sm-12">
|
10
|
+
<h3>Recurring Jobs <a style='font-size:50%; margin-left: 30px' href='scheduler/history'>history</a></h3>
|
11
|
+
</div>
|
12
|
+
</header>
|
13
|
+
|
14
|
+
<div class="container">
|
15
|
+
<div class="row">
|
16
|
+
|
17
|
+
<div class="col-md-9">
|
18
|
+
<% if @schedules.length > 0 %>
|
19
|
+
<table class="table table-striped table-bordered table-white" style="width: 100%; margin: 0; table-layout:fixed;">
|
20
|
+
<thead>
|
21
|
+
<th style="width: 30%">Worker</th>
|
22
|
+
<th style="width: 15%">Last Run</th>
|
23
|
+
<th style="width: 15%">Last Result</th>
|
24
|
+
<th style="width: 15%">Last Duration</th>
|
25
|
+
<th style="width: 15%">Last Owner</th>
|
26
|
+
<th style="width: 15%">Next Run Due</th>
|
27
|
+
<th style="width: 10%">Actions</th>
|
28
|
+
</thead>
|
29
|
+
<% @schedules.each do |schedule| %>
|
30
|
+
<% @info = schedule.schedule_info %>
|
31
|
+
<tr>
|
32
|
+
<td>
|
33
|
+
<%= schedule %>
|
34
|
+
<td>
|
35
|
+
<% prev = @info.prev_run %>
|
36
|
+
<% if prev.nil? %>
|
37
|
+
Never
|
38
|
+
<% else %>
|
39
|
+
<%= relative_time(Time.at(prev)) %>
|
40
|
+
<% end %>
|
41
|
+
</td>
|
42
|
+
<td>
|
43
|
+
<%= @info.prev_result %>
|
44
|
+
</td>
|
45
|
+
<td>
|
46
|
+
<%= sane_duration @info.prev_duration %>
|
47
|
+
</td>
|
48
|
+
<td>
|
49
|
+
<%= @info.current_owner %>
|
50
|
+
</td>
|
51
|
+
<td>
|
52
|
+
<% next_run = @info.next_run %>
|
53
|
+
<% if next_run.nil? %>
|
54
|
+
Not Scheduled Yet
|
55
|
+
<% else %>
|
56
|
+
<%= relative_time(Time.at(next_run)) %>
|
57
|
+
<% end %>
|
58
|
+
</td>
|
59
|
+
<td>
|
60
|
+
<form action="<%= "#{root_path}scheduler/#{schedule}/trigger" %>" method="post">
|
61
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
62
|
+
<input class="btn btn-danger btn-small" type="submit" name="trigger" value="Trigger" data-confirm="Are you sure you want to trigger this job?" />
|
63
|
+
</form>
|
64
|
+
</td>
|
65
|
+
</tr>
|
66
|
+
<% end %>
|
67
|
+
</table>
|
68
|
+
<% else %>
|
69
|
+
<div class="alert alert-success">No recurring jobs found.</div>
|
70
|
+
<% end %>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Based off sidetiq https://github.com/tobiassvn/sidetiq/blob/master/lib/sidetiq/web.rb
|
2
|
+
module MiniScheduler
|
3
|
+
module Web
|
4
|
+
VIEWS = File.expand_path('views', File.dirname(__FILE__)) unless defined? VIEWS
|
5
|
+
|
6
|
+
def self.registered(app)
|
7
|
+
|
8
|
+
app.helpers do
|
9
|
+
def sane_time(time)
|
10
|
+
return unless time
|
11
|
+
time
|
12
|
+
end
|
13
|
+
|
14
|
+
def sane_duration(duration)
|
15
|
+
return unless duration
|
16
|
+
if duration < 1000
|
17
|
+
"#{duration}ms"
|
18
|
+
elsif duration < 60 * 1000
|
19
|
+
"#{'%.2f' % (duration / 1000.0)} secs"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
app.get "/scheduler" do
|
25
|
+
@schedules = Manager.discover_schedules.sort do |a, b|
|
26
|
+
a_next = a.schedule_info.next_run
|
27
|
+
b_next = b.schedule_info.next_run
|
28
|
+
if a_next && b_next
|
29
|
+
a_next <=> b_next
|
30
|
+
elsif a_next
|
31
|
+
-1
|
32
|
+
else
|
33
|
+
1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: { view_path: VIEWS }
|
37
|
+
end
|
38
|
+
|
39
|
+
app.get "/scheduler/history" do
|
40
|
+
@scheduler_stats = Stat.order('started_at desc').limit(200)
|
41
|
+
erb File.read(File.join(VIEWS, 'history.erb')), locals: { view_path: VIEWS }
|
42
|
+
end
|
43
|
+
|
44
|
+
app.post "/scheduler/:name/trigger" do
|
45
|
+
halt 404 unless (name = params[:name])
|
46
|
+
|
47
|
+
klass = name.constantize
|
48
|
+
info = klass.schedule_info
|
49
|
+
info.next_run = Time.now.to_i
|
50
|
+
info.write!
|
51
|
+
|
52
|
+
redirect "#{root_path}scheduler"
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Sidekiq::Web.register(MiniScheduler::Web)
|
60
|
+
Sidekiq::Web.tabs["Scheduler"] = "scheduler"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "mini_scheduler/engine"
|
2
|
+
require 'mini_scheduler/schedule'
|
3
|
+
require 'mini_scheduler/schedule_info'
|
4
|
+
require 'mini_scheduler/manager'
|
5
|
+
require 'mini_scheduler/distributed_mutex'
|
6
|
+
|
7
|
+
require 'sidekiq/exception_handler'
|
8
|
+
|
9
|
+
module MiniScheduler
|
10
|
+
|
11
|
+
def self.configure
|
12
|
+
yield self
|
13
|
+
end
|
14
|
+
|
15
|
+
class SidekiqExceptionHandler
|
16
|
+
extend Sidekiq::ExceptionHandler
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.job_exception_handler(&blk)
|
20
|
+
@job_exception_handler = blk if blk
|
21
|
+
@job_exception_handler
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.handle_job_exception(ex, context = {})
|
25
|
+
if job_exception_handler
|
26
|
+
job_exception_handler.call(ex, context)
|
27
|
+
else
|
28
|
+
SidekiqExceptionHandler.handle_exception(ex, context)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.redis=(r)
|
33
|
+
@redis = r
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.redis
|
37
|
+
@redis
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.job_ran(&blk)
|
41
|
+
@job_ran = blk if blk
|
42
|
+
@job_ran
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.start
|
46
|
+
manager = Manager.new
|
47
|
+
Manager.discover_schedules.each do |schedule|
|
48
|
+
manager.ensure_schedule!(schedule)
|
49
|
+
end
|
50
|
+
Thread.new do
|
51
|
+
while true
|
52
|
+
begin
|
53
|
+
manager.tick
|
54
|
+
rescue => e
|
55
|
+
# the show must go on
|
56
|
+
handle_job_exception(e, message: "While ticking scheduling manager")
|
57
|
+
end
|
58
|
+
sleep 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "mini_scheduler/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "mini_scheduler"
|
9
|
+
spec.version = MiniScheduler::VERSION
|
10
|
+
spec.authors = ["Sam Saffron", "Neil Lalonde"]
|
11
|
+
spec.email = ["neil.lalonde@discourse.org"]
|
12
|
+
|
13
|
+
spec.summary = %q{Adds recurring jobs for Sidekiq}
|
14
|
+
spec.description = %q{Adds recurring jobs for Sidekiq}
|
15
|
+
spec.homepage = "https://github.com/discourse/mini_scheduler"
|
16
|
+
spec.license = "MIT"
|
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
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
28
|
+
spec.add_development_dependency "pg", "~> 1.0.0"
|
29
|
+
spec.add_development_dependency "guard", "~> 2.14"
|
30
|
+
spec.add_development_dependency "guard-minitest", "~> 2.4"
|
31
|
+
spec.add_development_dependency "activesupport", "~> 5.2"
|
32
|
+
spec.add_development_dependency "rspec"
|
33
|
+
spec.add_development_dependency "mocha"
|
34
|
+
spec.add_development_dependency "mock_redis"
|
35
|
+
spec.add_development_dependency "sidekiq"
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mini_scheduler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Saffron
|
8
|
+
- Neil Lalonde
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-07-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.16'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.16'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '10.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '10.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: minitest
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '5.0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '5.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: pg
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.0.0
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.0.0
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: guard
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '2.14'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '2.14'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: guard-minitest
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '2.4'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '2.4'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: activesupport
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '5.2'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '5.2'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rspec
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: mocha
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: mock_redis
|
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'
|
154
|
+
- !ruby/object:Gem::Dependency
|
155
|
+
name: sidekiq
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
type: :development
|
162
|
+
prerelease: false
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
description: Adds recurring jobs for Sidekiq
|
169
|
+
email:
|
170
|
+
- neil.lalonde@discourse.org
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- ".gitignore"
|
176
|
+
- ".rspec"
|
177
|
+
- ".rubocop.yml"
|
178
|
+
- Gemfile
|
179
|
+
- README.md
|
180
|
+
- app/models/mini_scheduler/stat.rb
|
181
|
+
- lib/generators/mini_scheduler/install/install_generator.rb
|
182
|
+
- lib/generators/mini_scheduler/install/templates/create_mini_scheduler_stats.rb
|
183
|
+
- lib/generators/mini_scheduler/install/templates/mini_scheduler_initializer.rb
|
184
|
+
- lib/mini_scheduler.rb
|
185
|
+
- lib/mini_scheduler/distributed_mutex.rb
|
186
|
+
- lib/mini_scheduler/engine.rb
|
187
|
+
- lib/mini_scheduler/manager.rb
|
188
|
+
- lib/mini_scheduler/schedule.rb
|
189
|
+
- lib/mini_scheduler/schedule_info.rb
|
190
|
+
- lib/mini_scheduler/version.rb
|
191
|
+
- lib/mini_scheduler/views/history.erb
|
192
|
+
- lib/mini_scheduler/views/scheduler.erb
|
193
|
+
- lib/mini_scheduler/web.rb
|
194
|
+
- mini_scheduler.gemspec
|
195
|
+
homepage: https://github.com/discourse/mini_scheduler
|
196
|
+
licenses:
|
197
|
+
- MIT
|
198
|
+
metadata: {}
|
199
|
+
post_install_message:
|
200
|
+
rdoc_options: []
|
201
|
+
require_paths:
|
202
|
+
- lib
|
203
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
213
|
+
requirements: []
|
214
|
+
rubyforge_project:
|
215
|
+
rubygems_version: 2.7.7
|
216
|
+
signing_key:
|
217
|
+
specification_version: 4
|
218
|
+
summary: Adds recurring jobs for Sidekiq
|
219
|
+
test_files: []
|