resque-scheduler 2.5.1 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/funding.yml +4 -0
  4. data/.github/workflows/codeql-analysis.yml +59 -0
  5. data/.github/workflows/rubocop.yml +27 -0
  6. data/.github/workflows/ruby.yml +81 -0
  7. data/AUTHORS.md +25 -0
  8. data/CHANGELOG.md +539 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +26 -2
  11. data/README.md +291 -70
  12. data/Rakefile +8 -19
  13. data/exe/resque-scheduler +5 -0
  14. data/lib/resque/scheduler/cli.rb +147 -0
  15. data/lib/resque/scheduler/configuration.rb +102 -0
  16. data/lib/resque/scheduler/delaying_extensions.rb +371 -0
  17. data/lib/resque/scheduler/env.rb +85 -0
  18. data/lib/resque/scheduler/extension.rb +13 -0
  19. data/lib/resque/scheduler/failure_handler.rb +11 -0
  20. data/lib/resque/scheduler/lock/base.rb +13 -4
  21. data/lib/resque/scheduler/lock/basic.rb +4 -5
  22. data/lib/resque/scheduler/lock/resilient.rb +52 -43
  23. data/lib/resque/scheduler/lock.rb +2 -1
  24. data/lib/resque/scheduler/locking.rb +104 -0
  25. data/lib/resque/scheduler/logger_builder.rb +83 -0
  26. data/lib/resque/scheduler/plugin.rb +31 -0
  27. data/lib/resque/scheduler/scheduling_extensions.rb +142 -0
  28. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +21 -12
  29. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_schedules.erb +1 -1
  30. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +1 -1
  31. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  32. data/lib/{resque_scheduler → resque/scheduler}/server/views/search.erb +4 -7
  33. data/lib/{resque_scheduler → resque/scheduler}/server/views/search_form.erb +1 -5
  34. data/lib/resque/scheduler/server.rb +268 -0
  35. data/lib/resque/scheduler/signal_handling.rb +40 -0
  36. data/lib/{resque_scheduler → resque/scheduler}/tasks.rb +3 -6
  37. data/lib/resque/scheduler/util.rb +39 -0
  38. data/lib/resque/scheduler/version.rb +7 -0
  39. data/lib/resque/scheduler.rb +271 -199
  40. data/lib/resque-scheduler.rb +3 -1
  41. data/resque-scheduler.gemspec +53 -20
  42. metadata +176 -132
  43. data/.gitignore +0 -11
  44. data/.rubocop.yml +0 -129
  45. data/.simplecov +0 -1
  46. data/.travis.yml +0 -21
  47. data/HISTORY.md +0 -226
  48. data/ROADMAP.md +0 -10
  49. data/bin/resque-scheduler +0 -5
  50. data/examples/Rakefile +0 -2
  51. data/examples/config/initializers/resque-web.rb +0 -37
  52. data/examples/dynamic-scheduling/README.md +0 -28
  53. data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +0 -54
  54. data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +0 -9
  55. data/examples/dynamic-scheduling/app/models/user.rb +0 -16
  56. data/examples/dynamic-scheduling/config/resque.yml +0 -4
  57. data/examples/dynamic-scheduling/config/static_schedule.yml +0 -7
  58. data/examples/dynamic-scheduling/lib/tasks/resque.rake +0 -48
  59. data/examples/run-resque-web +0 -3
  60. data/lib/resque/scheduler_locking.rb +0 -91
  61. data/lib/resque_scheduler/cli.rb +0 -160
  62. data/lib/resque_scheduler/logger_builder.rb +0 -70
  63. data/lib/resque_scheduler/plugin.rb +0 -28
  64. data/lib/resque_scheduler/server/views/scheduler.erb +0 -36
  65. data/lib/resque_scheduler/server.rb +0 -182
  66. data/lib/resque_scheduler/util.rb +0 -34
  67. data/lib/resque_scheduler/version.rb +0 -5
  68. data/lib/resque_scheduler.rb +0 -386
  69. data/script/migrate_to_timestamps_set.rb +0 -14
  70. data/tasks/resque_scheduler.rake +0 -2
  71. data/test/cli_test.rb +0 -286
  72. data/test/delayed_queue_test.rb +0 -449
  73. data/test/redis-test.conf +0 -108
  74. data/test/resque-web_test.rb +0 -199
  75. data/test/scheduler_args_test.rb +0 -190
  76. data/test/scheduler_hooks_test.rb +0 -23
  77. data/test/scheduler_locking_test.rb +0 -242
  78. data/test/scheduler_setup_test.rb +0 -95
  79. data/test/scheduler_task_test.rb +0 -35
  80. data/test/scheduler_test.rb +0 -344
  81. data/test/support/redis_instance.rb +0 -134
  82. data/test/test_helper.rb +0 -131
  83. /data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
