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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/bin/migrate_to_timestamps_set.rb +16 -0
  3. data/exe/resque-scheduler +5 -0
  4. data/lib/resque-scheduler.rb +4 -0
  5. data/lib/resque/scheduler.rb +447 -0
  6. data/lib/resque/scheduler/cli.rb +147 -0
  7. data/lib/resque/scheduler/configuration.rb +73 -0
  8. data/lib/resque/scheduler/delaying_extensions.rb +324 -0
  9. data/lib/resque/scheduler/env.rb +89 -0
  10. data/lib/resque/scheduler/extension.rb +13 -0
  11. data/lib/resque/scheduler/failure_handler.rb +11 -0
  12. data/lib/resque/scheduler/lock.rb +4 -0
  13. data/lib/resque/scheduler/lock/base.rb +61 -0
  14. data/lib/resque/scheduler/lock/basic.rb +27 -0
  15. data/lib/resque/scheduler/lock/resilient.rb +78 -0
  16. data/lib/resque/scheduler/locking.rb +104 -0
  17. data/lib/resque/scheduler/logger_builder.rb +72 -0
  18. data/lib/resque/scheduler/plugin.rb +31 -0
  19. data/lib/resque/scheduler/scheduling_extensions.rb +141 -0
  20. data/lib/resque/scheduler/server.rb +268 -0
  21. data/lib/resque/scheduler/server/views/delayed.erb +63 -0
  22. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  23. data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
  24. data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
  25. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  26. data/lib/resque/scheduler/server/views/search.erb +72 -0
  27. data/lib/resque/scheduler/server/views/search_form.erb +8 -0
  28. data/lib/resque/scheduler/signal_handling.rb +40 -0
  29. data/lib/resque/scheduler/tasks.rb +25 -0
  30. data/lib/resque/scheduler/util.rb +39 -0
  31. data/lib/resque/scheduler/version.rb +7 -0
  32. data/tasks/resque_scheduler.rake +2 -0
  33. metadata +267 -0
