resque-admin-scheduler 1.0.2

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS.md +81 -0
  3. data/CHANGELOG.md +456 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/CONTRIBUTING.md +6 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +23 -0
  8. data/README.md +691 -0
  9. data/Rakefile +21 -0
  10. data/exe/resque-scheduler +5 -0
  11. data/lib/resque/scheduler/cli.rb +147 -0
  12. data/lib/resque/scheduler/configuration.rb +73 -0
  13. data/lib/resque/scheduler/delaying_extensions.rb +324 -0
  14. data/lib/resque/scheduler/env.rb +89 -0
  15. data/lib/resque/scheduler/extension.rb +13 -0
  16. data/lib/resque/scheduler/failure_handler.rb +11 -0
  17. data/lib/resque/scheduler/lock/base.rb +61 -0
  18. data/lib/resque/scheduler/lock/basic.rb +27 -0
  19. data/lib/resque/scheduler/lock/resilient.rb +78 -0
  20. data/lib/resque/scheduler/lock.rb +4 -0
  21. data/lib/resque/scheduler/locking.rb +104 -0
  22. data/lib/resque/scheduler/logger_builder.rb +72 -0
  23. data/lib/resque/scheduler/plugin.rb +31 -0
  24. data/lib/resque/scheduler/scheduling_extensions.rb +141 -0
  25. data/lib/resque/scheduler/server/views/delayed.erb +63 -0
  26. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  27. data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
  28. data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
  29. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  30. data/lib/resque/scheduler/server/views/search.erb +72 -0
  31. data/lib/resque/scheduler/server/views/search_form.erb +8 -0
  32. data/lib/resque/scheduler/server.rb +268 -0
  33. data/lib/resque/scheduler/signal_handling.rb +40 -0
  34. data/lib/resque/scheduler/tasks.rb +25 -0
  35. data/lib/resque/scheduler/util.rb +39 -0
  36. data/lib/resque/scheduler/version.rb +7 -0
  37. data/lib/resque/scheduler.rb +447 -0
  38. data/lib/resque-scheduler.rb +4 -0
  39. data/resque-scheduler.gemspec +60 -0
  40. metadata +330 -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>
@@ -0,0 +1,72 @@
1
+ <h1>Search Results</h1>
2
+ <%= scheduler_view :search_form, layout: false %>
3
+ <hr>
4
+ <% delayed = @jobs.select { |j| j['where_at'] == 'delayed' } %>
5
+ <h1>Delayed jobs</h1>
6
+ <table class='jobs'>
7
+ <tr>
8
+ <th></th>
9
+ <th></th>
10
+ <th>Timestamp</th>
11
+ <th>Class</th>
12
+ <th>Args</th>
13
+ </tr>
14
+ <% delayed.each do |job| %>
15
+ <tr>
16
+ <td>
17
+ <form action="<%= u "/delayed/queue_now" %>" method="post">
18
+ <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
19
+ <input type="submit" value="Queue now">
20
+ </form>
21
+ </td>
22
+ <td>
23
+ <form action="<%= u "/delayed/cancel_now" %>" method="post">
24
+ <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
25
+ <input type="hidden" name="klass" value="<%= job['class'] %>">
26
+ <input type="hidden" name="args" value="<%= h(Resque.encode job['args']) %>">
27
+ <input type="submit" value="Cancel Job">
28
+ </form>
29
+ </td>
30
+ <td class='args'><%= format_time(Time.at(job['timestamp'])) %></td>
31
+ <td class='class'><%= job['class'] %></td>
32
+ <td class='args'><%= h job['args'].inspect %></td>
33
+ </tr>
34
+ <% end %>
35
+ </table>
36
+ </h1>
37
+
38
+ <% queued = @jobs.select { |j| j['where_at'] == 'queued' } %>
39
+ <h1>Queued jobs</h1>
40
+ <table class='jobs'>
41
+ <tr>
42
+ <th>Queue</th>
43
+ <th>Class</th>
44
+ <th>Args</th>
45
+ </tr>
46
+ <% queued.each do |job| %>
47
+ <tr>
48
+ <td class='class'><%= job['queue'] %></td>
49
+ <td class='class'><%= job['class'] %></td>
50
+ <td class='args'><%= h job['args'].inspect %></td>
51
+ </tr>
52
+ <% end %>
53
+ </table>
54
+
55
+ <% working = @jobs.select { |j| j['where_at'] == 'working' } %>
56
+ <h1>Working jobs</h1>
57
+ <table class='jobs'>
58
+ <tr>
59
+ <th>Queue</th>
60
+ <th>Class</th>
61
+ <th>Args</th>
62
+ </tr>
63
+ <% working.each do |job| %>
64
+ <tr>
65
+ <td class='class'><%= job['queue'] %></td>
66
+ <td class='class'><%= job['class'] %></td>
67
+ <td class='args'><%= h job['args'].inspect %></td>
68
+ </tr>
69
+ <% end %>
70
+ </table>
71
+
72
+
@@ -0,0 +1,8 @@
1
+ <form method="POST" action="<%= u 'delayed/search' %>">
2
+ <input type='input' name='search' value="<%= params[:search] %>"/>
3
+ <input type='submit' value='Search'/>
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(URI.decode(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/enqueing 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, alowing
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
@@ -0,0 +1,25 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'resque/tasks'
4
+ require 'resque-scheduler'
5
+
6
+ namespace :resque do
7
+ task :setup
8
+
9
+ def scheduler_cli
10
+ @scheduler_cli ||= Resque::Scheduler::Cli.new(
11
+ %W(#{ENV['RESQUE_SCHEDULER_OPTIONS']})
12
+ )
13
+ end
14
+
15
+ desc 'Start Resque Scheduler'
16
+ task scheduler: :scheduler_setup do
17
+ scheduler_cli.setup_env
18
+ scheduler_cli.run_forever
19
+ end
20
+
21
+ task :scheduler_setup do
22
+ scheduler_cli.parse_options
23
+ Rake::Task['resque:setup'].invoke unless scheduler_cli.pre_setup
24
+ end
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 usefull 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 = '1.0.2'.freeze
6
+ end
7
+ end