roundhouse-x 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (168) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +16 -0
  4. data/3.0-Upgrade.md +70 -0
  5. data/Changes.md +1127 -0
  6. data/Gemfile +27 -0
  7. data/LICENSE +7 -0
  8. data/README.md +52 -0
  9. data/Rakefile +9 -0
  10. data/bin/roundhouse +19 -0
  11. data/bin/roundhousectl +93 -0
  12. data/lib/generators/roundhouse/templates/worker.rb.erb +9 -0
  13. data/lib/generators/roundhouse/templates/worker_spec.rb.erb +6 -0
  14. data/lib/generators/roundhouse/templates/worker_test.rb.erb +8 -0
  15. data/lib/generators/roundhouse/worker_generator.rb +49 -0
  16. data/lib/roundhouse/actor.rb +39 -0
  17. data/lib/roundhouse/api.rb +859 -0
  18. data/lib/roundhouse/cli.rb +396 -0
  19. data/lib/roundhouse/client.rb +210 -0
  20. data/lib/roundhouse/core_ext.rb +105 -0
  21. data/lib/roundhouse/exception_handler.rb +30 -0
  22. data/lib/roundhouse/fetch.rb +154 -0
  23. data/lib/roundhouse/launcher.rb +98 -0
  24. data/lib/roundhouse/logging.rb +104 -0
  25. data/lib/roundhouse/manager.rb +236 -0
  26. data/lib/roundhouse/middleware/chain.rb +149 -0
  27. data/lib/roundhouse/middleware/i18n.rb +41 -0
  28. data/lib/roundhouse/middleware/server/active_record.rb +13 -0
  29. data/lib/roundhouse/middleware/server/logging.rb +40 -0
  30. data/lib/roundhouse/middleware/server/retry_jobs.rb +206 -0
  31. data/lib/roundhouse/monitor.rb +124 -0
  32. data/lib/roundhouse/paginator.rb +42 -0
  33. data/lib/roundhouse/processor.rb +159 -0
  34. data/lib/roundhouse/rails.rb +24 -0
  35. data/lib/roundhouse/redis_connection.rb +77 -0
  36. data/lib/roundhouse/scheduled.rb +115 -0
  37. data/lib/roundhouse/testing/inline.rb +28 -0
  38. data/lib/roundhouse/testing.rb +193 -0
  39. data/lib/roundhouse/util.rb +68 -0
  40. data/lib/roundhouse/version.rb +3 -0
  41. data/lib/roundhouse/web.rb +264 -0
  42. data/lib/roundhouse/web_helpers.rb +249 -0
  43. data/lib/roundhouse/worker.rb +90 -0
  44. data/lib/roundhouse.rb +177 -0
  45. data/roundhouse.gemspec +27 -0
  46. data/test/config.yml +9 -0
  47. data/test/env_based_config.yml +11 -0
  48. data/test/fake_env.rb +0 -0
  49. data/test/fixtures/en.yml +2 -0
  50. data/test/helper.rb +49 -0
  51. data/test/test_api.rb +521 -0
  52. data/test/test_cli.rb +389 -0
  53. data/test/test_client.rb +294 -0
  54. data/test/test_exception_handler.rb +55 -0
  55. data/test/test_fetch.rb +206 -0
  56. data/test/test_logging.rb +34 -0
  57. data/test/test_manager.rb +169 -0
  58. data/test/test_middleware.rb +160 -0
  59. data/test/test_monitor.rb +258 -0
  60. data/test/test_processor.rb +176 -0
  61. data/test/test_rails.rb +23 -0
  62. data/test/test_redis_connection.rb +127 -0
  63. data/test/test_retry.rb +390 -0
  64. data/test/test_roundhouse.rb +87 -0
  65. data/test/test_scheduled.rb +120 -0
  66. data/test/test_scheduling.rb +75 -0
  67. data/test/test_testing.rb +78 -0
  68. data/test/test_testing_fake.rb +240 -0
  69. data/test/test_testing_inline.rb +65 -0
  70. data/test/test_util.rb +18 -0
  71. data/test/test_web.rb +605 -0
  72. data/test/test_web_helpers.rb +52 -0
  73. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  74. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  75. data/web/assets/images/logo.png +0 -0
  76. data/web/assets/images/status/active.png +0 -0
  77. data/web/assets/images/status/idle.png +0 -0
  78. data/web/assets/images/status-sd8051fd480.png +0 -0
  79. data/web/assets/javascripts/application.js +83 -0
  80. data/web/assets/javascripts/dashboard.js +300 -0
  81. data/web/assets/javascripts/locales/README.md +27 -0
  82. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  83. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  84. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  85. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  86. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  87. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  88. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  89. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  90. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  91. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  92. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  93. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  94. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  95. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  96. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  97. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  98. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  126. data/web/assets/stylesheets/application.css +746 -0
  127. data/web/assets/stylesheets/bootstrap.css +9 -0
  128. data/web/locales/cs.yml +68 -0
  129. data/web/locales/da.yml +68 -0
  130. data/web/locales/de.yml +69 -0
  131. data/web/locales/el.yml +68 -0
  132. data/web/locales/en.yml +77 -0
  133. data/web/locales/es.yml +69 -0
  134. data/web/locales/fr.yml +69 -0
  135. data/web/locales/hi.yml +75 -0
  136. data/web/locales/it.yml +69 -0
  137. data/web/locales/ja.yml +69 -0
  138. data/web/locales/ko.yml +68 -0
  139. data/web/locales/nl.yml +68 -0
  140. data/web/locales/no.yml +69 -0
  141. data/web/locales/pl.yml +59 -0
  142. data/web/locales/pt-br.yml +68 -0
  143. data/web/locales/pt.yml +67 -0
  144. data/web/locales/ru.yml +75 -0
  145. data/web/locales/sv.yml +68 -0
  146. data/web/locales/ta.yml +75 -0
  147. data/web/locales/zh-cn.yml +68 -0
  148. data/web/locales/zh-tw.yml +68 -0
  149. data/web/views/_footer.erb +22 -0
  150. data/web/views/_job_info.erb +84 -0
  151. data/web/views/_nav.erb +66 -0
  152. data/web/views/_paging.erb +23 -0
  153. data/web/views/_poll_js.erb +5 -0
  154. data/web/views/_poll_link.erb +7 -0
  155. data/web/views/_status.erb +4 -0
  156. data/web/views/_summary.erb +40 -0
  157. data/web/views/busy.erb +90 -0
  158. data/web/views/dashboard.erb +75 -0
  159. data/web/views/dead.erb +34 -0
  160. data/web/views/layout.erb +31 -0
  161. data/web/views/morgue.erb +71 -0
  162. data/web/views/queue.erb +45 -0
  163. data/web/views/queues.erb +27 -0
  164. data/web/views/retries.erb +74 -0
  165. data/web/views/retry.erb +34 -0
  166. data/web/views/scheduled.erb +54 -0
  167. data/web/views/scheduled_job_info.erb +8 -0
  168. metadata +404 -0
