resque 1.23.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
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