resque 1.23.0 → 2.6.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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.md +271 -0
  3. data/README.markdown +454 -484
  4. data/Rakefile +4 -17
  5. data/bin/resque-web +10 -22
  6. data/lib/resque/data_store.rb +335 -0
  7. data/lib/resque/errors.rb +15 -1
  8. data/lib/resque/failure/airbrake.rb +32 -4
  9. data/lib/resque/failure/base.rb +16 -7
  10. data/lib/resque/failure/multiple.rb +26 -8
  11. data/lib/resque/failure/redis.rb +92 -15
  12. data/lib/resque/failure/redis_multi_queue.rb +104 -0
  13. data/lib/resque/failure.rb +62 -32
  14. data/lib/resque/helpers.rb +11 -57
  15. data/lib/resque/job.rb +79 -12
  16. data/lib/resque/log_formatters/quiet_formatter.rb +7 -0
  17. data/lib/resque/log_formatters/verbose_formatter.rb +7 -0
  18. data/lib/resque/log_formatters/very_verbose_formatter.rb +8 -0
  19. data/lib/resque/logging.rb +18 -0
  20. data/lib/resque/plugin.rb +22 -10
  21. data/lib/resque/railtie.rb +10 -0
  22. data/lib/resque/server/public/jquery-3.6.0.min.js +2 -0
  23. data/lib/resque/server/public/jquery.relatize_date.js +4 -4
  24. data/lib/resque/server/public/main.js +3 -0
  25. data/lib/resque/server/public/ranger.js +16 -8
  26. data/lib/resque/server/public/style.css +13 -8
  27. data/lib/resque/server/views/error.erb +1 -1
  28. data/lib/resque/server/views/failed.erb +27 -59
  29. data/lib/resque/server/views/failed_job.erb +50 -0
  30. data/lib/resque/server/views/failed_queues_overview.erb +24 -0
  31. data/lib/resque/server/views/job_class.erb +8 -0
  32. data/lib/resque/server/views/key_sets.erb +2 -4
  33. data/lib/resque/server/views/key_string.erb +1 -1
  34. data/lib/resque/server/views/layout.erb +7 -6
  35. data/lib/resque/server/views/next_more.erb +22 -10
  36. data/lib/resque/server/views/processing.erb +2 -0
  37. data/lib/resque/server/views/queues.erb +22 -13
  38. data/lib/resque/server/views/stats.erb +5 -5
  39. data/lib/resque/server/views/workers.erb +4 -4
  40. data/lib/resque/server/views/working.erb +10 -11
  41. data/lib/resque/server.rb +51 -108
  42. data/lib/resque/server_helper.rb +185 -0
  43. data/lib/resque/stat.rb +19 -7
  44. data/lib/resque/tasks.rb +26 -25
  45. data/lib/resque/thread_signal.rb +24 -0
  46. data/lib/resque/vendor/utf8_util.rb +2 -8
  47. data/lib/resque/version.rb +1 -1
  48. data/lib/resque/web_runner.rb +374 -0
  49. data/lib/resque/worker.rb +487 -163
  50. data/lib/resque.rb +332 -52
  51. data/lib/tasks/redis.rake +11 -11
  52. metadata +169 -149
  53. data/lib/resque/failure/hoptoad.rb +0 -33
  54. data/lib/resque/failure/thoughtbot.rb +0 -33
  55. data/lib/resque/server/public/jquery-1.3.2.min.js +0 -19
  56. data/lib/resque/server/test_helper.rb +0 -19
  57. data/lib/resque/vendor/utf8_util/utf8_util_18.rb +0 -91
  58. data/lib/resque/vendor/utf8_util/utf8_util_19.rb +0 -5
  59. data/test/airbrake_test.rb +0 -27
  60. data/test/hoptoad_test.rb +0 -26
  61. data/test/job_hooks_test.rb +0 -464
  62. data/test/job_plugins_test.rb +0 -230
  63. data/test/plugin_test.rb +0 -116
  64. data/test/redis-test-cluster.conf +0 -115
  65. data/test/redis-test.conf +0 -115
  66. data/test/resque-web_test.rb +0 -59
  67. data/test/resque_failure_redis_test.rb +0 -19
  68. data/test/resque_test.rb +0 -278
  69. data/test/test_helper.rb +0 -178
  70. data/test/worker_test.rb +0 -657