@@ -0,0 +1,58 @@
1
+ <h1>Schedule</h1>
2
+
3
+ <p class='intro'>
4
+ The list below contains all scheduled jobs. Click &quot;Queue now&quot; to queue
5
+ a job immediately.
6
+ <br/> Server local time: <%= Time.now %>
7
+ <br/> Server Environment: <%= Resque::Scheduler.env %>
8
+ <br/> Current master: <%= Resque.redis.get(Resque::Scheduler.master_lock.key) %>
9
+ </p>
10
+ <p class='intro'>
11
+ The highlighted jobs are skipped for current environment.
12
+ </p>
13
+ <div style="overflow-y: auto; width:100%; padding: 0px 5px;">
14
+ <table>
15
+ <tr>
16
+ <th>Index</th>
17
+ <% if Resque::Scheduler.dynamic %>
18
+ <th></th>
19
+ <% end %>
20
+ <th></th>
21
+ <th>Name</th>
22
+ <th>Description</th>
23
+ <th>Interval</th>
24
+ <th>Class</th>
25
+ <th>Queue</th>
26
+ <th>Arguments</th>
27
+ <th>Last Enqueued</th>
28
+ </tr>
29
+ <% Resque.schedule.keys.sort.each_with_index do |name, index| %>
30
+ <% config = Resque.schedule[name] %>
31
+ <tr style="<%= scheduled_in_this_env?(name) ? '' : 'color: #9F6000;background: #FEEFB3;' %>">
32
+ <td style="padding-left: 15px;"><%= index + 1 %>.</td>
33
+ <% if Resque::Scheduler.dynamic %>
34
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
35
+ <form action="<%= u "/schedule" %>" method="post" style="margin-left: 0">
36
+ <input type="hidden" name="job_name" value="<%= h name %>">
37
+ <input type="hidden" name="_method" value="delete">
38
+ <input type="submit" value="Delete">
39
+ </form>
40
+ </td>
41
+ <% end %>
42
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
43
+ <form action="<%= u "/schedule/requeue" %>" method="post" style="margin-left: 0">
44
+ <input type="hidden" name="job_name" value="<%= h name %>">
45
+ <input type="submit" value="Queue now">
46
+ </form>
47
+ </td>
48
+ <td><%= h name %></td>
49
+ <td><%= h config['description'] %></td>
50
+ <td style="white-space:nowrap"><%= h schedule_interval(config) %></td>
51
+ <td><%= h schedule_class(config) %></td>
52
+ <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
53
+ <td><%= h show_job_arguments(config['args']) %></td>
54
+ <td><%= h Resque.get_last_enqueued_at(name) || 'Never' %></td>
55
+ </tr>
56
+ <% end %>
57
+ </table>
58
+ </div>
@@ -1,5 +1,5 @@
1
1
  <h1>Search Results</h1>
2
- <%= erb File.read(File.join(File.dirname(__FILE__), 'server/views/search_form.erb')) %>
2
+ <%= scheduler_view :search_form, layout: false %>
3
3
  <hr>
4
4
  <% delayed = @jobs.select { |j| j['where_at'] == 'delayed' } %>
5
5
  <h1>Delayed jobs</h1>
@@ -13,17 +13,17 @@
13
13
  </tr>
14
14
  <% delayed.each do |job| %>
15
15
  <tr>
16
- <td>
16
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
17
17
  <form action="<%= u "/delayed/queue_now" %>" method="post">
18
18
  <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
19
19
  <input type="submit" value="Queue now">
20
20
  </form>
21
21
  </td>
22
- <td>
22
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
23
23
  <form action="<%= u "/delayed/cancel_now" %>" method="post">
24
24
  <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
25
25
  <input type="hidden" name="klass" value="<%= job['class'] %>">
26
- <input type="hidden" name="args" value="<%= Resque.encode job['args'] %>">
26
+ <input type="hidden" name="args" value="<%= h(Resque.encode job['args']) %>">
27
27
  <input type="submit" value="Cancel Job">
28
28
  </form>
29
29
  </td>
@@ -33,7 +33,6 @@
33
33
  </tr>