@@ -0,0 +1,31 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module Plugin
6
+ def self.hooks(job, pattern)
7
+ job.methods.grep(/^#{pattern}/).sort
8
+ end
9
+
10
+ def self.run_hooks(job, pattern, *args)
11
+ results = hooks(job, pattern).map do |hook|
12
+ job.send(hook, *args)
13
+ end
14
+
15
+ results.all? { |result| result != false }
16
+ end
17
+
18
+ def self.run_before_delayed_enqueue_hooks(klass, *args)
19
+ run_hooks(klass, 'before_delayed_enqueue', *args)
20
+ end
21
+
22
+ def self.run_before_schedule_hooks(klass, *args)
23
+ run_hooks(klass, 'before_schedule', *args)
24
+ end
25
+
26
+ def self.run_after_schedule_hooks(klass, *args)
27
+ run_hooks(klass, 'after_schedule', *args)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,141 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module SchedulingExtensions
6
+ # Accepts a new schedule configuration of the form:
7
+ #
8
+ # {
9
+ # "MakeTea" => {
10
+ # "every" => "1m" },
11
+ # "some_name" => {
12
+ # "cron" => "5/* * * *",
13
+ # "class" => "DoSomeWork",
14
+ # "args" => "work on this string",
15
+ # "description" => "this thing works it"s butter off" },
16
+ # ...
17
+ # }
18
+ #
19
+ # Hash keys can be anything and are used to describe and reference
20
+ # the scheduled job. If the "class" argument is missing, the key
21
+ # is used implicitly as "class" argument - in the "MakeTea" example,
22
+ # "MakeTea" is used both as job name and resque worker class.
23
+ #
24
+ # Any jobs that were in the old schedule, but are not
25
+ # present in the new schedule, will be removed.
26
+ #
27
+ # :cron can be any cron scheduling string
28
+ #
29
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every'
30
+ # usage for valid syntax. If :cron is present it will take precedence
31
+ # over :every.
32
+ #
33
+ # :class must be a resque worker class. If it is missing, the job name
34
+ # (hash key) will be used as :class.
35
+ #
36
+ # :args can be any yaml which will be converted to a ruby literal and
37
+ # passed in a params. (optional)
38
+ #
39
+ # :rails_envs is the list of envs where the job gets loaded. Envs are
40
+ # comma separated (optional)
41
+ #
42
+ # :description is just that, a description of the job (optional). If
43
+ # params is an array, each element in the array is passed as a separate
44
+ # param, otherwise params is passed in as the only parameter to
45
+ # perform.
46
+ def schedule=(schedule_hash)
47
+ @non_persistent_schedules = nil
48
+ prepared_schedules = prepare_schedules(schedule_hash)
49
+
50
+ prepared_schedules.each do |schedule, config|
51
+ set_schedule(schedule, config, false)
52
+ end
53
+
54
+ # ensure only return the successfully saved data!
55
+ reload_schedule!
56
+ end
57
+
58
+ # Returns the schedule hash
59
+ def schedule
60
+ @schedule ||= all_schedules
61
+ @schedule || {}
62
+ end
63
+
64
+ # reloads the schedule from redis and memory
65
+ def reload_schedule!
66
+ @schedule = all_schedules
67
+ end
68
+
69
+ # gets the schedules as it exists in redis
70
+ def all_schedules
71
+ non_persistent_schedules.merge(persistent_schedules)
72
+ end
73
+
74
+ # Create or update a schedule with the provided name and configuration.
75
+ #
76
+ # Note: values for class and custom_job_class need to be strings,
77
+ # not constants.
78
+ #
79
+ # Resque.set_schedule('some_job', {:class => 'SomeJob',
80
+ # :every => '15mins',
81
+ # :queue => 'high',
82
+ # :args => '/tmp/poop'})
83
+ #
84
+ # Preventing a reload is optional and available to batch operations
85
+ def set_schedule(name, config, reload = true)
86
+ persist = config.delete(:persist) || config.delete('persist')
87
+
88
+ if persist
89
+ redis.hset(:persistent_schedules, name, encode(config))
90
+ else
91
+ non_persistent_schedules[name] = decode(encode(config))
92
+ end
93
+
94
+ redis.sadd(:schedules_changed, name)
95
+ reload_schedule! if reload
96
+ end
97
+
98
+ # retrive the schedule configuration for the given name
99
+ def fetch_schedule(name)
100
+ schedule[name]
101
+ end
102
+
103
+ # remove a given schedule by name
104
+ def remove_schedule(name)
105
+ non_persistent_schedules.delete(name)
106
+ redis.hdel(:persistent_schedules, name)
107
+ redis.sadd(:schedules_changed, name)
108
+
109
+ reload_schedule!
110
+ end
111
+
112
+ private
113
+
114
+ # we store our non-persistent schedules in this hash
115
+ def non_persistent_schedules
116
+ @non_persistent_schedules ||= {}
117
+ end
118
+
119
+ # reads the persistent schedules from redis
120
+ def persistent_schedules
121
+ redis.hgetall(:persistent_schedules).tap do |h|
122
+ h.each do |name, config|
123
+ h[name] = decode(config)
124
+ end
125
+ end
126
+ end
127
+
128
+ def prepare_schedules(schedule_hash)
129
+ prepared_hash = {}
130
+ schedule_hash.each do |name, job_spec|
131
+ job_spec = job_spec.dup
132
+ unless job_spec.key?('class') || job_spec.key?(:class)
133
+ job_spec['class'] = name
134
+ end
135
+ prepared_hash[name] = job_spec
136
+ end
137
+ prepared_hash
138
+ end
139
+ end
140
+ end
141
+ end
@@ -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,63 @@
1
+ <h1>Delayed Jobs</h1>
2
+ <%- size = resque.delayed_queue_schedule_size %>
3
+
4
+ <%= scheduler_view :search_form, layout: false %>
5
+
6
+ <p style="font-color: red; font-weight: bold;">
7
+ <%= @error_message %>
8
+ </p>
9
+
10
+ <p class='intro'>
11
+ This list below contains the timestamps for scheduled delayed jobs.
12
+ Server local time: <%= Time.now %>
13
+ </p>
14
+
15
+ <p class='sub'>
16
+ Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%= size %></b> timestamps
17
+ </p>
18
+
19
+ <table>
20
+ <tr>
21
+ <th></th>
22
+ <th>Timestamp</th>
23
+ <th>Job count</th>
24
+ <th>Class</th>
25
+ <th>Args</th>
26
+ <th>All schedules</th>
27
+ </tr>
28
+ <% resque.delayed_queue_peek(start, 20).each do |timestamp| %>
29
+ <tr>
30
+ <td>
31
+ <form action="<%= u "/delayed/queue_now" %>" method="post">
32
+ <input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
33
+ <input type="submit" value="Queue now">
34
+ </form>
35
+ </td>
36
+ <td><a href="<%= u "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
37
+ <td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
38
+ <% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
39
+ <td>
40
+ <% if job && delayed_timestamp_size == 1 %>
41
+ <%= h(job['class']) %>
42
+ <% else %>
43
+ <a href="<%= u "delayed/#{timestamp}" %>">see details</a>
44
+ <% end %>
45
+ </td>
46
+ <td><%= h(show_job_arguments(job['args'])) if job && delayed_timestamp_size == 1 %></td>
47
+ <td>
48
+ <% if job %>
49
+ <a href="<%=u URI("/delayed/jobs/#{job['class']}?args=" + URI.encode(job['args'].to_json)) %>">All schedules</a>
50
+ <% end %>
51
+ </td>
52
+ </tr>
53
+ <% end %>
54
+ </table>
55
+
56
+ <% if size > 0 %>
57
+ <br>
58
+ <form method="POST" action="<%=u 'delayed/clear'%>" class='clear-delayed'>
59
+ <input type='submit' name='' value='Clear Delayed Jobs' />
60
+ </form>
61
+ <% end %>
62
+
63
+ <%= partial :next_more, :start => start, :size => size %>