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,55 @@
1
+ require_relative 'helper'
2
+ require 'roundhouse/exception_handler'
3
+ require 'stringio'
4
+ require 'logger'
5
+
6
+ ExceptionHandlerTestException = Class.new(StandardError)
7
+ TEST_EXCEPTION = ExceptionHandlerTestException.new("Something didn't work!")
8
+
9
+ class Component
10
+ include Roundhouse::ExceptionHandler
11
+
12
+ def invoke_exception(args)
13
+ raise TEST_EXCEPTION
14
+ rescue ExceptionHandlerTestException => e
15
+ handle_exception(e,args)
16
+ end
17
+ end
18
+
19
+ class TestExceptionHandler < Roundhouse::Test
20
+ describe "with mock logger" do
21
+ before do
22
+ @old_logger = Roundhouse.logger
23
+ @str_logger = StringIO.new
24
+ Roundhouse.logger = Logger.new(@str_logger)
25
+ end
26
+
27
+ after do
28
+ Roundhouse.logger = @old_logger
29
+ end
30
+
31
+ it "logs the exception to Roundhouse.logger" do
32
+ Component.new.invoke_exception(:a => 1)
33
+ @str_logger.rewind
34
+ log = @str_logger.readlines
35
+ assert_match(/a=>1/, log[0], "didn't include the context")
36
+ assert_match(/Something didn't work!/, log[1], "didn't include the exception message")
37
+ assert_match(/test\/test_exception_handler.rb/, log[2], "didn't include the backtrace")
38
+ end
39
+
40
+ describe "when the exception does not have a backtrace" do
41
+ it "does not fail" do
42
+ exception = ExceptionHandlerTestException.new
43
+ assert_nil exception.backtrace
44
+
45
+ begin
46
+ Component.new.handle_exception exception
47
+ pass
48
+ rescue StandardError
49
+ flunk "failed handling a nil backtrace"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,206 @@
1
+ require_relative 'helper'
2
+ require 'roundhouse/fetch'
3
+ require 'roundhouse/monitor'
4
+ require 'roundhouse/api'
5
+
6
+ class TestFetcher < Roundhouse::Test
7
+ describe 'fetcher' do
8
+ before do
9
+ Roundhouse.redis = { :url => REDIS_URL, :namespace => 'fuzzy' }
10
+ Roundhouse.redis { |conn| conn.flushdb }
11
+ end
12
+
13
+ after do
14
+ Roundhouse.redis = REDIS
15
+ end
16
+
17
+ describe '#retrieve_work' do
18
+ describe 'when nominal' do
19
+ before do
20
+ Roundhouse.redis do |conn|
21
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 100)
22
+ conn.hset(Roundhouse::Monitor.status_bucket(100), 100, Roundhouse::Monitor::ACTIVE)
23
+ conn.lpush("#{Roundhouse::Monitor::QUEUE}:100", 'msg')
24
+ end
25
+ end
26
+
27
+ it 'retrieves' do
28
+ fetch = Roundhouse::RoundRobinFetch.new
29
+ uow = fetch.retrieve_work
30
+ refute_nil uow
31
+ assert_equal '100', uow.queue_id
32
+ assert_equal 'msg', uow.message
33
+ q = Roundhouse::Queue.new(100)
34
+ assert_equal 0, q.size
35
+ uow.requeue
36
+ assert_equal 1, q.size
37
+ assert_nil uow.acknowledge
38
+ end
39
+ end
40
+
41
+ describe 'with empty status' do
42
+ before do
43
+ Roundhouse.redis do |conn|
44
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 100)
45
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 200)
46
+ conn.hset(Roundhouse::Monitor.status_bucket(100), 100, Roundhouse::Monitor::EMPTY)
47
+ conn.hset(Roundhouse::Monitor.status_bucket(200), 200, Roundhouse::Monitor::ACTIVE)
48
+ conn.lpush("#{Roundhouse::Monitor::QUEUE}:200", 'msg-200')
49
+ end
50
+ end
51
+
52
+ it 'skips queues with empty status' do
53
+ fetch = Roundhouse::RoundRobinFetch.new
54
+ uow = fetch.retrieve_work
55
+ refute_nil uow
56
+ assert_equal '200', uow.queue_id
57
+ assert_equal 'msg-200', uow.message
58
+ q = Roundhouse::Queue.new(200)
59
+ assert_equal 0, q.size
60
+ uow.requeue
61
+ assert_equal 1, q.size
62
+ assert_nil uow.acknowledge
63
+ end
64
+ end
65
+
66
+ describe 'with active status and empty queue' do
67
+ before do
68
+ Roundhouse.redis do |conn|
69
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 100)
70
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 200)
71
+ conn.hset(Roundhouse::Monitor.status_bucket(100), 100, Roundhouse::Monitor::ACTIVE)
72
+ conn.hset(Roundhouse::Monitor.status_bucket(200), 200, Roundhouse::Monitor::ACTIVE)
73
+ conn.lpush("#{Roundhouse::Monitor::QUEUE}:200", 'msg-200')
74
+ end
75
+ end
76
+
77
+ it 'skips queues with empty status' do
78
+ empty_queue = Roundhouse::Queue.new(100)
79
+ assert_equal 0, empty_queue.size
80
+ assert_equal :active, empty_queue.status
81
+
82
+ fetch = Roundhouse::RoundRobinFetch.new
83
+ uow = fetch.retrieve_work
84
+ refute_nil uow
85
+ assert_equal '200', uow.queue_id
86
+ assert_equal 'msg-200', uow.message
87
+
88
+ # After fetching, should set the active status
89
+ # of an empty queue to empty
90
+ assert_equal :empty, empty_queue.status
91
+
92
+ # The filled queue should act as normal
93
+ q = Roundhouse::Queue.new(200)
94
+ assert_equal 0, q.size
95
+ uow.requeue
96
+ assert_equal 1, q.size
97
+ assert_nil uow.acknowledge
98
+ end
99
+ end
100
+
101
+ describe 'with suspended status' do
102
+ before do
103
+ Roundhouse.redis do |conn|
104
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 100)
105
+ conn.lpush(Roundhouse::Monitor::SEMAPHORE, 200)
106
+ conn.hset(Roundhouse::Monitor.status_bucket(100), 100, Roundhouse::Monitor::SUSPENDED)
107
+ conn.hset(Roundhouse::Monitor.status_bucket(200), 200, Roundhouse::Monitor::ACTIVE)
108
+ conn.lpush("#{Roundhouse::Monitor::QUEUE}:100", 'msg-100')
109
+ conn.lpush("#{Roundhouse::Monitor::QUEUE}:200", 'msg-200')
110
+ end
111
+ end
112
+
113
+ it 'skips queues with suspended status' do
114
+ fetch = Roundhouse::RoundRobinFetch.new
115
+ uow = fetch.retrieve_work
116
+ refute_nil uow
117
+ assert_equal '200', uow.queue_id
118
+ assert_equal 'msg-200', uow.message
119
+
120
+ # Should not touch the work in suspended queue
121
+ suspended_q = Roundhouse::Queue.new(100)
122
+ assert_equal 1, suspended_q.size
123
+
124
+ q = Roundhouse::Queue.new(200)
125
+ assert_equal 0, q.size
126
+ uow.requeue
127
+ assert_equal 1, q.size
128
+ assert_nil uow.acknowledge
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ it 'bulk requeues' do
135
+ q1 = Roundhouse::Queue.new(1000)
136
+ q2 = Roundhouse::Queue.new(2000)
137
+ assert_equal 0, q1.size
138
+ assert_equal 0, q2.size
139
+ uow = Roundhouse::RoundRobinFetch::UnitOfWork
140
+ Roundhouse::RoundRobinFetch.bulk_requeue([uow.new('fuzzy:queue:1000', 'bob'), uow.new('fuzzy:queue:1000', 'bar'), uow.new('fuzzy:queue:2000', 'widget')], {:queues => []})
141
+ assert_equal 2, q1.size
142
+ assert_equal 1, q2.size
143
+
144
+ # Changed bulk requeues not to change status
145
+ # Not sure if this is a good idea yet
146
+ #assert_equal :active, q1.status
147
+ #assert_equal :active, q2.status
148
+ end
149
+
150
+ describe 'fetching' do
151
+ before do
152
+ Roundhouse::Fetcher.reset
153
+ end
154
+
155
+ it 'instantiates' do
156
+ begin
157
+ Roundhouse.options[:fetch] = NullFetch
158
+ mgr = Minitest::Mock.new
159
+ fetch = Roundhouse::Fetcher.new(mgr, {})
160
+ fetch.fetch
161
+ Roundhouse::Fetcher.done!
162
+ ensure
163
+ Roundhouse.options[:fetch] = Roundhouse::RoundRobinFetch
164
+ end
165
+ end
166
+
167
+ class NullFetch
168
+ def initialize(opts)
169
+ end
170
+ def retrieve_work
171
+ end
172
+ def self.bulk_requeue(*args)
173
+ end
174
+ end
175
+
176
+ it 'handles redis network errors' do
177
+ begin
178
+ Roundhouse.logger.level = Logger::FATAL
179
+ Roundhouse.options[:fetch] = ErrorFetch
180
+ mgr = Minitest::Mock.new
181
+ fetch = Roundhouse::Fetcher.new(mgr, {})
182
+ def fetch.pause
183
+ end
184
+ refute fetch.down
185
+ fetch.fetch
186
+ Roundhouse::Fetcher.done!
187
+ assert fetch.down
188
+ ensure
189
+ Roundhouse.options[:fetch] = Roundhouse::RoundRobinFetch
190
+ Roundhouse.logger.level = Logger::ERROR
191
+ end
192
+ end
193
+
194
+ class ErrorFetch
195
+ def initialize(opts)
196
+ end
197
+ def retrieve_work
198
+ raise IOError, "ker-BOOM"
199
+ end
200
+ def self.bulk_requeue(*args)
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'helper'
2
+ require 'roundhouse/logging'
3
+
4
+ class TestFetcher < Roundhouse::Test
5
+ describe Roundhouse::Logging do
6
+ describe "#with_context" do
7
+ def context
8
+ Roundhouse::Logging.logger.formatter.context
9
+ end
10
+
11
+ it "has no context by default" do
12
+ context.must_equal nil
13
+ end
14
+
15
+ it "can add a context" do
16
+ Roundhouse::Logging.with_context "xx" do
17
+ context.must_equal " xx"
18
+ end
19
+ context.must_equal nil
20
+ end
21
+
22
+ it "can use multiple contexts" do
23
+ Roundhouse::Logging.with_context "xx" do
24
+ context.must_equal " xx"
25
+ Roundhouse::Logging.with_context "yy" do
26
+ context.must_equal " xx yy"
27
+ end
28
+ context.must_equal " xx"
29
+ end
30
+ context.must_equal nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,169 @@
1
+ require_relative 'helper'
2
+ require 'roundhouse/manager'
3
+
4
+ class TestManager < Roundhouse::Test
5
+
6
+ describe 'manager' do
7
+ before do
8
+ Roundhouse.redis = REDIS
9
+ Roundhouse.redis {|c| c.flushdb }
10
+ end
11
+
12
+ def new_manager(opts)
13
+ condvar = Minitest::Mock.new
14
+ condvar.expect(:signal, nil, [])
15
+ Roundhouse::Manager.new(condvar, opts)
16
+ end
17
+
18
+ it 'creates N processor instances' do
19
+ mgr = new_manager(options)
20
+ assert_equal options[:concurrency], mgr.ready.size
21
+ assert_equal [], mgr.busy
22
+ end
23
+
24
+ it 'assigns work to a processor' do
25
+ uow = Object.new
26
+ processor = Minitest::Mock.new
27
+ processor.expect(:async, processor, [])
28
+ processor.expect(:process, nil, [uow])
29
+
30
+ mgr = new_manager(options)
31
+ mgr.ready << processor
32
+ mgr.assign(uow)
33
+ assert_equal 1, mgr.busy.size
34
+
35
+ processor.verify
36
+ end
37
+
38
+ it 'requeues work if stopping' do
39
+ uow = Minitest::Mock.new
40
+ uow.expect(:requeue, nil, [])
41
+
42
+ mgr = new_manager(options)
43
+ mgr.fetcher = Roundhouse::RoundRobinFetch.new({:queues => []})
44
+ mgr.stop
45
+ mgr.assign(uow)
46
+ uow.verify
47
+ end
48
+
49
+ it 'shuts down the system' do
50
+ mgr = new_manager(options)
51
+ mgr.fetcher = Roundhouse::RoundRobinFetch.new({:queues => []})
52
+ mgr.stop
53
+
54
+ assert mgr.busy.empty?
55
+ assert mgr.ready.empty?
56
+ end
57
+
58
+ it 'returns finished processors to the ready pool' do
59
+ fetcher = MiniTest::Mock.new
60
+ fetcher.expect :async, fetcher, []
61
+ fetcher.expect :fetch, nil, []
62
+ mgr = new_manager(options)
63
+ mgr.fetcher = fetcher
64
+ init_size = mgr.ready.size
65
+ processor = mgr.ready.pop
66
+ mgr.busy << processor
67
+ mgr.processor_done(processor)
68
+
69
+ assert_equal 0, mgr.busy.size
70
+ assert_equal init_size, mgr.ready.size
71
+ fetcher.verify
72
+ end
73
+
74
+ it 'throws away dead processors' do
75
+ fetcher = MiniTest::Mock.new
76
+ fetcher.expect :async, fetcher, []
77
+ fetcher.expect :fetch, nil, []
78
+ mgr = new_manager(options)
79
+ mgr.fetcher = fetcher
80
+ init_size = mgr.ready.size
81
+ processor = mgr.ready.pop
82
+ mgr.busy << processor
83
+ mgr.processor_died(processor, 'ignored')
84
+
85
+ assert_equal 0, mgr.busy.size
86
+ assert_equal init_size, mgr.ready.size
87
+ refute mgr.ready.include?(processor)
88
+ fetcher.verify
89
+ end
90
+
91
+ it 'does not support invalid concurrency' do
92
+ assert_raises(ArgumentError) { new_manager(concurrency: 0) }
93
+ assert_raises(ArgumentError) { new_manager(concurrency: -1) }
94
+ end
95
+
96
+ describe 'heartbeat' do
97
+ before do
98
+ uow = Object.new
99
+
100
+ @processor = Minitest::Mock.new
101
+ @processor.expect(:async, @processor, [])
102
+ @processor.expect(:process, nil, [uow])
103
+
104
+ @mgr = new_manager(options)
105
+ @mgr.ready << @processor
106
+ @mgr.assign(uow)
107
+
108
+ @processor.verify
109
+ @proctitle = $0
110
+ end
111
+
112
+ after do
113
+ $0 = @proctitle
114
+ end
115
+
116
+ describe 'when manager is active' do
117
+ before do
118
+ Roundhouse::Manager::PROCTITLES << proc { "xyz" }
119
+ @mgr.heartbeat('identity', heartbeat_data, Roundhouse.dump_json(heartbeat_data))
120
+ Roundhouse::Manager::PROCTITLES.pop
121
+ end
122
+
123
+ it 'sets useful info to proctitle' do
124
+ assert_equal "roundhouse #{Roundhouse::VERSION} myapp [1 of 3 busy] xyz", $0
125
+ end
126
+
127
+ it 'stores process info in redis' do
128
+ info = Roundhouse.redis { |c| c.hmget('identity', 'busy') }
129
+ assert_equal ["1"], info
130
+ expires = Roundhouse.redis { |c| c.pttl('identity') }
131
+ assert_in_delta 60000, expires, 500
132
+ end
133
+ end
134
+
135
+ describe 'when manager is stopped' do
136
+ before do
137
+ @processor.expect(:alive?, [])
138
+ @processor.expect(:terminate, [])
139
+
140
+ @mgr.stop
141
+ @mgr.processor_done(@processor)
142
+ @mgr.heartbeat('identity', heartbeat_data, Roundhouse.dump_json(heartbeat_data))
143
+
144
+ @processor.verify
145
+ end
146
+
147
+ it 'indicates stopping status in proctitle' do
148
+ assert_equal "roundhouse #{Roundhouse::VERSION} myapp [0 of 3 busy] stopping", $0
149
+ end
150
+
151
+ it 'stores process info in redis' do
152
+ info = Roundhouse.redis { |c| c.hmget('identity', 'busy') }
153
+ assert_equal ["0"], info
154
+ expires = Roundhouse.redis { |c| c.pttl('identity') }
155
+ assert_in_delta 60000, expires, 50
156
+ end
157
+ end
158
+ end
159
+
160
+ def options
161
+ { :concurrency => 3, :queues => ['default'] }
162
+ end
163
+
164
+ def heartbeat_data
165
+ { 'concurrency' => 3, 'tag' => 'myapp' }
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,160 @@
1
+ require_relative 'helper'
2
+ require 'roundhouse/middleware/chain'
3
+ require 'roundhouse/processor'
4
+ require 'roundhouse/fetch'
5
+
6
+ class TestMiddleware < Roundhouse::Test
7
+ describe 'middleware chain' do
8
+ before do
9
+ $errors = []
10
+ Roundhouse.redis = REDIS
11
+ end
12
+
13
+ class CustomMiddleware
14
+ def initialize(name, recorder)
15
+ @name = name
16
+ @recorder = recorder
17
+ end
18
+
19
+ def call(*args)
20
+ @recorder << [@name, 'before']
21
+ yield
22
+ @recorder << [@name, 'after']
23
+ end
24
+ end
25
+
26
+ it 'supports custom middleware' do
27
+ chain = Roundhouse::Middleware::Chain.new
28
+ chain.add CustomMiddleware, 1, []
29
+
30
+ assert_equal CustomMiddleware, chain.entries.last.klass
31
+ end
32
+
33
+ class CustomWorker
34
+ $recorder = []
35
+ include Roundhouse::Worker
36
+ def perform(recorder)
37
+ $recorder << ['work_performed']
38
+ end
39
+ end
40
+
41
+ class NonYieldingMiddleware
42
+ def call(*args)
43
+ end
44
+ end
45
+
46
+ class AnotherCustomMiddleware
47
+ def initialize(name, recorder)
48
+ @name = name
49
+ @recorder = recorder
50
+ end
51
+
52
+ def call(*args)
53
+ @recorder << [@name, 'before']
54
+ yield
55
+ @recorder << [@name, 'after']
56
+ end
57
+ end
58
+
59
+ class YetAnotherCustomMiddleware
60
+ def initialize(name, recorder)
61
+ @name = name
62
+ @recorder = recorder
63
+ end
64
+
65
+ def call(*args)
66
+ @recorder << [@name, 'before']
67
+ yield
68
+ @recorder << [@name, 'after']
69
+ end
70
+ end
71
+
72
+ it 'executes middleware in the proper order' do
73
+ msg = Roundhouse.dump_json({ 'class' => CustomWorker.to_s, 'args' => [$recorder] })
74
+
75
+ Roundhouse.server_middleware do |chain|
76
+ # should only add once, second should replace the first
77
+ 2.times { |i| chain.add CustomMiddleware, i.to_s, $recorder }
78
+ chain.insert_before CustomMiddleware, AnotherCustomMiddleware, '2', $recorder
79
+ chain.insert_after AnotherCustomMiddleware, YetAnotherCustomMiddleware, '3', $recorder
80
+ end
81
+
82
+ boss = Minitest::Mock.new
83
+ processor = Roundhouse::Processor.new(boss)
84
+ actor = Minitest::Mock.new
85
+ actor.expect(:processor_done, nil, [processor])
86
+ actor.expect(:real_thread, nil, [nil, Thread])
87
+ boss.expect(:async, actor, [])
88
+ boss.expect(:async, actor, [])
89
+ processor.process(Roundhouse::RoundRobinFetch::UnitOfWork.new('queue:100', msg))
90
+ assert_equal %w(2 before 3 before 1 before work_performed 1 after 3 after 2 after), $recorder.flatten
91
+ end
92
+
93
+ it 'correctly replaces middleware when using middleware with options in the initializer' do
94
+ chain = Roundhouse::Middleware::Chain.new
95
+ chain.add Roundhouse::Middleware::Server::RetryJobs
96
+ chain.add Roundhouse::Middleware::Server::RetryJobs, {:max_retries => 5}
97
+ assert_equal 1, chain.count
98
+ end
99
+
100
+ it 'correctly prepends middleware' do
101
+ chain = Roundhouse::Middleware::Chain.new
102
+ chain_entries = chain.entries
103
+ chain.add CustomMiddleware
104
+ chain.prepend YetAnotherCustomMiddleware
105
+ assert_equal YetAnotherCustomMiddleware, chain_entries.first.klass
106
+ assert_equal CustomMiddleware, chain_entries.last.klass
107
+ end
108
+
109
+ it 'allows middleware to abruptly stop processing rest of chain' do
110
+ recorder = []
111
+ chain = Roundhouse::Middleware::Chain.new
112
+ chain.add NonYieldingMiddleware
113
+ chain.add CustomMiddleware, 1, recorder
114
+
115
+ final_action = nil
116
+ chain.invoke { final_action = true }
117
+ assert_equal nil, final_action
118
+ assert_equal [], recorder
119
+ end
120
+ end
121
+
122
+ describe 'i18n' do
123
+ before do
124
+ require 'i18n'
125
+ I18n.enforce_available_locales = false
126
+ require 'roundhouse/middleware/i18n'
127
+ end
128
+
129
+ it 'saves and restores locale' do
130
+ I18n.locale = 'fr'
131
+ msg = {}
132
+ mw = Roundhouse::Middleware::I18n::Client.new
133
+ mw.call(nil, msg, nil, nil) { }
134
+ assert_equal :fr, msg['locale']
135
+
136
+ msg['locale'] = 'jp'
137
+ I18n.locale = I18n.default_locale
138
+ assert_equal :en, I18n.locale
139
+ mw = Roundhouse::Middleware::I18n::Server.new
140
+ mw.call(nil, msg, nil) do
141
+ assert_equal :jp, I18n.locale
142
+ end
143
+ assert_equal :en, I18n.locale
144
+ end
145
+
146
+ it 'supports I18n.enforce_available_locales = true' do
147
+ I18n.enforce_available_locales = true
148
+ I18n.available_locales = [:en, :jp]
149
+
150
+ msg = { 'locale' => 'jp' }
151
+ mw = Roundhouse::Middleware::I18n::Server.new
152
+ mw.call(nil, msg, nil) do
153
+ assert_equal :jp, I18n.locale
154
+ end
155
+
156
+ I18n.enforce_available_locales = false
157
+ I18n.available_locales = nil
158
+ end
159
+ end
160
+ end