34
34
  <% end %>
35
35
  </table>
36
- </h1>
37
36
 
38
37
  <% queued = @jobs.select { |j| j['where_at'] == 'queued' } %>
39
38
  <h1>Queued jobs</h1>
@@ -68,5 +67,3 @@
68
67
  </tr>
69
68
  <% end %>
70
69
  </table>
71
-
72
-
@@ -1,8 +1,4 @@
1
1
  <form method="POST" action="<%= u 'delayed/search' %>">
2
- <input type='input' name='search' value="<%= params[:search] %>"/>
2
+ <input type='input' name='search' value="<%= h params[:search] %>"/>
3
3
  <input type='submit' value='Search'/>
4
4
  </form>
5
-
6
-
7
-
8
-
@@ -0,0 +1,268 @@
1
+ # vim:fileencoding=utf-8
2
+ require 'resque-scheduler'
3
+ require 'resque/server'
4
+ require 'tilt/erb'
5
+ require 'json'
6
+
7
+ # Extend Resque::Server to add tabs
8
+ module Resque
9
+ module Scheduler
10
+ module Server
11
+ TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S %z'.freeze
12
+
13
+ unless defined?(::Resque::Scheduler::Server::VIEW_PATH)
14
+ VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
15
+ end
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ helpers { include HelperMethods }
20
+ include ServerMethods
21
+
22
+ get('/schedule') { schedule }
23
+ post('/schedule/requeue') { schedule_requeue }
24
+ post('/schedule/requeue_with_params') do
25
+ schedule_requeue_with_params
26
+ end
27
+ delete('/schedule') { delete_schedule }
28
+ get('/delayed') { delayed }
29
+ get('/delayed/jobs/:klass') { delayed_jobs_klass }
30
+ post('/delayed/search') { delayed_search }
31
+ get('/delayed/:timestamp') { delayed_timestamp }
32
+ post('/delayed/queue_now') { delayed_queue_now }
33
+ post('/delayed/cancel_now') { delayed_cancel_now }
34
+ post('/delayed/clear') { delayed_clear }
35
+ end
36
+ end
37
+
38
+ module ServerMethods
39
+ def schedule
40
+ Resque.reload_schedule! if Resque::Scheduler.dynamic
41
+ erb scheduler_template('scheduler')
42
+ end
43
+
44
+ def schedule_requeue
45
+ @job_name = params['job_name'] || params[:job_name]
46
+ config = Resque.schedule[@job_name]
47
+ @parameters = config['parameters'] || config[:parameters]
48
+ if @parameters
49
+ erb scheduler_template('requeue-params')
50
+ else
51
+ Resque::Scheduler.enqueue_from_config(config)
52
+ redirect u('/overview')
53
+ end
54
+ end
55
+
56
+ def schedule_requeue_with_params
57
+ job_name = params['job_name'] || params[:job_name]
58
+ config = Resque.schedule[job_name]
59
+ # Build args hash from post data (removing the job name)
60
+ submitted_args = params.reject do |key, _value|
61
+ key == 'job_name' || key == :job_name
62
+ end
63
+
64
+ # Merge constructed args hash with existing args hash for
65
+ # the job, if it exists
66
+ config_args = config['args'] || config[:args] || {}
67
+ config_args = config_args.merge(submitted_args)
68
+
69
+ # Insert the args hash into config and queue the resque job
70
+ config = config.merge('args' => config_args)
71
+ Resque::Scheduler.enqueue_from_config(config)
72
+ redirect u('/overview')
73
+ end
74
+
75
+ def delete_schedule
76
+ if Resque::Scheduler.dynamic
77
+ job_name = params['job_name'] || params[:job_name]
78
+ Resque.remove_schedule(job_name)
79
+ end
80
+ redirect u('/schedule')
81
+ end
82
+
83
+ def delayed
84
+ erb scheduler_template('delayed')
85
+ end
86
+
87
+ def delayed_jobs_klass
88
+ begin
89
+ klass = Resque::Scheduler::Util.constantize(params[:klass])
90
+ @args = JSON.load(CGI.unescape(params[:args]))
91
+ @timestamps = Resque.scheduled_at(klass, *@args)
92
+ rescue
93
+ @timestamps = []
94
+ end
95
+
96
+ erb scheduler_template('delayed_schedules')
97
+ end
98
+
99
+ def delayed_search
100
+ @jobs = find_job(params[:search])
101
+ erb scheduler_template('search')
102
+ end
103
+
104
+ def delayed_timestamp
105
+ erb scheduler_template('delayed_timestamp')
106
+ end
107
+
108
+ def delayed_queue_now
109
+ timestamp = params['timestamp'].to_i
110
+ formatted_time = Time.at(timestamp).strftime(
111
+ ::Resque::Scheduler::Server::TIMESTAMP_FORMAT
112
+ )
113
+
114
+ if timestamp > 0
115
+ unless Resque::Scheduler.enqueue_next_item(timestamp)
116
+ @error_message = "Unable to remove item at #{formatted_time}"
117
+ end
118
+ else
119
+ @error_message = "Incorrect timestamp #{formatted_time}"
120
+ end
121
+
122
+ erb scheduler_template('delayed')
123
+ end
124
+
125
+ def delayed_cancel_now
126
+ klass = Resque::Scheduler::Util.constantize(params['klass'])
127
+ timestamp = params['timestamp']
128
+ args = Resque.decode params['args']
129
+ Resque.remove_delayed_job_from_timestamp(timestamp, klass, *args)
130
+ redirect u('/delayed')
131
+ end
132
+
133
+ def delayed_clear
134
+ Resque.reset_delayed_queue
135
+ redirect u('delayed')
136
+ end
137
+ end
138
+
139
+ module HelperMethods
140
+ def format_time(t)
141
+ t.strftime(::Resque::Scheduler::Server::TIMESTAMP_FORMAT)
142
+ end
143
+
144
+ def show_job_arguments(args)
145
+ Array(args).map(&:inspect).join("\n")
146
+ end
147
+
148
+ def queue_from_class_name(class_name)
149
+ Resque.queue_from_class(
150
+ Resque::Scheduler::Util.constantize(class_name)
151
+ )
152
+ end
153
+
154
+ def find_job(worker)
155
+ worker = worker.downcase
156
+ results = working_jobs_for_worker(worker)
157
+
158
+ dels = delayed_jobs_for_worker(worker)
159
+ results += dels.select do |j|
160
+ j['class'].downcase.include?(worker) &&
161
+ j.merge!('where_at' => 'delayed')
162
+ end
163
+
164
+ Resque.queues.each do |queue|
165
+ queued = Resque.peek(queue, 0, Resque.size(queue))
166
+ queued = [queued] unless queued.is_a?(Array)
167
+ results += queued.select do |j|
168
+ j['class'].downcase.include?(worker) &&
169
+ j.merge!('queue' => queue, 'where_at' => 'queued')
170
+ end
171
+ end
172
+
173
+ results
174
+ end
175
+
176
+ def schedule_interval(config)
177
+ if config['every']
178
+ schedule_interval_every(config['every'])
179
+ elsif config['cron']
180
+ 'cron: ' + config['cron'].to_s
181
+ else
182
+ 'Not currently scheduled'
183
+ end
184
+ end
185
+
186
+ def schedule_interval_every(every)
187
+ every = [*every]
188
+ s = 'every: ' << every.first
189
+
190
+ return s unless every.length > 1
191
+
192
+ s << ' ('
193
+ meta = every.last.map do |key, value|
194
+ "#{key.to_s.tr('_', ' ')} #{value}"
195
+ end
196
+ s << meta.join(', ') << ')'
197
+ end
198
+
199
+ def schedule_class(config)
200
+ if config['class'].nil? && !config['custom_job_class'].nil?
201
+ config['custom_job_class']
202
+ else
203
+ config['class']
204
+ end
205
+ end
206
+
207
+ def scheduler_template(name)
208
+ File.read(
209
+ File.expand_path("../server/views/#{name}.erb", __FILE__)
210
+ )
211
+ end
212
+
213
+ def scheduled_in_this_env?(name)
214
+ return true if rails_env(name).nil?
215
+ rails_env(name).split(/[\s,]+/).include?(Resque::Scheduler.env)
216
+ end
217
+
218
+ def rails_env(name)
219
+ Resque.schedule[name]['rails_env'] || Resque.schedule[name]['env']
220
+ end
221
+
222
+ def scheduler_view(filename, options = {}, locals = {})
223
+ source = File.read(File.join(VIEW_PATH, "#{filename}.erb"))
224
+ erb source, options, locals
225
+ end
226
+
227
+ private
228
+
229
+ def working_jobs_for_worker(worker)
230
+ [].tap do |results|
231
+ working = [*Resque.working]
232
+ work = working.select do |w|
233
+ w.job && w.job['payload'] &&
234
+ w.job['payload']['class'].downcase.include?(worker)
235
+ end
236
+ work.each do |w|
237
+ results += [
238
+ w.job['payload'].merge(
239
+ 'queue' => w.job['queue'], 'where_at' => 'working'
240
+ )
241
+ ]
242
+ end
243
+ end
244
+ end
245
+
246
+ def delayed_jobs_for_worker(_worker)
247
+ [].tap do |dels|
248
+ schedule_size = Resque.delayed_queue_schedule_size
249
+ Resque.delayed_queue_peek(0, schedule_size).each do |d|
250
+ Resque.delayed_timestamp_peek(
251
+ d, 0, Resque.delayed_timestamp_size(d)
252
+ ).each do |j|
253
+ dels << j.merge!('timestamp' => d)
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ Resque::Server.tabs << 'Schedule'
264
+ Resque::Server.tabs << 'Delayed'
265
+
266
+ Resque::Server.class_eval do
267
+ include Resque::Scheduler::Server
268
+ end
@@ -0,0 +1,40 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module SignalHandling
6
+ attr_writer :signal_queue
7
+
8
+ def signal_queue
9
+ @signal_queue ||= []
10
+ end
11
+
12
+ # For all signals, set the shutdown flag and wait for current
13
+ # poll/enqueuing to finish (should be almost instant). In the
14
+ # case of sleeping, exit immediately.
15
+ def register_signal_handlers
16
+ (Signal.list.keys & %w(INT TERM USR1 USR2 QUIT)).each do |sig|
17
+ trap(sig) do
18
+ signal_queue << sig
19
+ # break sleep in the primary scheduler thread, allowing
20
+ # the signal queue to get processed as soon as possible.
21
+ @th.wakeup if @th && @th.alive?
22
+ end
23
+ end
24
+ end
25
+
26
+ def handle_signals
27
+ loop do
28
+ sig = signal_queue.shift
29
+ break unless sig
30
+ log! "Got #{sig} signal"
31
+ case sig
32
+ when 'INT', 'TERM', 'QUIT' then shutdown
33
+ when 'USR1' then print_schedule
34
+ when 'USR2' then reload_schedule!
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,14 +1,13 @@
1
1
  # vim:fileencoding=utf-8