data/Rakefile CHANGED
@@ -11,6 +11,10 @@ def command?(command)
11
11
  system("type #{command} > /dev/null 2>&1")
12
12
  end
13
13
 
14
+ require 'rubygems'
15
+ require 'bundler/setup'
16
+ require 'bundler/gem_tasks'
17
+
14
18
 
15
19
  #
16
20
  # Tests
@@ -51,20 +55,3 @@ begin
51
55
  require 'sdoc_helpers'
52
56
  rescue LoadError
53
57
  end
54
-
55
-
56
- #
57
- # Publishing
58
- #
59
-
60
- desc "Push a new version to Gemcutter"
61
- task :publish do
62
- require 'resque/version'
63
-
64
- sh "gem build resque.gemspec"
65
- sh "gem push resque-#{Resque::Version}.gem"
66
- sh "git tag v#{Resque::Version}"
67
- sh "git push origin v#{Resque::Version}"
68
- sh "git push origin master"
69
- sh "git clean -fd"
70
- end
data/bin/resque-web CHANGED
@@ -1,27 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
- begin
5
- require 'vegas'
6
- rescue LoadError
7
- require 'rubygems'
8
- require 'vegas'
9
- end
10
- require 'resque/server'
11
4
 
12
-
13
- Vegas::Runner.new(Resque::Server, 'resque-web', {
14
- :before_run => lambda {|v|
15
- path = (ENV['RESQUECONFIG'] || v.args.first)
16
- load path.to_s.strip if path
17
- }
18
- }) do |runner, opts, app|
19
- opts.on('-N NAMESPACE', "--namespace NAMESPACE", "set the Redis namespace") {|namespace|
20
- runner.logger.info "Using Redis namespace '#{namespace}'"
21
- Resque.redis.namespace = namespace
22
- }
23
- opts.on('-r redis-connection', "--redis redis-connection", "set the Redis connection string") {|redis_conf|
24
- runner.logger.info "Using Redis connection '#{redis_conf}'"
25
- Resque.redis = redis_conf
26
- }
5
+ if !!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i)
6
+ begin
7
+ require 'win32/process'
8
+ rescue
9
+ puts "Sorry, in order to use resque-web on Windows you need the win32-process gem:",
10
+ "gem install win32-process"
11
+ end
27
12
  end
