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,149 @@
1
+ module Roundhouse
2
+ # Middleware is code configured to run before/after
3
+ # a message is processed. It is patterned after Rack
4
+ # middleware. Middleware exists for the client side
5
+ # (pushing jobs onto the queue) as well as the server
6
+ # side (when jobs are actually processed).
7
+ #
8
+ # To add middleware for the client:
9
+ #
10
+ # Roundhouse.configure_client do |config|
11
+ # config.client_middleware do |chain|
12
+ # chain.add MyClientHook
13
+ # end
14
+ # end
15
+ #
16
+ # To modify middleware for the server, just call
17
+ # with another block:
18
+ #
19
+ # Roundhouse.configure_server do |config|
20
+ # config.server_middleware do |chain|
21
+ # chain.add MyServerHook
22
+ # chain.remove ActiveRecord
23
+ # end
24
+ # end
25
+ #
26
+ # To insert immediately preceding another entry:
27
+ #
28
+ # Roundhouse.configure_client do |config|
29
+ # config.client_middleware do |chain|
30
+ # chain.insert_before ActiveRecord, MyClientHook
31
+ # end
32
+ # end
33
+ #
34
+ # To insert immediately after another entry:
35
+ #
36
+ # Roundhouse.configure_client do |config|
37
+ # config.client_middleware do |chain|
38
+ # chain.insert_after ActiveRecord, MyClientHook
39
+ # end
40
+ # end
41
+ #
42
+ # This is an example of a minimal server middleware:
43
+ #
44
+ # class MyServerHook
45
+ # def call(worker_instance, msg, queue)
46
+ # puts "Before work"
47
+ # yield
48
+ # puts "After work"
49
+ # end
50
+ # end
51
+ #
52
+ # This is an example of a minimal client middleware, note
53
+ # the method must return the result or the job will not push
54
+ # to Redis:
55
+ #
56
+ # class MyClientHook
57
+ # def call(worker_class, msg, queue, redis_pool)
58
+ # puts "Before push"
59
+ # result = yield
60
+ # puts "After push"
61
+ # result
62
+ # end
63
+ # end
64
+ #
65
+ module Middleware
66
+ class Chain
67
+ include Enumerable
68
+ attr_reader :entries
69
+
70
+ def initialize_copy(copy)
71
+ copy.instance_variable_set(:@entries, entries.dup)
72
+ end
73
+
74
+ def each(&block)
75
+ entries.each(&block)
76
+ end
77
+
78
+ def initialize
79
+ @entries = []
80
+ yield self if block_given?
81
+ end
82
+
83
+ def remove(klass)
84
+ entries.delete_if { |entry| entry.klass == klass }
85
+ end
86
+
87
+ def add(klass, *args)
88
+ remove(klass) if exists?(klass)
89
+ entries << Entry.new(klass, *args)
90
+ end
91
+
92
+ def prepend(klass, *args)
93
+ remove(klass) if exists?(klass)
94
+ entries.insert(0, Entry.new(klass, *args))
95
+ end
96
+
97
+ def insert_before(oldklass, newklass, *args)
98
+ i = entries.index { |entry| entry.klass == newklass }
99
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
100
+ i = entries.index { |entry| entry.klass == oldklass } || 0
101
+ entries.insert(i, new_entry)
102
+ end
103
+
104
+ def insert_after(oldklass, newklass, *args)
105
+ i = entries.index { |entry| entry.klass == newklass }
106
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
107
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
108
+ entries.insert(i+1, new_entry)
109
+ end
110
+
111
+ def exists?(klass)
112
+ any? { |entry| entry.klass == klass }
113
+ end
114
+
115
+ def retrieve
116
+ map(&:make_new)
117
+ end
118
+
119
+ def clear
120
+ entries.clear
121
+ end
122
+
123
+ def invoke(*args)
124
+ chain = retrieve.dup
125
+ traverse_chain = lambda do
126
+ if chain.empty?
127
+ yield
128
+ else
129
+ chain.shift.call(*args, &traverse_chain)
130
+ end
131
+ end
132
+ traverse_chain.call
133
+ end
134
+ end
135
+
136
+ class Entry
137
+ attr_reader :klass
138
+
139
+ def initialize(klass, *args)
140
+ @klass = klass
141
+ @args = args
142
+ end
143
+
144
+ def make_new
145
+ @klass.new(*@args)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Simple middleware to save the current locale and restore it when the job executes.
3
+ # Use it by requiring it in your initializer:
4
+ #
5
+ # require 'roundhouse/middleware/i18n'
6
+ #
7
+ module Roundhouse::Middleware::I18n
8
+ # Get the current locale and store it in the message
9
+ # to be sent to Roundhouse.
10
+ class Client
11
+ def call(worker_class, msg, queue, redis_pool)
12
+ msg['locale'] ||= I18n.locale
13
+ yield
14
+ end
15
+ end
16
+
17
+ # Pull the msg locale out and set the current thread to use it.
18
+ class Server
19
+ def call(worker, msg, queue)
20
+ I18n.locale = msg['locale'] || I18n.default_locale
21
+ yield
22
+ ensure
23
+ I18n.locale = I18n.default_locale
24
+ end
25
+ end
26
+ end
27
+
28
+ Roundhouse.configure_client do |config|
29
+ config.client_middleware do |chain|
30
+ chain.add Roundhouse::Middleware::I18n::Client
31
+ end
32
+ end
33
+
34
+ Roundhouse.configure_server do |config|
35
+ config.client_middleware do |chain|
36
+ chain.add Roundhouse::Middleware::I18n::Client
37
+ end
38
+ config.server_middleware do |chain|
39
+ chain.add Roundhouse::Middleware::I18n::Server
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Roundhouse
2
+ module Middleware
3
+ module Server
4
+ class ActiveRecord
5
+ def call(*args)
6
+ yield
7
+ ensure
8
+ ::ActiveRecord::Base.clear_active_connections!
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ module Roundhouse
2
+ module Middleware
3
+ module Server
4
+ class Logging
5
+
6
+ def call(worker, item, queue)
7
+ Roundhouse::Logging.with_context(log_context(worker, item)) do
8
+ begin
9
+ start = Time.now
10
+ logger.info { "start" }
11
+ yield
12
+ logger.info { "done: #{elapsed(start)} sec" }
13
+ rescue Exception
14
+ logger.info { "fail: #{elapsed(start)} sec" }
15
+ raise
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
23
+ # attribute to expose the underlying thing.
24
+ def log_context(worker, item)
25
+ klass = item['wrapped'.freeze] || worker.class.to_s
26
+ "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
27
+ end
28
+
29
+ def elapsed(start)
30
+ (Time.now - start).round(3)
31
+ end
32
+
33
+ def logger
34
+ Roundhouse.logger
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,206 @@
1
+ require 'roundhouse/scheduled'
2
+ require 'roundhouse/api'
3
+
4
+ module Roundhouse
5
+ module Middleware
6
+ module Server
7
+ ##
8
+ # Automatically retry jobs that fail in Roundhouse.
9
+ # Roundhouse's retry support assumes a typical development lifecycle:
10
+ #
11
+ # 0. push some code changes with a bug in it
12
+ # 1. bug causes job processing to fail, roundhouse's middleware captures
13
+ # the job and pushes it onto a retry queue
14
+ # 2. roundhouse retries jobs in the retry queue multiple times with
15
+ # an exponential delay, the job continues to fail
16
+ # 3. after a few days, a developer deploys a fix. the job is
17
+ # reprocessed successfully.
18
+ # 4. once retries are exhausted, roundhouse will give up and move the
19
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
20
+ # manually in the Web UI.
21
+ # 5. After 6 months on the DJQ, Roundhouse will discard the job.
22
+ #
23
+ # A job looks like:
24
+ #
25
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
26
+ #
27
+ # The 'retry' option also accepts a number (in place of 'true'):
28
+ #
29
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
30
+ #
31
+ # The job will be retried this number of times before giving up. (If simply
32
+ # 'true', Roundhouse retries 25 times)
33
+ #
34
+ # We'll add a bit more data to the job to support retries:
35
+ #
36
+ # * 'queue' - the queue to use
37
+ # * 'retry_count' - number of times we've retried so far.
38
+ # * 'error_message' - the message from the exception
39
+ # * 'error_class' - the exception class
40
+ # * 'failed_at' - the first time it failed
41
+ # * 'retried_at' - the last time it was retried
42
+ # * 'backtrace' - the number of lines of error backtrace to store
43
+ #
44
+ # We don't store the backtrace by default as that can add a lot of overhead
45
+ # to the job and everyone is using an error service, right?
46
+ #
47
+ # The default number of retry attempts is 25 which works out to about 3 weeks
48
+ # of retries. You can pass a value for the max number of retry attempts when
49
+ # adding the middleware using the options hash:
50
+ #
51
+ # Roundhouse.configure_server do |config|
52
+ # config.server_middleware do |chain|
53
+ # chain.add Roundhouse::Middleware::Server::RetryJobs, :max_retries => 7
54
+ # end
55
+ # end
56
+ #
57
+ # or limit the number of retries for a particular worker with:
58
+ #
59
+ # class MyWorker
60
+ # include Roundhouse::Worker
61
+ # roundhouse_options :retry => 10
62
+ # end
63
+ #
64
+ class RetryJobs
65
+ include Roundhouse::Util
66
+
67
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
68
+
69
+ def initialize(options = {})
70
+ @max_retries = options.fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
71
+ end
72
+
73
+ def call(worker, msg, queue)
74
+ yield
75
+ rescue Roundhouse::Shutdown
76
+ # ignore, will be pushed back onto queue during hard_shutdown
77
+ raise
78
+ rescue Exception => e
79
+ # ignore, will be pushed back onto queue during hard_shutdown
80
+ raise Roundhouse::Shutdown if exception_caused_by_shutdown?(e)
81
+
82
+ raise e unless msg['retry']
83
+ attempt_retry(worker, msg, queue, e)
84
+ end
85
+
86
+ private
87
+
88
+ def attempt_retry(worker, msg, queue, exception)
89
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
90
+
91
+ msg['queue'] = if msg['retry_queue']
92
+ msg['retry_queue']
93
+ else
94
+ queue
95
+ end
96
+
97
+ # App code can stuff all sorts of crazy binary data into the error message
98
+ # that won't convert to JSON.
99
+ m = exception.message[0..10_000]
100
+ if m.respond_to?(:scrub!)
101
+ m.force_encoding("utf-8")
102
+ m.scrub!
103
+ end
104
+
105
+ msg['error_message'] = m
106
+ msg['error_class'] = exception.class.name
107
+ count = if msg['retry_count']
108
+ msg['retried_at'] = Time.now.to_f
109
+ msg['retry_count'] += 1
110
+ else
111
+ msg['failed_at'] = Time.now.to_f
112
+ msg['retry_count'] = 0
113
+ end
114
+
115
+ if msg['backtrace'] == true
116
+ msg['error_backtrace'] = exception.backtrace
117
+ elsif !msg['backtrace']
118
+ # do nothing
119
+ elsif msg['backtrace'].to_i != 0
120
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
121
+ end
122
+
123
+ if count < max_retry_attempts
124
+ delay = delay_for(worker, count)
125
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
126
+ retry_at = Time.now.to_f + delay
127
+ payload = Roundhouse.dump_json(msg)
128
+ Roundhouse.redis do |conn|
129
+ conn.zadd('retry', retry_at.to_s, payload)
130
+ end
131
+ else
132
+ # Goodbye dear message, you (re)tried your best I'm sure.
133
+ retries_exhausted(worker, msg)
134
+ end
135
+
136
+ raise exception
137
+ end
138
+
139
+ def retries_exhausted(worker, msg)
140
+ logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
141
+ begin
142
+ if worker.roundhouse_retries_exhausted_block?
143
+ worker.roundhouse_retries_exhausted_block.call(msg)
144
+ end
145
+ rescue => e
146
+ handle_exception(e, { context: "Error calling retries_exhausted for #{worker.class}", job: msg })
147
+ end
148
+
149
+ send_to_morgue(msg) unless msg['dead'] == false
150
+ end
151
+
152
+ def send_to_morgue(msg)
153
+ Roundhouse.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
154
+ payload = Roundhouse.dump_json(msg)
155
+ now = Time.now.to_f
156
+ Roundhouse.redis do |conn|
157
+ conn.multi do
158
+ conn.zadd('dead', now, payload)
159
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
160
+ conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
161
+ end
162
+ end
163
+ end
164
+
165
+ def retry_attempts_from(msg_retry, default)
166
+ if msg_retry.is_a?(Fixnum)
167
+ msg_retry
168
+ else
169
+ default
170
+ end
171
+ end
172
+
173
+ def delay_for(worker, count)
174
+ worker.roundhouse_retry_in_block? && retry_in(worker, count) || seconds_to_delay(count)
175
+ end
176
+
177
+ # delayed_job uses the same basic formula
178
+ def seconds_to_delay(count)
179
+ (count ** 4) + 15 + (rand(30)*(count+1))
180
+ end
181
+
182
+ def retry_in(worker, count)
183
+ begin
184
+ worker.roundhouse_retry_in_block.call(count)
185
+ rescue Exception => e
186
+ handle_exception(e, { context: "Failure scheduling retry using the defined `roundhouse_retry_in` in #{worker.class.name}, falling back to default" })
187
+ nil
188
+ end
189
+ end
190
+
191
+ def exception_caused_by_shutdown?(e, checked_causes = [])
192
+ # In Ruby 2.1.0 only, check if exception is a result of shutdown.
193
+ return false unless defined?(e.cause)
194
+
195
+ # Handle circular causes
196
+ checked_causes << e.object_id
197
+ return false if checked_causes.include?(e.cause.object_id)
198
+
199
+ e.cause.instance_of?(Roundhouse::Shutdown) ||
200
+ exception_caused_by_shutdown?(e.cause, checked_causes)
201
+ end
202
+
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,124 @@
1
+ module Roundhouse
2
+ # This class implements two things:
3
+ # 1. A queueing semaphore - the fetcher can pop the next available
4
+ # exclusive right to something (such as API request with a given
5
+ # auth token)
6
+ # 2. Track which access right is temporarily suspended
7
+ class Monitor
8
+ ACTIVE = 1
9
+ EMPTY = 0
10
+ SUSPENDED = -1
11
+
12
+ # This helps catch problems with key names at runtime
13
+ SEMAPHORE = 'semaphore'.freeze
14
+ BUCKETS = 'buckets'.freeze
15
+ QUEUE = 'queue'.freeze
16
+ SCHEDULE = 'schedule'.freeze
17
+ STATUS = 'status'.freeze
18
+
19
+ class << self
20
+ # Find the first active queue
21
+ # If nothing is in the rotation, then block
22
+ def pop(conn)
23
+ loop do
24
+ _, q_id = conn.brpop(SEMAPHORE)
25
+ return q_id if queue_status(conn, q_id) == ACTIVE
26
+ end
27
+ end
28
+
29
+ def push(conn, q_id)
30
+ return unless queue_status(conn, q_id) == ACTIVE
31
+ conn.lpush(SEMAPHORE, q_id)
32
+ end
33
+
34
+ # Bulk requeue (push from right). Usually done
35
+ # via Client, when Roundhouse is terminating
36
+ def requeue(conn, q_id, jobs)
37
+ conn.rpush("#{QUEUE}:#{q_id}", jobs)
38
+ end
39
+
40
+ def await_next_job(conn)
41
+ loop do
42
+ queue_id = pop(conn)
43
+ job = pop_job(conn, queue_id)
44
+ return queue_id, job if job
45
+ Roundhouse::Monitor.set_queue_is_empty(conn, queue_id)
46
+ end
47
+ end
48
+
49
+ def pop_job(conn, q_id)
50
+ conn.rpop("#{QUEUE}:#{q_id}")
51
+ end
52
+
53
+ def push_job(conn, payloads)
54
+ return schedule(conn, payloads) if payloads.first['at']
55
+
56
+ q_id = payloads.first['queue_id']
57
+ now = Time.now.to_f
58
+ to_push = payloads.map do |entry|
59
+ entry['enqueued_at'.freeze] = now
60
+ Roundhouse.dump_json(entry)
61
+ end
62
+ conn.lpush("#{QUEUE}:#{q_id}", to_push)
63
+
64
+ maybe_add_to_rotation(conn, q_id)
65
+ end
66
+
67
+ def set_queue_is_empty(conn, q_id)
68
+ set_queue_status(conn, q_id, EMPTY)
69
+ end
70
+
71
+ def activate(conn, q_id)
72
+ set_queue_status(conn, q_id, ACTIVE)
73
+ end
74
+
75
+ def suspend(conn, q_id)
76
+ set_queue_status(conn, q_id, SUSPENDED)
77
+ end
78
+
79
+ def resume(conn, q_id)
80
+ return unless queue_status(conn, q_id) == SUSPENDED
81
+ set_queue_status(conn, q_id, ACTIVE)
82
+ conn.lpush(SEMAPHORE, q_id)
83
+ end
84
+
85
+ def queue_status(conn, q_id)
86
+ conn.hget(status_bucket(q_id), q_id).to_i || EMPTY
87
+ end
88
+
89
+ def maybe_add_to_rotation(conn, q_id)
90
+ # NOTE: this really should be written in LUA to make
91
+ # sure this is set to ACTIVE after pushing it into the
92
+ # queuing semaphore. Otherwise, race conditions might
93
+ # creep in giving this queue an unfair advantage.
94
+ # See: https://github.com/resque/redis-namespace/blob/master/lib/redis/namespace.rb#L403-L413
95
+ # See: https://www.redisgreen.net/blog/intro-to-lua-for-redis-programmers/
96
+ return false unless queue_status(conn, q_id) == EMPTY
97
+ activate(conn, q_id)
98
+ conn.lpush(SEMAPHORE, q_id)
99
+ end
100
+
101
+ def status_bucket(q_id)
102
+ "#{STATUS}:#{bucket_num(q_id)}"
103
+ end
104
+
105
+ def bucket_num(q_id)
106
+ q_id.to_i / 1000
107
+ end
108
+
109
+ private
110
+
111
+ def schedule(conn, payloads)
112
+ conn.zadd(SCHEDULE.freeze, payloads.map do |hash|
113
+ at = hash.delete('at'.freeze).to_s
114
+ [at, Roundhouse.dump_json(hash)]
115
+ end )
116
+ end
117
+
118
+ def set_queue_status(conn, q_id, status)
119
+ conn.sadd(BUCKETS, bucket_num(q_id))
120
+ conn.hset(status_bucket(q_id), q_id, status)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,42 @@
1
+ module Roundhouse
2
+ module Paginator
3
+
4
+ def page(key, pageidx=1, page_size=25, opts=nil)
5
+ current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
6
+ pageidx = current_page - 1
7
+ total_size = 0
8
+ items = []
9
+ starting = pageidx * page_size
10
+ ending = starting + page_size - 1
11
+
12
+ Roundhouse.redis do |conn|
13
+ type = conn.type(key)
14
+
15
+ case type
16
+ when 'zset'
17
+ rev = opts && opts[:reverse]
18
+ total_size, items = conn.multi do
19
+ conn.zcard(key)
20
+ if rev
21
+ conn.zrevrange(key, starting, ending, :with_scores => true)
22
+ else
23
+ conn.zrange(key, starting, ending, :with_scores => true)
24
+ end
25
+ end
26
+ [current_page, total_size, items]
27
+ when 'list'
28
+ total_size, items = conn.multi do
29
+ conn.llen(key)
30
+ conn.lrange(key, starting, ending)
31
+ end
32
+ [current_page, total_size, items]
33
+ when 'none'
34
+ [1, 0, []]
35
+ else
36
+ raise "can't page a #{type}"
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end