2
2
 
3
- require 'English'
4
3
  require 'resque/tasks'
5
- require 'resque_scheduler'
4
+ require 'resque-scheduler'
6
5
 
7
6
  namespace :resque do
8
7
  task :setup
9
8
 
10
9
  def scheduler_cli
11
- @scheduler_cli ||= ResqueScheduler::Cli.new(
10
+ @scheduler_cli ||= Resque::Scheduler::Cli.new(
12
11
  %W(#{ENV['RESQUE_SCHEDULER_OPTIONS']})
13
12
  )
14
13
  end
@@ -21,8 +20,6 @@ namespace :resque do
21
20
 
22
21
  task :scheduler_setup do
23
22
  scheduler_cli.parse_options
24
- unless scheduler_cli.pre_setup
25
- Rake::Task['resque:setup'].invoke
26
- end
23
+ Rake::Task['resque:setup'].invoke unless scheduler_cli.pre_setup
27
24
  end
28
25
  end
@@ -0,0 +1,39 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ class Util
6
+ # In order to upgrade to resque(1.25) which has deprecated following
7
+ # methods, we just added these useful helpers back to use in Resque
8
+ # Scheduler. refer to:
9
+ # https://github.com/resque/resque-scheduler/pull/273
10
+
11
+ def self.constantize(camel_cased_word)
12
+ camel_cased_word = camel_cased_word.to_s
13
+
14
+ if camel_cased_word.include?('-')
15
+ camel_cased_word = classify(camel_cased_word)
16
+ end
17
+
18
+ names = camel_cased_word.split('::')
19
+ names.shift if names.empty? || names.first.empty?
20
+
21
+ constant = Object
22
+ names.each do |name|
23
+ args = Module.method(:const_get).arity != 1 ? [false] : []
24
+
25
+ constant = if constant.const_defined?(name, *args)
26
+ constant.const_get(name)
27
+ else
28
+ constant.const_missing(name)
29
+ end
30
+ end
31
+ constant
32
+ end
33
+
34
+ def self.classify(dashed_word)
35
+ dashed_word.split('-').map(&:capitalize).join
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ VERSION = '4.10.2'.freeze
6
+ end
7
+ end