13
+ require 'resque/web_runner'
14
+
15
+ Resque::WebRunner.new(*ARGV)
@@ -0,0 +1,335 @@
1
+ module Resque
2
+ # An interface between Resque's persistence and the actual
3
+ # implementation.
4
+ class DataStore
5
+ extend Forwardable
6
+
7
+ HEARTBEAT_KEY = "workers:heartbeat"
8
+
9
+ def initialize(redis)
10
+ @redis = redis
11
+ @queue_access = QueueAccess.new(@redis)
12
+ @failed_queue_access = FailedQueueAccess.new(@redis)
13
+ @workers = Workers.new(@redis)
14
+ @stats_access = StatsAccess.new(@redis)
15
+ end
16
+
17
+ def_delegators :@queue_access, :push_to_queue,
18
+ :pop_from_queue,
19
+ :queue_size,
20
+ :peek_in_queue,
21
+ :queue_names,
22
+ :remove_queue,
23
+ :everything_in_queue,
24
+ :remove_from_queue,
25
+ :watch_queue,
26
+ :list_range
27
+
28
+ def_delegators :@failed_queue_access, :add_failed_queue,
29
+ :remove_failed_queue,
30
+ :num_failed,
31
+ :failed_queue_names,
32
+ :push_to_failed_queue,
33
+ :clear_failed_queue,
34
+ :update_item_in_failed_queue,
35
+ :remove_from_failed_queue
36
+ def_delegators :@workers, :worker_ids,
37
+ :workers_map,
38
+ :get_worker_payload,
39
+ :worker_exists?,
40
+ :register_worker,
41
+ :worker_started,
42
+ :unregister_worker,
43
+ :heartbeat,
44
+ :heartbeat!,
45
+ :remove_heartbeat,
46
+ :all_heartbeats,
47
+ :acquire_pruning_dead_worker_lock,
48
+ :set_worker_payload,
49
+ :worker_start_time,
50
+ :worker_done_working
51
+
52
+ def_delegators :@stats_access, :clear_stat,
53
+ :decrement_stat,
54
+ :increment_stat,
55
+ :stat
56
+
57
+ def decremet_stat(*args)
58
+ warn '[Resque] [Deprecation] Resque::DataStore #decremet_stat method is deprecated (please use #decrement_stat)'
59
+ decrement_stat(*args)
60
+ end
61
+
62
+ # Compatibility with any non-Resque classes that were using Resque.redis as a way to access Redis
63
+ def method_missing(sym,*args,&block)
64
+ # TODO: deprecation warning?
65
+ @redis.send(sym,*args,&block)
66
+ end
67
+
68
+ # make use respond like redis
69
+ def respond_to?(method,include_all=false)
70
+ @redis.respond_to?(method,include_all) || super
71
+ end
72
+
73
+ # Get a string identifying the underlying server.
74
+ # Probably should be private, but was public so must stay public
75
+ def identifier
76
+ @redis.inspect
77
+ end
78
+
79
+ # Force a reconnect to Redis without closing the connection in the parent
80
+ # process after a fork.
81
+ def reconnect
82
+ @redis.disconnect!
83
+ @redis.ping
84
+ nil
85
+ end
86
+
87
+ # Returns an array of all known Resque keys in Redis. Redis' KEYS operation
88
+ # is O(N) for the keyspace, so be careful - this can be slow for big databases.
89
+ def all_resque_keys
90
+ @redis.keys("*").map do |key|
91
+ key.sub("#{@redis.namespace}:", '')
92
+ end
93
+ end
94
+
95
+ def server_time
96
+ time, _ = @redis.time
97
+ Time.at(time)
98
+ end
99
+
100
+ class QueueAccess
101
+ def initialize(redis)
102
+ @redis = redis
103
+ end
104
+ def push_to_queue(queue,encoded_item)
105
+ @redis.pipelined do |piped|
106
+ watch_queue(queue, redis: piped)
107
+ piped.rpush redis_key_for_queue(queue), encoded_item
108
+ end
109
+ end
110
+
111
+ # Pop whatever is on queue
112
+ def pop_from_queue(queue)
113
+ @redis.lpop(redis_key_for_queue(queue))
114
+ end
115
+
116
+ # Get the number of items in the queue
117
+ def queue_size(queue)
118
+ @redis.llen(redis_key_for_queue(queue)).to_i
119
+ end
120
+
121
+ # Examine items in the queue.
122
+ #
123
+ # NOTE: if count is 1, you will get back an object, otherwise you will
124
+ # get an Array. I'm not making this up.
125
+ def peek_in_queue(queue, start = 0, count = 1)
126
+ list_range(redis_key_for_queue(queue), start, count)
127
+ end
128
+
129
+ def queue_names
130
+ Array(@redis.smembers(:queues))
131
+ end
132
+
133
+ def remove_queue(queue)
134
+ @redis.pipelined do |piped|
135
+ piped.srem(:queues, [queue.to_s])
136
+ piped.del(redis_key_for_queue(queue))
137
+ end
138
+ end
139
+
140
+ def everything_in_queue(queue)
141
+ @redis.lrange(redis_key_for_queue(queue), 0, -1)
142
+ end
143
+
144
+ # Remove data from the queue, if it's there, returning the number of removed elements
145
+ def remove_from_queue(queue,data)
146
+ @redis.lrem(redis_key_for_queue(queue), 0, data)
147
+ end
148
+
149
+ # Private: do not call
150
+ def watch_queue(queue, redis: @redis)
151
+ redis.sadd(:queues, [queue.to_s])
152
+ end
153
+
154
+ # Private: do not call
155
+ def list_range(key, start = 0, count = 1)
156
+ if count == 1
157
+ @redis.lindex(key, start)
158
+ else
159
+ Array(@redis.lrange(key, start, start+count-1))
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def redis_key_for_queue(queue)
166
+ "queue:#{queue}"
167
+ end
168
+
169
+ end
170
+
171
+ class FailedQueueAccess
172
+ def initialize(redis)
173
+ @redis = redis
174
+ end
175
+
176
+ def add_failed_queue(failed_queue_name)
177
+ @redis.sadd(:failed_queues, [failed_queue_name])
178
+ end
179
+
180
+ def remove_failed_queue(failed_queue_name=:failed)
181
+ @redis.del(failed_queue_name)
182
+ end
183
+
184
+ def num_failed(failed_queue_name=:failed)
185
+ @redis.llen(failed_queue_name).to_i
186
+ end
187
+
188
+ def failed_queue_names(find_queue_names_in_key=nil)
189
+ if find_queue_names_in_key.nil?
190
+ [:failed]
191
+ else
192
+ Array(@redis.smembers(find_queue_names_in_key))
193
+ end
194
+ end
195
+
196
+ def push_to_failed_queue(data,failed_queue_name=:failed)
197
+ @redis.rpush(failed_queue_name,data)
198
+ end
199
+
200
+ def clear_failed_queue(failed_queue_name=:failed)
201
+ @redis.del(failed_queue_name)
202
+ end
203
+
204
+ def update_item_in_failed_queue(index_in_failed_queue,new_item_data,failed_queue_name=:failed)
205
+ @redis.lset(failed_queue_name, index_in_failed_queue, new_item_data)
206
+ end
207
+
208
+ def remove_from_failed_queue(index_in_failed_queue,failed_queue_name=nil)
209
+ failed_queue_name ||= :failed
210
+ hopefully_unique_value_we_can_use_to_delete_job = ""
211
+ @redis.lset(failed_queue_name, index_in_failed_queue, hopefully_unique_value_we_can_use_to_delete_job)
212
+ @redis.lrem(failed_queue_name, 1, hopefully_unique_value_we_can_use_to_delete_job)
213
+ end
214
+ end
215
+
216
+ class Workers
217
+ def initialize(redis)
218
+ @redis = redis
219
+ end
220
+
221
+ def worker_ids
222
+ Array(@redis.smembers(:workers))
223
+ end
224
+
225
+ # Given a list of worker ids, returns a map of those ids to the worker's value
226
+ # in redis, even if that value maps to nil
227
+ def workers_map(worker_ids)
228
+ redis_keys = worker_ids.map { |id| "worker:#{id}" }
229
+ @redis.mapped_mget(*redis_keys)
230
+ end
231
+
232
+ # return the worker's payload i.e. job
233
+ def get_worker_payload(worker_id)
234
+ @redis.get("worker:#{worker_id}")
235
+ end
236
+
237
+ def worker_exists?(worker_id)
238
+ @redis.sismember(:workers, worker_id)
239
+ end
240
+
241
+ def register_worker(worker)
242
+ @redis.pipelined do |piped|
243
+ piped.sadd(:workers, [worker.id])
244
+ worker_started(worker, redis: piped)
245
+ end
246
+ end
247
+
248
+ def worker_started(worker, redis: @redis)
249
+ redis.set(redis_key_for_worker_start_time(worker), Time.now.to_s)
250
+ end
251
+
252
+ def unregister_worker(worker, &block)
253
+ @redis.pipelined do |piped|
254
+ piped.srem(:workers, [worker.id])
255
+ piped.del(redis_key_for_worker(worker))
256
+ piped.del(redis_key_for_worker_start_time(worker))
257
+ piped.hdel(HEARTBEAT_KEY, worker.to_s)
258
+
259
+ block.call redis: piped
260
+ end
261
+ end
262
+
263
+ def remove_heartbeat(worker)
264
+ @redis.hdel(HEARTBEAT_KEY, worker.to_s)
265
+ end
266
+
267
+ def heartbeat(worker)
268
+ heartbeat = @redis.hget(HEARTBEAT_KEY, worker.to_s)
269
+ heartbeat && Time.parse(heartbeat)
270
+ end
271
+
272
+ def heartbeat!(worker, time)
273
+ @redis.hset(HEARTBEAT_KEY, worker.to_s, time.iso8601)
274
+ end
275
+
276
+ def all_heartbeats
277
+ @redis.hgetall(HEARTBEAT_KEY)
278
+ end
279
+
280
+ def acquire_pruning_dead_worker_lock(worker, expiry)
281
+ @redis.set(redis_key_for_worker_pruning, worker.to_s, :ex => expiry, :nx => true)
282
+ end
283
+
284
+ def set_worker_payload(worker, data)
285
+ @redis.set(redis_key_for_worker(worker), data)
286
+ end
287
+
288
+ def worker_start_time(worker)
289
+ @redis.get(redis_key_for_worker_start_time(worker))
290
+ end
291
+
292
+ def worker_done_working(worker, &block)
293
+ @redis.pipelined do |piped|
294
+ piped.del(redis_key_for_worker(worker))
295
+ block.call redis: piped
296
+ end
297
+ end
298
+
299
+ private
300
+
301
+ def redis_key_for_worker(worker)
302
+ "worker:#{worker}"
303
+ end
304
+
305
+ def redis_key_for_worker_start_time(worker)
306
+ "#{redis_key_for_worker(worker)}:started"
307
+ end
308
+
309
+ def redis_key_for_worker_pruning
310
+ "pruning_dead_workers_in_progress"
311
+ end
312
+ end
313
+
314
+ class StatsAccess
315
+ def initialize(redis)
316
+ @redis = redis
317
+ end
318
+ def stat(stat)
319
+ @redis.get("stat:#{stat}").to_i
320
+ end
321
+
322
+ def increment_stat(stat, by = 1, redis: @redis)
323
+ redis.incrby("stat:#{stat}", by)
324
+ end
325
+
326
+ def decremet_stat(stat, by = 1)
327
+ @redis.decrby("stat:#{stat}", by)
328
+ end
329
+
330
+ def clear_stat(stat, redis: @redis)
331
+ redis.del("stat:#{stat}")
332
+ end
333
+ end
334
+ end
335
+ end
data/lib/resque/errors.rb CHANGED
@@ -6,7 +6,21 @@ module Resque
6
6
  class NoClassError < RuntimeError; end
