istox-resque-scheduler 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS.md +87 -0
  3. data/CHANGELOG.md +478 -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 +698 -0
  9. data/Rakefile +21 -0
  10. data/exe/resque-scheduler +5 -0
  11. data/istox-resque-scheduler.gemspec +61 -0
  12. data/lib/resque-scheduler.rb +4 -0
  13. data/lib/resque/scheduler.rb +460 -0
  14. data/lib/resque/scheduler/cli.rb +147 -0
  15. data/lib/resque/scheduler/configuration.rb +73 -0
  16. data/lib/resque/scheduler/delaying_extensions.rb +356 -0
  17. data/lib/resque/scheduler/env.rb +89 -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.rb +4 -0
  21. data/lib/resque/scheduler/lock/base.rb +61 -0
  22. data/lib/resque/scheduler/lock/basic.rb +27 -0
  23. data/lib/resque/scheduler/lock/resilient.rb +78 -0
  24. data/lib/resque/scheduler/locking.rb +104 -0
  25. data/lib/resque/scheduler/logger_builder.rb +72 -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/server.rb +268 -0
  29. data/lib/resque/scheduler/server/views/delayed.erb +63 -0
  30. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  31. data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
  32. data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
  33. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  34. data/lib/resque/scheduler/server/views/search.erb +72 -0
  35. data/lib/resque/scheduler/server/views/search_form.erb +8 -0
  36. data/lib/resque/scheduler/signal_handling.rb +40 -0
  37. data/lib/resque/scheduler/tasks.rb +25 -0
  38. data/lib/resque/scheduler/util.rb +39 -0
  39. data/lib/resque/scheduler/version.rb +7 -0
  40. metadata +343 -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,142 @@
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_env 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
+ # Preventing a reload is optional and available to batch operations
105
+ def remove_schedule(name, reload = true)
106
+ non_persistent_schedules.delete(name)
107
+ redis.hdel(:persistent_schedules, name)
108
+ redis.sadd(:schedules_changed, name)
109
+
110
+ reload_schedule! if reload
111
+ end
112
+
113
+ private
114
+
115
+ # we store our non-persistent schedules in this hash
116
+ def non_persistent_schedules
117
+ @non_persistent_schedules ||= {}
118
+ end
119
+
120
+ # reads the persistent schedules from redis
121
+ def persistent_schedules
122
+ redis.hgetall(:persistent_schedules).tap do |h|
123
+ h.each do |name, config|
124
+ h[name] = decode(config)
125
+ end
126
+ end
127
+ end
128
+
129
+ def prepare_schedules(schedule_hash)
130
+ prepared_hash = {}
131
+ schedule_hash.each do |name, job_spec|
132
+ job_spec = job_spec.dup
133
+ unless job_spec.key?('class') || job_spec.key?(:class)
134
+ job_spec['class'] = name
135
+ end
136
+ prepared_hash[name] = job_spec
137
+ end
138
+ prepared_hash
139
+ end
140
+ end
141
+ end
142
+ 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/#{URI.escape(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 %>