roundhouse-x 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/3.0-Upgrade.md +70 -0
- data/Changes.md +1127 -0
- data/Gemfile +27 -0
- data/LICENSE +7 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/bin/roundhouse +19 -0
- data/bin/roundhousectl +93 -0
- data/lib/generators/roundhouse/templates/worker.rb.erb +9 -0
- data/lib/generators/roundhouse/templates/worker_spec.rb.erb +6 -0
- data/lib/generators/roundhouse/templates/worker_test.rb.erb +8 -0
- data/lib/generators/roundhouse/worker_generator.rb +49 -0
- data/lib/roundhouse/actor.rb +39 -0
- data/lib/roundhouse/api.rb +859 -0
- data/lib/roundhouse/cli.rb +396 -0
- data/lib/roundhouse/client.rb +210 -0
- data/lib/roundhouse/core_ext.rb +105 -0
- data/lib/roundhouse/exception_handler.rb +30 -0
- data/lib/roundhouse/fetch.rb +154 -0
- data/lib/roundhouse/launcher.rb +98 -0
- data/lib/roundhouse/logging.rb +104 -0
- data/lib/roundhouse/manager.rb +236 -0
- data/lib/roundhouse/middleware/chain.rb +149 -0
- data/lib/roundhouse/middleware/i18n.rb +41 -0
- data/lib/roundhouse/middleware/server/active_record.rb +13 -0
- data/lib/roundhouse/middleware/server/logging.rb +40 -0
- data/lib/roundhouse/middleware/server/retry_jobs.rb +206 -0
- data/lib/roundhouse/monitor.rb +124 -0
- data/lib/roundhouse/paginator.rb +42 -0
- data/lib/roundhouse/processor.rb +159 -0
- data/lib/roundhouse/rails.rb +24 -0
- data/lib/roundhouse/redis_connection.rb +77 -0
- data/lib/roundhouse/scheduled.rb +115 -0
- data/lib/roundhouse/testing/inline.rb +28 -0
- data/lib/roundhouse/testing.rb +193 -0
- data/lib/roundhouse/util.rb +68 -0
- data/lib/roundhouse/version.rb +3 -0
- data/lib/roundhouse/web.rb +264 -0
- data/lib/roundhouse/web_helpers.rb +249 -0
- data/lib/roundhouse/worker.rb +90 -0
- data/lib/roundhouse.rb +177 -0
- data/roundhouse.gemspec +27 -0
- data/test/config.yml +9 -0
- data/test/env_based_config.yml +11 -0
- data/test/fake_env.rb +0 -0
- data/test/fixtures/en.yml +2 -0
- data/test/helper.rb +49 -0
- data/test/test_api.rb +521 -0
- data/test/test_cli.rb +389 -0
- data/test/test_client.rb +294 -0
- data/test/test_exception_handler.rb +55 -0
- data/test/test_fetch.rb +206 -0
- data/test/test_logging.rb +34 -0
- data/test/test_manager.rb +169 -0
- data/test/test_middleware.rb +160 -0
- data/test/test_monitor.rb +258 -0
- data/test/test_processor.rb +176 -0
- data/test/test_rails.rb +23 -0
- data/test/test_redis_connection.rb +127 -0
- data/test/test_retry.rb +390 -0
- data/test/test_roundhouse.rb +87 -0
- data/test/test_scheduled.rb +120 -0
- data/test/test_scheduling.rb +75 -0
- data/test/test_testing.rb +78 -0
- data/test/test_testing_fake.rb +240 -0
- data/test/test_testing_inline.rb +65 -0
- data/test/test_util.rb +18 -0
- data/test/test_web.rb +605 -0
- data/test/test_web_helpers.rb +52 -0
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/assets/images/status-sd8051fd480.png +0 -0
- data/web/assets/javascripts/application.js +83 -0
- data/web/assets/javascripts/dashboard.js +300 -0
- data/web/assets/javascripts/locales/README.md +27 -0
- data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
- data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
- data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
- data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
- data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
- data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
- data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
- data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
- data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
- data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
- data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
- data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
- data/web/assets/stylesheets/application.css +746 -0
- data/web/assets/stylesheets/bootstrap.css +9 -0
- data/web/locales/cs.yml +68 -0
- data/web/locales/da.yml +68 -0
- data/web/locales/de.yml +69 -0
- data/web/locales/el.yml +68 -0
- data/web/locales/en.yml +77 -0
- data/web/locales/es.yml +69 -0
- data/web/locales/fr.yml +69 -0
- data/web/locales/hi.yml +75 -0
- data/web/locales/it.yml +69 -0
- data/web/locales/ja.yml +69 -0
- data/web/locales/ko.yml +68 -0
- data/web/locales/nl.yml +68 -0
- data/web/locales/no.yml +69 -0
- data/web/locales/pl.yml +59 -0
- data/web/locales/pt-br.yml +68 -0
- data/web/locales/pt.yml +67 -0
- data/web/locales/ru.yml +75 -0
- data/web/locales/sv.yml +68 -0
- data/web/locales/ta.yml +75 -0
- data/web/locales/zh-cn.yml +68 -0
- data/web/locales/zh-tw.yml +68 -0
- data/web/views/_footer.erb +22 -0
- data/web/views/_job_info.erb +84 -0
- data/web/views/_nav.erb +66 -0
- data/web/views/_paging.erb +23 -0
- data/web/views/_poll_js.erb +5 -0
- data/web/views/_poll_link.erb +7 -0
- data/web/views/_status.erb +4 -0
- data/web/views/_summary.erb +40 -0
- data/web/views/busy.erb +90 -0
- data/web/views/dashboard.erb +75 -0
- data/web/views/dead.erb +34 -0
- data/web/views/layout.erb +31 -0
- data/web/views/morgue.erb +71 -0
- data/web/views/queue.erb +45 -0
- data/web/views/queues.erb +27 -0
- data/web/views/retries.erb +74 -0
- data/web/views/retry.erb +34 -0
- data/web/views/scheduled.erb +54 -0
- data/web/views/scheduled_job_info.erb +8 -0
- metadata +404 -0
data/test/test_retry.rb
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require_relative 'helper'
|
|
3
|
+
require 'roundhouse/scheduled'
|
|
4
|
+
require 'roundhouse/middleware/server/retry_jobs'
|
|
5
|
+
|
|
6
|
+
class TestRetry < Roundhouse::Test
|
|
7
|
+
describe 'middleware' do
|
|
8
|
+
before do
|
|
9
|
+
@redis = Minitest::Mock.new
|
|
10
|
+
# Ugh, this is terrible.
|
|
11
|
+
Roundhouse.instance_variable_set(:@redis, @redis)
|
|
12
|
+
|
|
13
|
+
def @redis.with; yield self; end
|
|
14
|
+
def @redis.multi; yield self; end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
after do
|
|
18
|
+
Roundhouse.redis = REDIS
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
let(:worker) do
|
|
22
|
+
Class.new do
|
|
23
|
+
include ::Roundhouse::Worker
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'allows disabling retry' do
|
|
28
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => false }
|
|
29
|
+
msg2 = msg.dup
|
|
30
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
31
|
+
assert_raises RuntimeError do
|
|
32
|
+
handler.call(worker, msg2, 'default') do
|
|
33
|
+
raise "kerblammo!"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
assert_equal msg, msg2
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'allows a numeric retry' do
|
|
40
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
41
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => 2 }
|
|
42
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
43
|
+
assert_raises RuntimeError do
|
|
44
|
+
handler.call(worker, msg, 'default') do
|
|
45
|
+
raise "kerblammo!"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
msg.delete('failed_at')
|
|
49
|
+
assert_equal({"class"=>"Bob", "args"=>[1, 2, "foo"], "retry"=>2, "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "retry_count"=>0}, msg)
|
|
50
|
+
@redis.verify
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'allows 0 retry => no retry and dead queue' do
|
|
54
|
+
@redis.expect :zadd, 1, ['dead', Float, String]
|
|
55
|
+
@redis.expect :zremrangebyscore, 0, ['dead', String, Float]
|
|
56
|
+
@redis.expect :zremrangebyrank, 0, ['dead', Numeric, Numeric]
|
|
57
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => 0 }
|
|
58
|
+
msg2 = msg.dup
|
|
59
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
60
|
+
assert_raises RuntimeError do
|
|
61
|
+
handler.call(worker, msg, 'default') do
|
|
62
|
+
raise "kerblammo!"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
msg.delete('failed_at')
|
|
66
|
+
expected = msg2.merge "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "retry_count"=>0
|
|
67
|
+
assert_equal expected, msg
|
|
68
|
+
@redis.verify # not called
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'handles zany characters in error message, #1705' do
|
|
72
|
+
skip 'skipped! test requires ruby 2.1+' if RUBY_VERSION <= '2.1.0'
|
|
73
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
74
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => 2 }
|
|
75
|
+
msg2 = msg.dup
|
|
76
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
77
|
+
assert_raises RuntimeError do
|
|
78
|
+
handler.call(worker, msg2, 'default') do
|
|
79
|
+
raise "kerblammo! #{195.chr}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
msg2.delete('failed_at')
|
|
83
|
+
assert_equal({"class"=>"Bob", "args"=>[1, 2, "foo"], "retry"=>2, "queue"=>"default", "error_message"=>"kerblammo! �", "error_class"=>"RuntimeError", "retry_count"=>0}, msg2)
|
|
84
|
+
@redis.verify
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
it 'allows a max_retries option in initializer' do
|
|
89
|
+
max_retries = 7
|
|
90
|
+
1.upto(max_retries) do
|
|
91
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
92
|
+
end
|
|
93
|
+
@redis.expect :zadd, 1, ['dead', Float, String]
|
|
94
|
+
@redis.expect :zremrangebyscore, 0, ['dead', String, Float]
|
|
95
|
+
@redis.expect :zremrangebyrank, 0, ['dead', Numeric, Numeric]
|
|
96
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }
|
|
97
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new({:max_retries => max_retries})
|
|
98
|
+
1.upto(max_retries + 1) do
|
|
99
|
+
assert_raises RuntimeError do
|
|
100
|
+
handler.call(worker, msg, 'default') do
|
|
101
|
+
raise "kerblammo!"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
@redis.verify
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'saves backtraces' do
|
|
109
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
110
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true, 'backtrace' => true }
|
|
111
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
112
|
+
c = nil
|
|
113
|
+
assert_raises RuntimeError do
|
|
114
|
+
handler.call(worker, msg, 'default') do
|
|
115
|
+
c = caller(0); raise "kerblammo!"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
assert msg["error_backtrace"]
|
|
119
|
+
assert_equal c[0], msg["error_backtrace"][0]
|
|
120
|
+
@redis.verify
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'saves partial backtraces' do
|
|
124
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
125
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true, 'backtrace' => 3 }
|
|
126
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
127
|
+
c = nil
|
|
128
|
+
assert_raises RuntimeError do
|
|
129
|
+
handler.call(worker, msg, 'default') do
|
|
130
|
+
c = caller(0)[0...3]; raise "kerblammo!"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
assert msg["error_backtrace"]
|
|
134
|
+
assert_equal c, msg["error_backtrace"]
|
|
135
|
+
assert_equal 3, c.size
|
|
136
|
+
assert_equal 3, msg["error_backtrace"].size
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'handles a new failed message' do
|
|
140
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
141
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }
|
|
142
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
143
|
+
assert_raises RuntimeError do
|
|
144
|
+
handler.call(worker, msg, 'default') do
|
|
145
|
+
raise "kerblammo!"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
assert_equal 'default', msg["queue"]
|
|
149
|
+
assert_equal 'kerblammo!', msg["error_message"]
|
|
150
|
+
assert_equal 'RuntimeError', msg["error_class"]
|
|
151
|
+
assert_equal 0, msg["retry_count"]
|
|
152
|
+
refute msg["error_backtrace"]
|
|
153
|
+
assert msg["failed_at"]
|
|
154
|
+
@redis.verify
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'shuts down without retrying work-in-progress, which will resume' do
|
|
158
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
159
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }
|
|
160
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
161
|
+
assert_raises Roundhouse::Shutdown do
|
|
162
|
+
handler.call(worker, msg, 'default') do
|
|
163
|
+
raise Roundhouse::Shutdown
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
assert_raises(MockExpectationError, "zadd should not be called") do
|
|
167
|
+
@redis.verify
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'shuts down cleanly when shutdown causes exception' do
|
|
172
|
+
skip('Not supported in Ruby < 2.1.0') if RUBY_VERSION < '2.1.0'
|
|
173
|
+
|
|
174
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
175
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }
|
|
176
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
177
|
+
assert_raises Roundhouse::Shutdown do
|
|
178
|
+
handler.call(worker, msg, 'default') do
|
|
179
|
+
begin
|
|
180
|
+
raise Roundhouse::Shutdown
|
|
181
|
+
rescue Interrupt
|
|
182
|
+
raise "kerblammo!"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
assert_raises(MockExpectationError, "zadd should not be called") do
|
|
187
|
+
@redis.verify
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'shuts down cleanly when shutdown causes chained exceptions' do
|
|
192
|
+
skip('Not supported in Ruby < 2.1.0') if RUBY_VERSION < '2.1.0'
|
|
193
|
+
|
|
194
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
195
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }
|
|
196
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
197
|
+
assert_raises Roundhouse::Shutdown do
|
|
198
|
+
handler.call(worker, msg, 'default') do
|
|
199
|
+
begin
|
|
200
|
+
raise Roundhouse::Shutdown
|
|
201
|
+
rescue Interrupt
|
|
202
|
+
begin
|
|
203
|
+
raise "kerblammo!"
|
|
204
|
+
rescue
|
|
205
|
+
raise "kablooie!"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
assert_raises(MockExpectationError, "zadd should not be called") do
|
|
211
|
+
@redis.verify
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'allows a retry queue' do
|
|
216
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
217
|
+
msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true, 'retry_queue' => 'retry' }
|
|
218
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
219
|
+
assert_raises RuntimeError do
|
|
220
|
+
handler.call(worker, msg, 'default') do
|
|
221
|
+
raise "kerblammo!"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
assert_equal 'retry', msg["queue"]
|
|
225
|
+
assert_equal 'kerblammo!', msg["error_message"]
|
|
226
|
+
assert_equal 'RuntimeError', msg["error_class"]
|
|
227
|
+
assert_equal 0, msg["retry_count"]
|
|
228
|
+
refute msg["error_backtrace"]
|
|
229
|
+
assert msg["failed_at"]
|
|
230
|
+
@redis.verify
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'handles a recurring failed message' do
|
|
234
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
235
|
+
now = Time.now.to_f
|
|
236
|
+
msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], 'retry' => true, "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>10}
|
|
237
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
238
|
+
assert_raises RuntimeError do
|
|
239
|
+
handler.call(worker, msg, 'default') do
|
|
240
|
+
raise "kerblammo!"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
assert_equal 'default', msg["queue"]
|
|
244
|
+
assert_equal 'kerblammo!', msg["error_message"]
|
|
245
|
+
assert_equal 'RuntimeError', msg["error_class"]
|
|
246
|
+
assert_equal 11, msg["retry_count"]
|
|
247
|
+
assert msg["failed_at"]
|
|
248
|
+
@redis.verify
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'handles a recurring failed message before reaching user-specifed max' do
|
|
252
|
+
@redis.expect :zadd, 1, ['retry', String, String]
|
|
253
|
+
now = Time.now.to_f
|
|
254
|
+
msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], 'retry' => 10, "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>8}
|
|
255
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
256
|
+
assert_raises RuntimeError do
|
|
257
|
+
handler.call(worker, msg, 'default') do
|
|
258
|
+
raise "kerblammo!"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
assert_equal 'default', msg["queue"]
|
|
262
|
+
assert_equal 'kerblammo!', msg["error_message"]
|
|
263
|
+
assert_equal 'RuntimeError', msg["error_class"]
|
|
264
|
+
assert_equal 9, msg["retry_count"]
|
|
265
|
+
assert msg["failed_at"]
|
|
266
|
+
@redis.verify
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'throws away old messages after too many retries (using the default)' do
|
|
270
|
+
now = Time.now.to_f
|
|
271
|
+
msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry"=>true, "retry_count"=>25}
|
|
272
|
+
@redis.expect :zadd, 1, ['dead', Float, String]
|
|
273
|
+
@redis.expect :zremrangebyscore, 0, ['dead', String, Float]
|
|
274
|
+
@redis.expect :zremrangebyrank, 0, ['dead', Numeric, Numeric]
|
|
275
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
276
|
+
assert_raises RuntimeError do
|
|
277
|
+
handler.call(worker, msg, 'default') do
|
|
278
|
+
raise "kerblammo!"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
@redis.verify
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it 'throws away old messages after too many retries (using user-specified max)' do
|
|
285
|
+
now = Time.now.to_f
|
|
286
|
+
msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry"=>3, "retry_count"=>3}
|
|
287
|
+
@redis.expect :zadd, 1, ['dead', Float, String]
|
|
288
|
+
@redis.expect :zremrangebyscore, 0, ['dead', String, Float]
|
|
289
|
+
@redis.expect :zremrangebyrank, 0, ['dead', Numeric, Numeric]
|
|
290
|
+
handler = Roundhouse::Middleware::Server::RetryJobs.new
|
|
291
|
+
assert_raises RuntimeError do
|
|
292
|
+
handler.call(worker, msg, 'default') do
|
|
293
|
+
raise "kerblammo!"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
@redis.verify
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
describe "custom retry delay" do
|
|
300
|
+
before do
|
|
301
|
+
@old_logger = Roundhouse.logger
|
|
302
|
+
@tmp_log_path = '/tmp/roundhouse-retries.log'
|
|
303
|
+
Roundhouse.logger = Logger.new(@tmp_log_path)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
after do
|
|
307
|
+
Roundhouse.logger = @old_logger
|
|
308
|
+
Roundhouse.options.delete(:logfile)
|
|
309
|
+
File.unlink @tmp_log_path if File.exist?(@tmp_log_path)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
let(:custom_worker) do
|
|
313
|
+
Class.new do
|
|
314
|
+
include ::Roundhouse::Worker
|
|
315
|
+
|
|
316
|
+
roundhouse_retry_in do |count|
|
|
317
|
+
count * 2
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
let(:error_worker) do
|
|
323
|
+
Class.new do
|
|
324
|
+
include ::Roundhouse::Worker
|
|
325
|
+
|
|
326
|
+
roundhouse_retry_in do |count|
|
|
327
|
+
count / 0
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
let(:handler) { Roundhouse::Middleware::Server::RetryJobs.new }
|
|
333
|
+
|
|
334
|
+
it "retries with a default delay" do
|
|
335
|
+
refute_equal 4, handler.__send__(:delay_for, worker, 2)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "retries with a custom delay" do
|
|
339
|
+
assert_equal 4, handler.__send__(:delay_for, custom_worker, 2)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it "falls back to the default retry on exception" do
|
|
343
|
+
refute_equal 4, handler.__send__(:delay_for, error_worker, 2)
|
|
344
|
+
assert_match(/Failure scheduling retry using the defined `roundhouse_retry_in`/,
|
|
345
|
+
File.read(@tmp_log_path), 'Log entry missing for roundhouse_retry_in')
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
describe 'handles errors withouth cause' do
|
|
350
|
+
before do
|
|
351
|
+
@error = nil
|
|
352
|
+
begin
|
|
353
|
+
raise ::StandardError, 'Error'
|
|
354
|
+
rescue ::StandardError => e
|
|
355
|
+
@error = e
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it "does not recurse infinitely checking if it's a shutdown" do
|
|
360
|
+
assert(!Roundhouse::Middleware::Server::RetryJobs.new.send(
|
|
361
|
+
:exception_caused_by_shutdown?, @error))
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
describe 'handles errors with circular causes' do
|
|
366
|
+
before do
|
|
367
|
+
@error = nil
|
|
368
|
+
begin
|
|
369
|
+
begin
|
|
370
|
+
raise ::StandardError, 'Error 1'
|
|
371
|
+
rescue ::StandardError => e1
|
|
372
|
+
begin
|
|
373
|
+
raise ::StandardError, 'Error 2'
|
|
374
|
+
rescue ::StandardError
|
|
375
|
+
raise e1
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
rescue ::StandardError => e
|
|
379
|
+
@error = e
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it "does not recurse infinitely checking if it's a shutdown" do
|
|
384
|
+
assert(!Roundhouse::Middleware::Server::RetryJobs.new.send(
|
|
385
|
+
:exception_caused_by_shutdown?, @error))
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require_relative 'helper'
|
|
3
|
+
|
|
4
|
+
class TestRoundhouse < Roundhouse::Test
|
|
5
|
+
describe 'json processing' do
|
|
6
|
+
it 'loads json' do
|
|
7
|
+
assert_equal ({"foo" => "bar"}), Roundhouse.load_json("{\"foo\":\"bar\"}")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'dumps json' do
|
|
11
|
+
assert_equal "{\"foo\":\"bar\"}", Roundhouse.dump_json({ "foo" => "bar" })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "redis connection" do
|
|
16
|
+
it "returns error without creating a connection if block is not given" do
|
|
17
|
+
mock = Minitest::Mock.new
|
|
18
|
+
mock.expect :create, nil #Roundhouse::RedisConnection, create
|
|
19
|
+
assert_raises(ArgumentError) {
|
|
20
|
+
Roundhouse.redis
|
|
21
|
+
}
|
|
22
|
+
assert_raises(MockExpectationError, "create should not be called") do
|
|
23
|
+
mock.verify
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "❨╯°□°❩╯︵┻━┻" do
|
|
29
|
+
before { $stdout = StringIO.new }
|
|
30
|
+
after { $stdout = STDOUT }
|
|
31
|
+
|
|
32
|
+
it "allows angry developers to express their emotional constitution and remedies it" do
|
|
33
|
+
Roundhouse.❨╯°□°❩╯︵┻━┻
|
|
34
|
+
assert_equal "Calm down, yo.\n", $stdout.string
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe 'lifecycle events' do
|
|
39
|
+
it 'handles invalid input' do
|
|
40
|
+
Roundhouse.options[:lifecycle_events][:startup].clear
|
|
41
|
+
|
|
42
|
+
e = assert_raises ArgumentError do
|
|
43
|
+
Roundhouse.on(:startp)
|
|
44
|
+
end
|
|
45
|
+
assert_match(/Invalid event name/, e.message)
|
|
46
|
+
e = assert_raises ArgumentError do
|
|
47
|
+
Roundhouse.on('startup')
|
|
48
|
+
end
|
|
49
|
+
assert_match(/Symbols only/, e.message)
|
|
50
|
+
Roundhouse.on(:startup) do
|
|
51
|
+
1 + 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
assert_equal 2, Roundhouse.options[:lifecycle_events][:startup].first.call
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe 'default_worker_options' do
|
|
59
|
+
before do
|
|
60
|
+
@old_options = Roundhouse.default_worker_options
|
|
61
|
+
end
|
|
62
|
+
after { Roundhouse.default_worker_options = @old_options }
|
|
63
|
+
|
|
64
|
+
it 'stringify keys' do
|
|
65
|
+
Roundhouse.default_worker_options = { queue: 'cat'}
|
|
66
|
+
assert_equal 'cat', Roundhouse.default_worker_options['queue']
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe 'error handling' do
|
|
71
|
+
it 'deals with user-specified error handlers which raise errors' do
|
|
72
|
+
output = capture_logging do
|
|
73
|
+
begin
|
|
74
|
+
Roundhouse.error_handlers << proc {|x, hash|
|
|
75
|
+
raise 'boom'
|
|
76
|
+
}
|
|
77
|
+
cli = Roundhouse::CLI.new
|
|
78
|
+
cli.handle_exception(RuntimeError.new("hello"))
|
|
79
|
+
ensure
|
|
80
|
+
Roundhouse.error_handlers.pop
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
assert_includes output, "boom"
|
|
84
|
+
assert_includes output, "ERROR"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative 'helper'
|
|
2
|
+
require 'roundhouse/scheduled'
|
|
3
|
+
|
|
4
|
+
class TestScheduled < Roundhouse::Test
|
|
5
|
+
class ScheduledWorker
|
|
6
|
+
include Roundhouse::Worker
|
|
7
|
+
def perform(x)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe 'poller' do
|
|
12
|
+
before do
|
|
13
|
+
Roundhouse.redis = REDIS
|
|
14
|
+
Roundhouse.redis do |conn|
|
|
15
|
+
conn.flushdb
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@error_1 = { 'class' => ScheduledWorker.name, 'args' => [0], 'queue_id' => 100 }
|
|
19
|
+
@error_2 = { 'class' => ScheduledWorker.name, 'args' => [1], 'queue_id' => 200 }
|
|
20
|
+
@error_3 = { 'class' => ScheduledWorker.name, 'args' => [2], 'queue_id' => 300 }
|
|
21
|
+
@future_1 = { 'class' => ScheduledWorker.name, 'args' => [3], 'queue_id' => 400 }
|
|
22
|
+
@future_2 = { 'class' => ScheduledWorker.name, 'args' => [4], 'queue_id' => 500 }
|
|
23
|
+
@future_3 = { 'class' => ScheduledWorker.name, 'args' => [5], 'queue_id' => 600 }
|
|
24
|
+
|
|
25
|
+
@retry = Roundhouse::RetrySet.new
|
|
26
|
+
@scheduled = Roundhouse::ScheduledSet.new
|
|
27
|
+
@poller = Roundhouse::Scheduled::Poller.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Stopper
|
|
31
|
+
def call(worker_class, message, queue, r)
|
|
32
|
+
yield if message['args'].first.odd?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'executes client middleware' do
|
|
37
|
+
Roundhouse.client_middleware.add Stopper
|
|
38
|
+
begin
|
|
39
|
+
@retry.schedule (Time.now - 60).to_f, @error_1
|
|
40
|
+
@retry.schedule (Time.now - 60).to_f, @error_2
|
|
41
|
+
@scheduled.schedule (Time.now - 60).to_f, @future_2
|
|
42
|
+
@scheduled.schedule (Time.now - 60).to_f, @future_3
|
|
43
|
+
|
|
44
|
+
@poller.poll
|
|
45
|
+
|
|
46
|
+
Roundhouse.redis do |conn|
|
|
47
|
+
assert_equal 0, conn.llen("queue:100")
|
|
48
|
+
assert_equal 1, conn.llen("queue:200")
|
|
49
|
+
assert_equal 0, conn.llen("queue:500")
|
|
50
|
+
assert_equal 1, conn.llen("queue:600")
|
|
51
|
+
end
|
|
52
|
+
ensure
|
|
53
|
+
Roundhouse.client_middleware.remove Stopper
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should empty the retry and scheduled queues up to the current time' do
|
|
58
|
+
created_time = Time.new(2013, 2, 3)
|
|
59
|
+
enqueued_time = Time.new(2013, 2, 4)
|
|
60
|
+
|
|
61
|
+
Time.stub(:now, created_time) do
|
|
62
|
+
@retry.schedule (enqueued_time - 60).to_f, @error_1.merge!('created_at' => created_time.to_f)
|
|
63
|
+
@retry.schedule (enqueued_time - 50).to_f, @error_2.merge!('created_at' => created_time.to_f)
|
|
64
|
+
@retry.schedule (enqueued_time + 60).to_f, @error_3.merge!('created_at' => created_time.to_f)
|
|
65
|
+
@scheduled.schedule (enqueued_time - 60).to_f, @future_1.merge!('created_at' => created_time.to_f)
|
|
66
|
+
@scheduled.schedule (enqueued_time - 50).to_f, @future_2.merge!('created_at' => created_time.to_f)
|
|
67
|
+
@scheduled.schedule (enqueued_time + 60).to_f, @future_3.merge!('created_at' => created_time.to_f)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Time.stub(:now, enqueued_time) do
|
|
71
|
+
@poller.poll
|
|
72
|
+
|
|
73
|
+
Roundhouse.redis do |conn|
|
|
74
|
+
%w(queue:100 queue:200 queue:400 queue:500).each do |queue_id|
|
|
75
|
+
assert_equal 1, conn.llen(queue_id)
|
|
76
|
+
job = Roundhouse.load_json(conn.lrange(queue_id, 0, -1)[0])
|
|
77
|
+
assert_equal enqueued_time.to_f, job['enqueued_at']
|
|
78
|
+
assert_equal created_time.to_f, job['created_at']
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
assert_equal 1, @retry.size
|
|
83
|
+
assert_equal 1, @scheduled.size
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def with_roundhouse_option(name, value)
|
|
88
|
+
_original, Roundhouse.options[name] = Roundhouse.options[name], value
|
|
89
|
+
begin
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
Roundhouse.options[name] = _original
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'generates random intervals that target a configured average' do
|
|
97
|
+
with_roundhouse_option(:poll_interval_average, 10) do
|
|
98
|
+
i = 500
|
|
99
|
+
intervals = i.times.map{ @poller.send(:random_poll_interval) }
|
|
100
|
+
|
|
101
|
+
assert intervals.all?{|x| x >= 5}
|
|
102
|
+
assert intervals.all?{|x| x <= 15}
|
|
103
|
+
assert_in_delta 10, intervals.reduce(&:+).to_f / i, 0.5
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'calculates an average poll interval based on the number of known Roundhouse processes' do
|
|
108
|
+
with_roundhouse_option(:average_scheduled_poll_interval, 10) do
|
|
109
|
+
3.times do |i|
|
|
110
|
+
Roundhouse.redis do |conn|
|
|
111
|
+
conn.sadd("processes", "process-#{i}")
|
|
112
|
+
conn.hset("process-#{i}", "info", nil)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
assert_equal 30, @poller.send(:scaled_poll_interval)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require_relative 'helper'
|
|
2
|
+
require 'roundhouse/scheduled'
|
|
3
|
+
require 'active_support/core_ext/numeric/time'
|
|
4
|
+
require 'active_support/core_ext/integer/time'
|
|
5
|
+
require 'active_support/core_ext/date'
|
|
6
|
+
|
|
7
|
+
class TestScheduling < Roundhouse::Test
|
|
8
|
+
describe 'middleware' do
|
|
9
|
+
before do
|
|
10
|
+
Roundhouse::Client.instance_variable_set(:@default, nil)
|
|
11
|
+
@redis = Minitest::Mock.new
|
|
12
|
+
# Ugh, this is terrible.
|
|
13
|
+
Roundhouse.instance_variable_set(:@redis, @redis)
|
|
14
|
+
def @redis.multi; [yield] * 2 if block_given?; end
|
|
15
|
+
def @redis.with; yield self; end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after do
|
|
19
|
+
Roundhouse::Client.instance_variable_set(:@default, nil)
|
|
20
|
+
Roundhouse.instance_variable_set(:@redis, REDIS)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ScheduledWorker
|
|
24
|
+
include Roundhouse::Worker
|
|
25
|
+
roundhouse_options :queue => :custom_queue
|
|
26
|
+
def perform(x)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'schedules a job via interval' do
|
|
31
|
+
@redis.expect :zadd, true, ['schedule', Array]
|
|
32
|
+
assert ScheduledWorker.perform_in(100, 600, 'mike')
|
|
33
|
+
@redis.verify
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'schedules a job in one month' do
|
|
37
|
+
@redis.expect :zadd, true do |key, args|
|
|
38
|
+
assert_equal 'schedule', key
|
|
39
|
+
assert_in_delta 1.month.since.to_f, args[0][0].to_f, 1
|
|
40
|
+
end
|
|
41
|
+
assert ScheduledWorker.perform_in(100, 1.month, 'mike')
|
|
42
|
+
@redis.verify
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'schedules a job via timestamp' do
|
|
46
|
+
@redis.expect :zadd, true, ['schedule', Array]
|
|
47
|
+
assert ScheduledWorker.perform_in(100, 5.days.from_now, 'mike')
|
|
48
|
+
@redis.verify
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'schedules job right away on negative timestamp/interval' do
|
|
52
|
+
Roundhouse::Monitor.stub :maybe_add_to_rotation, true do
|
|
53
|
+
@redis.expect :lpush, true, ['queue:100', Array]
|
|
54
|
+
assert ScheduledWorker.perform_in(100, -300, 'mike')
|
|
55
|
+
end
|
|
56
|
+
@redis.verify
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'schedules multiple jobs at once' do
|
|
60
|
+
@redis.expect :zadd, true, ['schedule', Array]
|
|
61
|
+
assert Roundhouse::Client.push_bulk('queue_id' => 100, 'class' => ScheduledWorker, 'args' => [['mike'], ['mike']], 'at' => 600)
|
|
62
|
+
@redis.verify
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'removes the enqueued_at field when scheduling' do
|
|
66
|
+
@redis.expect :zadd, true do |key, args|
|
|
67
|
+
job = Roundhouse.load_json(args.first.last)
|
|
68
|
+
job.key?('created_at') && !job.key?('enqueued_at')
|
|
69
|
+
end
|
|
70
|
+
assert ScheduledWorker.perform_in(100, 1.month, 'mike')
|
|
71
|
+
@redis.verify
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|