7
7
 
8
8
  # Raised when a worker was killed while processing a job.
9
- class DirtyExit < RuntimeError; end
9
+ class DirtyExit < RuntimeError
10
+ attr_reader :process_status
11
+
12
+ def initialize(message=nil, process_status=nil)
13
+ @process_status = process_status
14
+ super message
15
+ end
16
+ end
17
+
18
+ class PruneDeadWorkerDirtyExit < DirtyExit
19
+ def initialize(hostname, job)
20
+ job ||= "<Unknown Job>"
21
+ super("Worker #{hostname} did not gracefully exit while processing #{job}")
22
+ end
23
+ end
10
24
 
11
25
  # Raised when child process is TERM'd so job can rescue this to do shutdown work.
12
26
  class TermException < SignalException; end
@@ -4,14 +4,42 @@ rescue LoadError
4
4
  raise "Can't find 'airbrake' gem. Please add it to your Gemfile or install it."
5
5
  end
6
6
 
7
- require 'resque/failure/thoughtbot'
8
-
9
7
  module Resque
10
8
  module Failure
11
9
  class Airbrake < Base
12
- include Resque::Failure::Thoughtbot
10
+ def self.configure(&block)
11
+ Resque.logger.warn "This actually sets global Airbrake configuration, " \
12
+ "which is probably not what you want."
13
+ Resque::Failure.backend = self
14
+ ::Airbrake.configure(&block)
15
+ end
16
+
17
+ def self.count(queue = nil, class_name = nil)
18
+ # We can't get the total # of errors from Airbrake so we fake it
19
+ # by asking Resque how many errors it has seen.
20
+ Stat[:failed]
21
+ end
22
+
23
+ def save
24
+ notify(
25
+ exception,
26
+ parameters: {
27
+ payload_class: payload['class'].to_s,
28
+ payload_args: payload['args'].inspect
29
+ }
30
+ )
31
+ end
32
+
33
+ private
13
34
 