@@ -0,0 +1,859 @@
1
+ # encoding: utf-8
2
+ require 'roundhouse'
3
+
4
+ module Roundhouse
5
+ class Stats
6
+ def initialize
7
+ fetch_stats!
8
+ end
9
+
10
+ def in_rotation; stat :in_rotation end
11
+ def num_queues; stat :num_queues end
12
+ def num_empty_queues; stat :num_empty_queues end
13
+ def num_suspended_queues; stat :num_suspended_queues end
14
+ def avg_queue_len; stat :avg_queue_len end
15
+
16
+ def processed
17
+ stat :processed
18
+ end
19
+
20
+ def failed
21
+ stat :failed
22
+ end
23
+
24
+ def scheduled_size
25
+ stat :scheduled_size
26
+ end
27
+
28
+ def retry_size
29
+ stat :retry_size
30
+ end
31
+
32
+ def dead_size
33
+ stat :dead_size
34
+ end
35
+
36
+ def enqueued
37
+ stat :enqueued
38
+ end
39
+
40
+ def processes_size
41
+ stat :processes_size
42
+ end
43
+
44
+ def workers_size
45
+ stat :workers_size
46
+ end
47
+
48
+ def default_queue_latency
49
+ stat :default_queue_latency
50
+ end
51
+
52
+ def queues
53
+ Roundhouse::Stats::Queues.new.lengths
54
+ end
55
+
56
+ def fetch_stats!
57
+ pipe1_res = Roundhouse.redis do |conn|
58
+ conn.pipelined do
59
+ conn.get('stat:processed'.freeze)
60
+ conn.get('stat:failed'.freeze)
61
+ conn.zcard('schedule'.freeze)
62
+ conn.zcard('retry'.freeze)
63
+ conn.zcard('dead'.freeze)
64
+ conn.scard('processes'.freeze)
65
+ conn.llen('semaphore'.freeze)
66
+ conn.smembers('processes'.freeze)
67
+ conn.smembers(Roundhouse::Monitor::BUCKETS)
68
+ end
69
+ end
70
+
71
+ queues_res = Roundhouse.redis do |conn|
72
+ conn.pipelined do
73
+ pipe1_res[8].each { |bucket| conn.hgetall("#{Roundhouse::Monitor::STATUS}:#{bucket}") }
74
+ end
75
+ end
76
+
77
+ all_queue_ids = queues_res.map(&:keys).flatten
78
+
79
+ pipe2_res = Roundhouse.redis do |conn|
80
+ conn.pipelined do
81
+ pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
82
+ all_queue_ids.each {|queue| conn.llen("queue:#{queue}") }
83
+ end
84
+ end
85
+
86
+ s = pipe1_res[7].size
87
+ workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
88
+ enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
89
+
90
+ # Calculate queue status
91
+ all_queue_count = all_queue_ids.size
92
+ empty_queues = 0
93
+ suspended_queues = 0
94
+ queues_res.each do |h|
95
+ h.each do |_,v|
96
+ case v.to_i
97
+ when Roundhouse::Monitor::EMPTY then empty_queues += 1
98
+ when Roundhouse::Monitor::SUSPENDED then suspended_queues +=1
99
+ end
100
+ end
101
+ end
102
+
103
+ avg_queue_len = (all_queue_count == 0 ? nil : enqueued / all_queue_count)
104
+
105
+ #default_queue_latency = if (entry = pipe1_res[6].first)
106
+ # Time.now.to_f - Roundhouse.load_json(entry)['enqueued_at'.freeze]
107
+ # else
108
+ # 0
109
+ # end
110
+
111
+ @stats = {
112
+ in_rotation: pipe1_res[6].to_i,
113
+ processed: pipe1_res[0].to_i,
114
+ failed: pipe1_res[1].to_i,
115
+ scheduled_size: pipe1_res[2],
116
+ retry_size: pipe1_res[3],
117
+ dead_size: pipe1_res[4],
118
+ processes_size: pipe1_res[5],
119
+
120
+ #default_queue_latency: default_queue_latency,
121
+ workers_size: workers_size,
122
+ enqueued: enqueued,
123
+ num_queues: all_queue_count,
124
+ num_empty_queues: empty_queues,
125
+ num_suspended_queues: suspended_queues,
126
+ avg_queue_len: avg_queue_len
127
+ }
128
+ end
129
+
130
+ def reset(*stats)
131
+ all = %w(failed processed)
132
+ stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
133
+
134
+ mset_args = []
135
+ stats.each do |stat|
136
+ mset_args << "stat:#{stat}"
137
+ mset_args << 0
138
+ end
139
+ Roundhouse.redis do |conn|
140
+ conn.mset(*mset_args)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def stat(s)
147
+ @stats[s]
148
+ end
149
+
150
+ class Queues
151
+ def lengths
152
+ Roundhouse.redis do |conn|
153
+ # Refactor note: this set contains the bucket names, not
154
+ # the queue names
155
+ queues = conn.smembers('queues'.freeze)
156
+
157
+ lengths = conn.pipelined do
158
+ queues.each do |queue|
159
+ conn.llen("queue:#{queue}")
160
+ end
161
+ end
162
+
163
+ i = 0
164
+ array_of_arrays = queues.inject({}) do |memo, queue|
165
+ memo[queue] = lengths[i]
166
+ i += 1
167
+ memo
168
+ end.sort_by { |_, size| size }
169
+
170
+ Hash[array_of_arrays.reverse]
171
+ end
172
+ end
173
+ end
174
+
175
+ class History
176
+ def initialize(days_previous, start_date = nil)
177
+ @days_previous = days_previous
178
+ @start_date = start_date || Time.now.utc.to_date
179
+ end
180
+
181
+ def processed
182
+ date_stat_hash("processed")
183
+ end
184
+
185
+ def failed
186
+ date_stat_hash("failed")
187
+ end
188
+
189
+ private
190
+
191
+ def date_stat_hash(stat)
192
+ i = 0
193
+ stat_hash = {}
194
+ keys = []
195
+ dates = []
196
+
197
+ while i < @days_previous
198
+ date = @start_date - i
199
+ datestr = date.strftime("%Y-%m-%d".freeze)
200
+ keys << "stat:#{stat}:#{datestr}"
201
+ dates << datestr
202
+ i += 1
203
+ end
204
+
205
+ Roundhouse.redis do |conn|
206
+ conn.mget(keys).each_with_index do |value, idx|
207
+ stat_hash[dates[idx]] = value ? value.to_i : 0
208
+ end
209
+ end
210
+
211
+ stat_hash
212
+ end
213
+ end
214
+ end
215
+
216
+ ##
217
+ # Encapsulates a queue within Roundhouse.
218
+ # Allows enumeration of all jobs within the queue
219
+ # and deletion of jobs.
220
+ #
221
+ # queue = Roundhouse::Queue.new("mailer")
222
+ # queue.each do |job|
223
+ # job.klass # => 'MyWorker'
224
+ # job.args # => [1, 2, 3]
225
+ # job.delete if job.jid == 'abcdef1234567890'
226
+ # end
227
+ #
228
+ class Queue
229
+ include Enumerable
230
+
231
+ def self.all
232
+ Roundhouse.redis do |c|
233
+ c.smembers(Roundhouse::Monitor::BUCKETS).map { |bucket_num| c.hkeys("#{Roundhouse::Monitor::STATUS}:#{bucket_num}") }
234
+ end.flatten.sort.map {|q| Roundhouse::Queue.new(q) }
235
+ end
236
+
237
+ attr_reader :queue_id
238
+
239
+ def initialize(queue_id)
240
+ @queue_id = queue_id.to_i
241
+ @rname = "queue:#{queue_id}"
242
+ end
243
+
244
+ def status
245
+ case Roundhouse.redis { |conn| Roundhouse::Monitor.queue_status(conn, queue_id) }
246
+ when Roundhouse::Monitor::ACTIVE then :active
247
+ when Roundhouse::Monitor::EMPTY then :empty
248
+ when Roundhouse::Monitor::SUSPENDED then :suspended
249
+ else :unknown
250
+ end
251
+ end
252
+
253
+ def bucket
254
+ Roundhouse::Monitor.status_bucket(queue_id)
255
+ end
256
+
257
+ def size
258
+ Roundhouse.redis { |con| con.llen(@rname) }
259
+ end
260
+
261
+ # Roundhouse Pro overrides this
262
+ def paused?
263
+ false
264
+ end
265
+
266
+ def latency
267
+ entry = Roundhouse.redis do |conn|
268
+ conn.lrange(@rname, -1, -1)
269
+ end.first
270
+ return 0 unless entry
271
+ Time.now.to_f - Roundhouse.load_json(entry)['enqueued_at']
272
+ end
273
+
274
+ def each
275
+ initial_size = size
276
+ deleted_size = 0
277
+ page = 0
278
+ page_size = 50
279
+
280
+ loop do
281
+ range_start = page * page_size - deleted_size
282
+ range_end = page * page_size - deleted_size + (page_size - 1)
283
+ entries = Roundhouse.redis do |conn|
284
+ conn.lrange @rname, range_start, range_end
285
+ end
286
+ break if entries.empty?
287
+ page += 1
288
+ entries.each do |entry|
289
+ yield Job.new(entry, @name)
290
+ end
291
+ deleted_size = initial_size - size
292
+ end
293
+ end
294
+
295
+ def find_job(jid)
296
+ detect { |j| j.jid == jid }
297
+ end
298
+
299
+ def clear
300
+ Roundhouse.redis do |conn|
301
+ conn.multi do
302
+ conn.del(@rname)
303
+ conn.hdel(bucket, queue_id)
304
+ end
305
+ end
306
+ end
307
+ alias_method :💣, :clear
308
+ end
309
+
310
+ ##
311
+ # Encapsulates a pending job within a Roundhouse queue or
312
+ # sorted set.
313
+ #
314
+ # The job should be considered immutable but may be
315
+ # removed from the queue via Job#delete.
316
+ #
317
+ class Job
318
+ attr_reader :item
319
+
320
+ def initialize(item, queue_name=nil)
321
+ @value = item
322
+ @item = item.is_a?(Hash) ? item : Roundhouse.load_json(item)
323
+ @queue_id = queue_name || @item['queue_id']
324
+ end
325
+
326
+ def klass
327
+ @item['class']
328
+ end
329
+
330
+ def display_class
331
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
332
+ @klass ||= case klass
333
+ when /\ARoundhouse::Extensions::Delayed/
334
+ safe_load(args[0], klass) do |target, method, _|
335
+ "#{target}.#{method}"
336
+ end
337
+ when "ActiveJob::QueueAdapters::RoundhouseAdapter::JobWrapper"
338
+ @item['wrapped'] || args[0]
339
+ else
340
+ klass
341
+ end
342
+ end
343
+
344
+ def display_args
345
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
346
+ @args ||= case klass
347
+ when /\ARoundhouse::Extensions::Delayed/
348
+ safe_load(args[0], args) do |_, _, arg|
349
+ arg
350
+ end
351
+ when "ActiveJob::QueueAdapters::RoundhouseAdapter::JobWrapper"
352
+ @item['wrapped'] ? args[0]["arguments"] : []
353
+ else
354
+ args
355
+ end
356
+ end
357
+
358
+ def args
359
+ @item['args']
360
+ end
361
+
362
+ def jid
363
+ @item['jid']
364
+ end
365
+
366
+ def enqueued_at
367
+ @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
368
+ end
369
+
370
+ def created_at
371
+ Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
372
+ end
373
+
374
+ def queue_id
375
+ @queue_id
376
+ end
377
+
378
+ def latency
379
+ Time.now.to_f - (@item['enqueued_at'] || @item['created_at'])
380
+ end
381
+
382
+ ##
383
+ # Remove this job from the queue.
384
+ def delete
385
+ count = Roundhouse.redis do |conn|
386
+ conn.lrem("#{Roundhouse::Monitor::QUEUE}:#{@queue_id}", 1, @value)
387
+ end
388
+ count != 0
389
+ end
390
+
391
+ def [](name)
392
+ @item.__send__(:[], name)
393
+ end
394
+
395
+ private
396
+
397
+ def safe_load(content, default)
398
+ begin
399
+ yield(*YAML.load(content))
400
+ rescue => ex
401
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
402
+ # memory yet so the YAML can't be loaded.
403
+ Roundhouse.logger.warn "Unable to load YAML: #{ex.message}" unless Roundhouse.options[:environment] == 'development'
404
+ default
405
+ end
406
+ end
407
+ end
408
+
409
+ class SortedEntry < Job
410
+ attr_reader :score
411
+ attr_reader :parent
412
+
413
+ def initialize(parent, score, item)
414
+ super(item)
415
+ @score = score
416
+ @parent = parent
417
+ end
418
+
419
+ def at
420
+ Time.at(score).utc
421
+ end
422
+
423
+ def delete
424
+ if @value
425
+ @parent.delete_by_value(@parent.name, @value)
426
+ else
427
+ @parent.delete_by_jid(score, jid)
428
+ end
429
+ end
430
+
431
+ def reschedule(at)
432
+ delete
433
+ @parent.schedule(at, item)
434
+ end
435
+
436
+ def add_to_queue
437
+ remove_job do |message|
438
+ msg = Roundhouse.load_json(message)
439
+ Roundhouse::Client.push(msg)
440
+ end
441
+ end
442
+
443
+ def retry
444
+ raise "Retry not available on jobs which have not failed" unless item["failed_at"]
445
+ remove_job do |message|
446
+ msg = Roundhouse.load_json(message)
447
+ msg['retry_count'] -= 1
448
+ Roundhouse::Client.push(msg)
449
+ end
450
+ end
451
+
452
+ ##
453
+ # Place job in the dead set
454
+ def kill
455
+ raise 'Kill not available on jobs which have not failed' unless item['failed_at']
456
+ remove_job do |message|
457
+ Roundhouse.logger.info { "Killing job #{message['jid']}" }
458
+ now = Time.now.to_f
459
+ Roundhouse.redis do |conn|
460
+ conn.multi do
461
+ conn.zadd('dead', now, message)
462
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
463
+ conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ private
470
+
471
+ def remove_job
472
+ Roundhouse.redis do |conn|
473
+ results = conn.multi do
474
+ conn.zrangebyscore(parent.name, score, score)
475
+ conn.zremrangebyscore(parent.name, score, score)
476
+ end.first
477
+
478
+ if results.size == 1
479
+ yield results.first
480
+ else
481
+ # multiple jobs with the same score
482
+ # find the one with the right JID and push it
483
+ hash = results.group_by do |message|
484
+ if message.index(jid)
485
+ msg = Roundhouse.load_json(message)
486
+ msg['jid'] == jid
487
+ else
488
+ false
489
+ end
490
+ end
491
+
492
+ msg = hash.fetch(true, []).first
493
+ yield msg if msg
494
+
495
+ # push the rest back onto the sorted set
496
+ conn.multi do
497
+ hash.fetch(false, []).each do |message|
498
+ conn.zadd(parent.name, score.to_f.to_s, message)
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end
504
+
505
+ end
506
+
507
+ class SortedSet
508
+ include Enumerable
509
+
510
+ attr_reader :name
511
+
512
+ def initialize(name)
513
+ @name = name
514
+ @_size = size
515
+ end
516
+
517
+ def size
518
+ Roundhouse.redis { |c| c.zcard(name) }
519
+ end
520
+
521
+ def clear
522
+ Roundhouse.redis do |conn|
523
+ conn.del(name)
524
+ end
525
+ end
526
+ alias_method :💣, :clear
527
+ end
528
+
529
+ class JobSet < SortedSet
530
+
531
+ def schedule(timestamp, message)
532
+ Roundhouse.redis do |conn|
533
+ conn.zadd(name, timestamp.to_f.to_s, Roundhouse.dump_json(message))
534
+ end
535
+ end
536
+
537
+ def each
538
+ initial_size = @_size
539
+ offset_size = 0
540
+ page = -1
541
+ page_size = 50
542
+
543
+ loop do
544
+ range_start = page * page_size + offset_size
545
+ range_end = page * page_size + offset_size + (page_size - 1)
546
+ elements = Roundhouse.redis do |conn|
547
+ conn.zrange name, range_start, range_end, with_scores: true
548
+ end
549
+ break if elements.empty?
550
+ page -= 1
551
+ elements.each do |element, score|
552
+ yield SortedEntry.new(self, score, element)
553
+ end
554
+ offset_size = initial_size - @_size
555
+ end
556
+ end
557
+
558
+ def fetch(score, jid = nil)
559
+ elements = Roundhouse.redis do |conn|
560
+ conn.zrangebyscore(name, score, score)
561
+ end
562
+
563
+ elements.inject([]) do |result, element|
564
+ entry = SortedEntry.new(self, score, element)
565
+ if jid
566
+ result << entry if entry.jid == jid
567
+ else
568
+ result << entry
569
+ end
570
+ result
571
+ end
572
+ end
573
+
574
+ def find_job(jid)
575
+ self.detect { |j| j.jid == jid }
576
+ end
577
+
578
+ def delete_by_value(name, value)
579
+ Roundhouse.redis do |conn|
580
+ ret = conn.zrem(name, value)
581
+ @_size -= 1 if ret
582
+ ret
583
+ end
584
+ end
585
+
586
+ def delete_by_jid(score, jid)
587
+ Roundhouse.redis do |conn|
588
+ elements = conn.zrangebyscore(name, score, score)
589
+ elements.each do |element|
590
+ message = Roundhouse.load_json(element)
591
+ if message["jid"] == jid
592
+ ret = conn.zrem(name, element)
593
+ @_size -= 1 if ret
594
+ break ret
595
+ end
596
+ false
597
+ end
598
+ end
599
+ end
600
+
601
+ alias_method :delete, :delete_by_jid
602
+ end
603
+
604
+ ##
605
+ # Allows enumeration of scheduled jobs within Roundhouse.
606
+ # Based on this, you can search/filter for jobs. Here's an
607
+ # example where I'm selecting all jobs of a certain type
608
+ # and deleting them from the retry queue.
609
+ #
610
+ # r = Roundhouse::ScheduledSet.new
611
+ # r.select do |retri|
612
+ # retri.klass == 'Roundhouse::Extensions::DelayedClass' &&
613
+ # retri.args[0] == 'User' &&
614
+ # retri.args[1] == 'setup_new_subscriber'
615
+ # end.map(&:delete)
616
+ class ScheduledSet < JobSet
617
+ def initialize
618
+ super 'schedule'
619
+ end
620
+ end
621
+
622
+ ##
623
+ # Allows enumeration of retries within Roundhouse.
624
+ # Based on this, you can search/filter for jobs. Here's an
625
+ # example where I'm selecting all jobs of a certain type
626
+ # and deleting them from the retry queue.
627
+ #
628
+ # r = Roundhouse::RetrySet.new
629
+ # r.select do |retri|
630
+ # retri.klass == 'Roundhouse::Extensions::DelayedClass' &&
631
+ # retri.args[0] == 'User' &&
632
+ # retri.args[1] == 'setup_new_subscriber'
633
+ # end.map(&:delete)
634
+ class RetrySet < JobSet
635
+ def initialize
636
+ super 'retry'
637
+ end
638
+
639
+ def retry_all
640
+ while size > 0
641
+ each(&:retry)
642
+ end
643
+ end
644
+ end
645
+
646
+ ##
647
+ # Allows enumeration of dead jobs within Roundhouse.
648
+ #
649
+ class DeadSet < JobSet
650
+ def initialize
651
+ super 'dead'
652
+ end
653
+
654
+ def retry_all
655
+ while size > 0
656
+ each(&:retry)
657
+ end
658
+ end
659
+
660
+ def self.max_jobs
661
+ Roundhouse.options[:dead_max_jobs]
662
+ end
663
+
664
+ def self.timeout
665
+ Roundhouse.options[:dead_timeout_in_seconds]
666
+ end
667
+ end
668
+
669
+ ##
670
+ # Enumerates the set of Roundhouse processes which are actively working
671
+ # right now. Each process send a heartbeat to Redis every 5 seconds
672
+ # so this set should be relatively accurate, barring network partitions.
673
+ #
674
+ # Yields a Roundhouse::Process.
675
+ #
676
+
677
+ class ProcessSet
678
+ include Enumerable
679
+
680
+ def initialize(clean_plz=true)
681
+ self.class.cleanup if clean_plz
682
+ end
683
+
684
+ # Cleans up dead processes recorded in Redis.
685
+ # Returns the number of processes cleaned.
686
+ def self.cleanup
687
+ count = 0
688
+ Roundhouse.redis do |conn|
689
+ procs = conn.smembers('processes').sort
690
+ heartbeats = conn.pipelined do
691
+ procs.each do |key|
692
+ conn.hget(key, 'info')
693
+ end
694
+ end
695
+
696
+ # the hash named key has an expiry of 60 seconds.
697
+ # if it's not found, that means the process has not reported
698
+ # in to Redis and probably died.
699
+ to_prune = []
700
+ heartbeats.each_with_index do |beat, i|
701
+ to_prune << procs[i] if beat.nil?
702
+ end
703
+ count = conn.srem('processes', to_prune) unless to_prune.empty?
704
+ end
705
+ count
706
+ end
707
+
708
+ def each
709
+ procs = Roundhouse.redis { |conn| conn.smembers('processes') }.sort
710
+
711
+ Roundhouse.redis do |conn|
712
+ # We're making a tradeoff here between consuming more memory instead of
713
+ # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
714
+ # you'll be happier this way
715
+ result = conn.pipelined do
716
+ procs.each do |key|
717
+ conn.hmget(key, 'info', 'busy', 'beat')
718
+ end
719
+ end
720
+
721
+ result.each do |info, busy, at_s|
722
+ hash = Roundhouse.load_json(info)
723
+ yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f))
724
+ end
725
+ end
726
+
727
+ nil
728
+ end
729
+
730
+ # This method is not guaranteed accurate since it does not prune the set
731
+ # based on current heartbeat. #each does that and ensures the set only
732
+ # contains Roundhouse processes which have sent a heartbeat within the last
733
+ # 60 seconds.
734
+ def size
735
+ Roundhouse.redis { |conn| conn.scard('processes') }
736
+ end
737
+ end
738
+
739
+ #
740
+ # Roundhouse::Process has a set of attributes which look like this:
741
+ #
742
+ # {
743
+ # 'hostname' => 'app-1.example.com',
744
+ # 'started_at' => <process start time>,
745
+ # 'pid' => 12345,
746
+ # 'tag' => 'myapp'
747
+ # 'concurrency' => 25,
748
+ # 'queues' => ['default', 'low'],
749
+ # 'busy' => 10,
750
+ # 'beat' => <last heartbeat>,
751
+ # 'identity' => <unique string identifying the process>,
752
+ # }
753
+ class Process
754
+ def initialize(hash)
755
+ @attribs = hash
756
+ end
757
+
758
+ def tag
759
+ self['tag']
760
+ end
761
+
762
+ def labels
763
+ Array(self['labels'])
764
+ end
765
+
766
+ def [](key)
767
+ @attribs[key]
768
+ end
769
+
770
+ def quiet!
771
+ signal('USR1')
772
+ end
773
+
774
+ def stop!
775
+ signal('TERM')
776
+ end
777
+
778
+ def dump_threads
779
+ signal('TTIN')
780
+ end
781
+
782
+ private
783
+
784
+ def signal(sig)
785
+ key = "#{identity}-signals"
786
+ Roundhouse.redis do |c|
787
+ c.multi do
788
+ c.lpush(key, sig)
789
+ c.expire(key, 60)
790
+ end
791
+ end
792
+ end
793
+
794
+ def identity
795
+ self['identity']
796
+ end
797
+ end
798
+
799
+ ##
800
+ # Programmatic access to the current active worker set.
801
+ #
802
+ # WARNING WARNING WARNING
803
+ #
804
+ # This is live data that can change every millisecond.
805
+ # If you call #size => 5 and then expect #each to be
806
+ # called 5 times, you're going to have a bad time.
807
+ #
808
+ # workers = Roundhouse::Workers.new
809
+ # workers.size => 2
810
+ # workers.each do |process_id, thread_id, work|
811
+ # # process_id is a unique identifier per Roundhouse process
812
+ # # thread_id is a unique identifier per thread
813
+ # # work is a Hash which looks like:
814
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
815
+ # # run_at is an epoch Integer.
816
+ # end
817
+ #
818
+ class Workers
819
+ include Enumerable
820
+
821
+ def each
822
+ Roundhouse.redis do |conn|
823
+ procs = conn.smembers('processes')
824
+ procs.sort.each do |key|
825
+ valid, workers = conn.pipelined do
826
+ conn.exists(key)
827
+ conn.hgetall("#{key}:workers")
828
+ end
829
+ next unless valid
830
+ workers.each_pair do |tid, json|
831
+ yield key, tid, Roundhouse.load_json(json)
832
+ end
833
+ end
834
+ end
835
+ end
836
+
837
+ # Note that #size is only as accurate as Roundhouse's heartbeat,
838
+ # which happens every 5 seconds. It is NOT real-time.
839
+ #
840
+ # Not very efficient if you have lots of Roundhouse
841
+ # processes but the alternative is a global counter
842
+ # which can easily get out of sync with crashy processes.
843
+ def size
844
+ Roundhouse.redis do |conn|
845
+ procs = conn.smembers('processes')
846
+ if procs.empty?
847
+ 0
848
+ else
849
+ conn.pipelined do
850
+ procs.each do |key|
851
+ conn.hget(key, 'busy')
852
+ end
853
+ end.map(&:to_i).inject(:+)
854
+ end
855
+ end
856
+ end
857
+ end
858
+
859
+ end