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.
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