14
- @klass = ::Airbrake
35
+ def notify(exception, options)
36
+ if ::Airbrake.respond_to?(:notify_sync)
37
+ ::Airbrake.notify_sync(exception, options)
38
+ else
39
+ # Older versions of Airbrake (< 5)
40
+ ::Airbrake.notify(exception, options)
41
+ end
42
+ end
15
43
  end
16
44
  end
17
45
  end
@@ -32,27 +32,36 @@ module Resque
32
32
  end
33
33
 
34
34
  # The number of failures.
35
- def self.count
35
+ def self.count(queue = nil, class_name = nil)
36
36
  0
37
37
  end
38
38
 
39
+ # Returns an array of all available failure queues
40
+ def self.queues
41
+ []
42
+ end
43
+
39
44
  # Returns a paginated array of failure objects.
40
- def self.all(start = 0, count = 1)
45
+ def self.all(offset = 0, limit = 1, queue = nil)
41
46
  []
42
47
  end
43
48
 
49
+ # Iterate across failed objects
50
+ def self.each(*args)
51
+ end
52
+
44
53
  # A URL where someone can go to view failures.
45
54
  def self.url
46
55
  end
47
-
56
+
48
57
  # Clear all failure objects
49
- def self.clear
58
+ def self.clear(*args)
50
59
  end
51
-
52
- def self.requeue(index)
60
+
61
+ def self.requeue(*args)
53
62
  end
54
63
 
55
- def self.remove(index)
64
+ def self.remove(*args)
56
65
  end
57
66
 
58
67
  # Logging!
@@ -23,13 +23,23 @@ module Resque
23
23
  end
24
24
 
25
25
  # The number of failures.
26
- def self.count
27
- classes.first.count
26
+ def self.count(*args)
27
+ classes.first.count(*args)
28
+ end
29
+
30
+ # Returns an array of all available failure queues
31
+ def self.queues
32
+ classes.first.queues
28
33
  end
29
34
 
30
35
  # Returns a paginated array of failure objects.
31
- def self.all(start = 0, count = 1)
32
- classes.first.all(start,count)
36
+ def self.all(*args)
37
+ classes.first.all(*args)
38
+ end
39
+
40
+ # Iterate across failed objects
41
+ def self.each(*args, &block)
42
+ classes.first.each(*args, &block)
33
43
  end
34
44
 
35
45
  # A URL where someone can go to view failures.
@@ -38,16 +48,24 @@ module Resque
38
48
  end
39
49
 
40
50
  # Clear all failure objects
41
- def self.clear
42
- classes.first.clear
51
+ def self.clear(*args)
52
+ classes.first.clear(*args)
43
53
  end
44
54
 
45
55
  def self.requeue(*args)
46
56
  classes.first.requeue(*args)
47
57
  end
48
58
 
49
- def self.remove(index)
50
- classes.each { |klass| klass.remove(index) }
59
+ def self.requeue_all
60
+ classes.first.requeue_all
61
+ end
62
+
63
+ def self.requeue_queue(queue)
64
+ classes.first.requeue_queue(queue)
65
+ end
66
+
67
+ def self.remove(index, queue = nil)
68
+ classes.each { |klass| klass.remove(index, queue) }
51
69
  end
52
70
  end
53
71
  end