tochtli 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +14 -0
  3. data/Gemfile +32 -0
  4. data/History.md +138 -0
  5. data/README.md +46 -0
  6. data/Rakefile +50 -0
  7. data/VERSION +1 -0
  8. data/assets/communication.png +0 -0
  9. data/assets/layers.png +0 -0
  10. data/examples/01-screencap-service/Gemfile +3 -0
  11. data/examples/01-screencap-service/README.md +5 -0
  12. data/examples/01-screencap-service/client.rb +15 -0
  13. data/examples/01-screencap-service/common.rb +15 -0
  14. data/examples/01-screencap-service/server.rb +26 -0
  15. data/examples/02-log-analyzer/Gemfile +3 -0
  16. data/examples/02-log-analyzer/README.md +5 -0
  17. data/examples/02-log-analyzer/client.rb +95 -0
  18. data/examples/02-log-analyzer/common.rb +33 -0
  19. data/examples/02-log-analyzer/sample.log +10001 -0
  20. data/examples/02-log-analyzer/server.rb +133 -0
  21. data/lib/tochtli.rb +177 -0
  22. data/lib/tochtli/active_record_connection_cleaner.rb +9 -0
  23. data/lib/tochtli/application.rb +135 -0
  24. data/lib/tochtli/base_client.rb +135 -0
  25. data/lib/tochtli/base_controller.rb +360 -0
  26. data/lib/tochtli/controller_manager.rb +99 -0
  27. data/lib/tochtli/engine.rb +15 -0
  28. data/lib/tochtli/message.rb +114 -0
  29. data/lib/tochtli/rabbit_client.rb +36 -0
  30. data/lib/tochtli/rabbit_connection.rb +249 -0
  31. data/lib/tochtli/reply_queue.rb +129 -0
  32. data/lib/tochtli/simple_validation.rb +23 -0
  33. data/lib/tochtli/test.rb +9 -0
  34. data/lib/tochtli/test/client.rb +28 -0
  35. data/lib/tochtli/test/controller.rb +66 -0
  36. data/lib/tochtli/test/integration.rb +78 -0
  37. data/lib/tochtli/test/memory_cache.rb +22 -0
  38. data/lib/tochtli/test/test_case.rb +191 -0
  39. data/lib/tochtli/test/test_unit.rb +22 -0
  40. data/lib/tochtli/version.rb +3 -0
  41. data/log_generator.rb +11 -0
  42. data/test/base_client_test.rb +68 -0
  43. data/test/controller_functional_test.rb +87 -0
  44. data/test/controller_integration_test.rb +274 -0
  45. data/test/controller_manager_test.rb +75 -0
  46. data/test/dummy/Rakefile +7 -0
  47. data/test/dummy/config/application.rb +36 -0
  48. data/test/dummy/config/boot.rb +4 -0
  49. data/test/dummy/config/database.yml +3 -0
  50. data/test/dummy/config/environment.rb +5 -0
  51. data/test/dummy/config/rabbit.yml +4 -0
  52. data/test/dummy/db/.gitkeep +0 -0
  53. data/test/dummy/log/.gitkeep +0 -0
  54. data/test/key_matcher_test.rb +100 -0
  55. data/test/log/.gitkeep +0 -0
  56. data/test/message_test.rb +80 -0
  57. data/test/rabbit_client_test.rb +71 -0
  58. data/test/rabbit_connection_test.rb +151 -0
  59. data/test/test_helper.rb +32 -0
  60. data/test/version_test.rb +8 -0
  61. data/tochtli.gemspec +129 -0
  62. metadata +259 -0
@@ -0,0 +1,22 @@
1
+ module Tochtli
2
+ module Test
3
+ module UnitTestSupport
4
+ module BaseBeforeSetup
5
+ def before_setup
6
+ end
7
+ end
8
+
9
+ def append_features(base)
10
+ base.send :include, BaseBeforeSetup
11
+ super
12
+ end
13
+
14
+ def included(base)
15
+ if base < ::Test::Unit::TestCase
16
+ base.setup :before_setup # Run before_setup for Test::Unit (Minitest uses it as an only callback)
17
+ end
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Tochtli
2
+ VERSION = File.readlines(File.expand_path('../../../VERSION', __FILE__))[0].chomp unless defined?(VERSION)
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'logger'
2
+
3
+ SEVERITIES = [:fatal] + [:error]*3 + [:warn]*5 + [:info]*12 + [:debug]*20
4
+
5
+ log = Logger.new('sample.log')
6
+
7
+ 10000.times do
8
+ severity = SEVERITIES.sample
9
+ log.send severity, "Sample #{severity}"
10
+ end
11
+
@@ -0,0 +1,68 @@
1
+ require_relative 'test_helper'
2
+
3
+ class BaseClientTest < Minitest::Test
4
+ include Tochtli::Test::Client
5
+
6
+ def setup
7
+ @fake_client = FakeClient.new(@client)
8
+ end
9
+
10
+ def test_instance
11
+ assert_respond_to @fake_client, :rabbit_client
12
+ assert_respond_to @fake_client, :rabbit_connection
13
+ end
14
+
15
+ def test_synchronous_method
16
+ expect_published FakeMessage do
17
+ handle_reply FakeReply, @message, result: 'OK'
18
+ end
19
+
20
+ result = @fake_client.do_sync
21
+
22
+ assert_equal 'OK', result
23
+ end
24
+
25
+ def test_asynchronous_method
26
+ result = nil
27
+ @fake_client.do_async('attr') { |r| result = r }
28
+
29
+ assert_published FakeMessage, test_attr: 'attr'
30
+ handle_reply FakeReply, @message, result: 'OK'
31
+ assert_equal 'OK', result
32
+ end
33
+
34
+ def test_dropped_message
35
+ expect_published FakeMessage do
36
+ @reply_queue.handle_reply Tochtli::MessageDropped.new("Message dropped", @message), @message.id
37
+ end
38
+
39
+ assert_raises Tochtli::MessageDropped do
40
+ @fake_client.do_sync
41
+ end
42
+ end
43
+
44
+ class FakeClient < Tochtli::BaseClient
45
+ def do_sync(attr=nil)
46
+ handler = SyncMessageHandler.new
47
+ timeout = 1
48
+ @rabbit_client.publish FakeMessage.new(test_attr: attr), handler: handler, timeout: timeout
49
+ reply = handler.wait!(timeout)
50
+ reply.result
51
+ end
52
+
53
+ def do_async(attr, &block)
54
+ handler = ->(reply) { block.call(reply.result) }
55
+ timeout = 1
56
+ @rabbit_client.publish FakeMessage.new(test_attr: attr), handler: handler, timeout: timeout
57
+ end
58
+ end
59
+
60
+ class FakeMessage < Tochtli::Message
61
+ attribute :test_attr, String, required: false
62
+ end
63
+
64
+ class FakeReply < Tochtli::Message
65
+ attribute :result, String
66
+ end
67
+
68
+ end
@@ -0,0 +1,87 @@
1
+ require_relative 'test_helper'
2
+ require 'benchmark'
3
+
4
+ class ControllerFunctionalTest < Minitest::Test
5
+ include Tochtli::Test::Controller
6
+
7
+ class TestMessage < Tochtli::Message
8
+ route_to 'fn.test.echo'
9
+
10
+ attribute :text, String
11
+ end
12
+
13
+ class CustomTopicMessage < Tochtli::Message
14
+ route_to { "fn.test.#{key}.#{action}" }
15
+
16
+ attribute :resource, String
17
+
18
+ attr_accessor :key, :action
19
+ end
20
+
21
+ class TestEchoReply < Tochtli::Message
22
+ attribute :original_text, String
23
+ end
24
+
25
+ class TestCustomReply < Tochtli::Message
26
+ attribute :message, String
27
+ end
28
+
29
+ class TestController < Tochtli::BaseController
30
+ bind 'fn.test.#'
31
+
32
+ on TestMessage, :echo
33
+ on CustomTopicMessage, routing_key: 'fn.test.1234.accept' do
34
+ reply TestCustomReply.new(:message => "#{message.resource} accepted")
35
+ end
36
+
37
+ on CustomTopicMessage, routing_key: 'fn.test.off.accept' do
38
+ raise "Should not reach this code"
39
+ end
40
+ off 'fn.test.off.accept'
41
+
42
+ on CustomTopicMessage, routing_key: 'fn.test.*.reject' do
43
+ reply TestCustomReply.new(:message => "#{message.resource} rejected")
44
+ end
45
+
46
+ def echo
47
+ reply TestEchoReply.new(:original_text => message.text)
48
+ end
49
+ end
50
+
51
+ tests TestController
52
+
53
+ def test_echo_command
54
+ message = TestMessage.new(:text => 'Hello world!')
55
+
56
+ publish message
57
+
58
+ assert_kind_of TestEchoReply, @reply
59
+ assert_equal message.text, @reply.original_text
60
+ end
61
+
62
+ def test_accept_command
63
+ message = CustomTopicMessage.new(action: 'accept', key: '1234', resource: 'Red car')
64
+
65
+ publish message
66
+
67
+ assert_kind_of TestCustomReply, @reply
68
+ assert_equal "Red car accepted", @reply.message
69
+ end
70
+
71
+ def test_off_key
72
+ message = CustomTopicMessage.new(action: 'accept', key: 'off', resource: 'Red car')
73
+
74
+ assert_raises RoutingNotFound do
75
+ publish message
76
+ end
77
+ end
78
+
79
+ def test_reject_command
80
+ message = CustomTopicMessage.new(action: 'reject', key: '1234', resource: 'Red car')
81
+
82
+ publish message
83
+
84
+ assert_kind_of TestCustomReply, @reply
85
+ assert_equal "Red car rejected", @reply.message
86
+ end
87
+ end
@@ -0,0 +1,274 @@
1
+ require_relative 'test_helper'
2
+ require 'benchmark'
3
+
4
+ Thread.abort_on_exception = true
5
+
6
+ class ControllerIntegrationTest < Minitest::Test
7
+ include Tochtli::Test::Integration
8
+
9
+ class TestMessage < Tochtli::Message
10
+ route_to 'test.controller.echo'
11
+
12
+ attribute :text, String
13
+ end
14
+
15
+ class ErrorMessage < Tochtli::Message
16
+ route_to 'test.controller.error'
17
+ end
18
+
19
+ class FailureMessage < Tochtli::Message
20
+ route_to 'test.controller.failure'
21
+ end
22
+
23
+ class SleepyMessage < Tochtli::Message
24
+ route_to 'test.controller.sleepy'
25
+
26
+ attribute :duration, Float
27
+ end
28
+
29
+ class DroppedMessage < Tochtli::Message
30
+ route_to 'test.invalid.route'
31
+ end
32
+
33
+ class TestEchoReply < Tochtli::Message
34
+ attribute :original_text, String
35
+ end
36
+
37
+ class TestController < Tochtli::BaseController
38
+ bind 'test.controller.*'
39
+
40
+ self.work_pool_size = 10
41
+
42
+ on TestMessage, :echo
43
+ on ErrorMessage, :error
44
+ on FailureMessage, :failure
45
+ on SleepyMessage, :sleepy
46
+
47
+ def echo
48
+ reply TestEchoReply.new(:original_text => message.text)
49
+ end
50
+
51
+ def error
52
+ raise "Error"
53
+ end
54
+
55
+ def failure
56
+ @rabbit_connection.connection.close # simulate network failure
57
+ end
58
+
59
+ def sleepy
60
+ sleep message.duration
61
+ reply TestEchoReply.new(:original_text => "Done after #{message.duration}s")
62
+ end
63
+ end
64
+
65
+ class CustomNameController < Tochtli::BaseController
66
+ self.queue_name = 'test/custom/queue/name'
67
+ end
68
+
69
+ class CustomExchangeController < Tochtli::BaseController
70
+ self.queue_name = ''
71
+ self.queue_durable = false
72
+ self.queue_exclusive = true
73
+ self.exchange_type = :fanout
74
+ self.exchange_name = 'test.notifications'
75
+ self.exchange_durable = false
76
+ end
77
+
78
+ class BeforeSetupBindingController < Tochtli::BaseController
79
+ before_setup do
80
+ bind 'custom.topic'
81
+ end
82
+ end
83
+
84
+ class TestReplyHandler
85
+ attr_reader :pending_replies, :errors, :timeouts
86
+
87
+ def initialize(expected_replies)
88
+ @pending_replies = expected_replies
89
+ @errors = 0
90
+ @timeouts = 0
91
+ @mutex = Mutex.new
92
+ @cv = ConditionVariable.new
93
+ end
94
+
95
+ def call(reply)
96
+ @mutex.synchronize do
97
+ @pending_replies -= 1
98
+ @cv.signal if done?
99
+ end
100
+ end
101
+
102
+ def on_error(reply)
103
+ @errors += 1
104
+ call(reply)
105
+ end
106
+
107
+ def on_timeout(original_message)
108
+ @timeouts += 1
109
+ call(nil)
110
+ end
111
+
112
+ def wait(timeout)
113
+ timeout = timeout.to_f unless timeout.nil? # ensure it is numerical, e.g. for Rubinius compatibility
114
+ @mutex.synchronize { done? || @cv.wait(@mutex, timeout) }
115
+ end
116
+
117
+ protected
118
+
119
+ def done?
120
+ @pending_replies == 0
121
+ end
122
+ end
123
+
124
+ def test_echo_command
125
+ message = TestMessage.new(:text => 'Hello world!')
126
+
127
+ publish message, :expect => TestEchoReply
128
+
129
+ assert_equal message.text, @reply.original_text
130
+ end
131
+
132
+ def test_error
133
+ message = ErrorMessage.new
134
+ handler = TestReplyHandler.new(1)
135
+
136
+ publish message, :reply_handler => handler, :timeout => 1.5
137
+
138
+ handler.wait(2)
139
+
140
+ assert_equal 1, handler.errors
141
+ end
142
+
143
+ def test_network_failure
144
+ message = FailureMessage.new
145
+ handler = TestReplyHandler.new(1)
146
+
147
+ publish message, :reply_handler => handler, :timeout => 2.5
148
+
149
+ handler.wait(3)
150
+
151
+ assert_equal 1, handler.timeouts
152
+ end
153
+
154
+ def test_sleepy
155
+ count = 20
156
+ handler = TestReplyHandler.new(count)
157
+ start_t = Time.now
158
+ count.times do
159
+ message = SleepyMessage.new(:duration => 0.2)
160
+ publish message, :expect => TestEchoReply, :reply_handler => handler, :timeout => 3.0
161
+ end
162
+
163
+ handler.wait(4)
164
+
165
+ duration = Time.now - start_t
166
+
167
+ assert_equal 0, handler.errors
168
+ assert_equal 0, handler.timeouts
169
+ assert duration < 3.0, "The total processing time should be less then the processing time sum (multi customers expected), duration: #{duration}s"
170
+ end
171
+
172
+ def test_dropped
173
+ message = DroppedMessage.new
174
+ handler = TestReplyHandler.new(1)
175
+
176
+ publish message, :reply_handler => handler, :timeout => 0.5
177
+
178
+ handler.wait(1)
179
+
180
+ assert_equal 1, handler.errors
181
+ end
182
+
183
+ def restart_test(timeout)
184
+ count = 20
185
+ handler = TestReplyHandler.new(count)
186
+ count.times do
187
+ message = SleepyMessage.new(:duration => 0.5)
188
+ publish message, :expect => TestEchoReply, :reply_handler => handler, :timeout => 3.0
189
+ end
190
+ sleep(0.3)
191
+ refute_equal 0, handler.pending_replies
192
+
193
+ # right now there should be some messages not processed
194
+ # restart should wait until they are done
195
+
196
+ TestController.restart(timeout: timeout)
197
+ handler.wait(4)
198
+
199
+ handler
200
+ end
201
+
202
+ def test_graceful_restart
203
+ handler = restart_test(15)
204
+
205
+ assert_equal 0, handler.pending_replies
206
+ assert_equal 0, handler.errors
207
+ assert_equal 0, handler.timeouts
208
+ end
209
+
210
+ def test_forced_restart
211
+ handler = restart_test(0)
212
+
213
+ assert_equal 0, handler.pending_replies
214
+ refute_equal 0, handler.errors + handler.timeouts
215
+ end
216
+
217
+
218
+ def test_echo_performance
219
+ begin
220
+ @logger.level = Logger::ERROR # mute logger to speed up test
221
+
222
+ count = 200
223
+ handler = TestReplyHandler.new(count)
224
+
225
+ start_t = Time.now
226
+
227
+ count.times do |i|
228
+ message = TestMessage.new(:text => "#{i}: Hello world!")
229
+ publish message, :expect => TestEchoReply, :reply_handler => handler, :timeout => 6
230
+ end
231
+
232
+ handler.wait(2)
233
+
234
+ end_t = Time.now
235
+ time = end_t - start_t
236
+
237
+ assert_equal 0, handler.errors
238
+ assert_equal 0, handler.timeouts
239
+ assert_equal 0, handler.pending_replies
240
+
241
+ puts "Published: #{count} in #{time} (#{count/time}req/s)"
242
+ ensure
243
+ @logger.level = Logger::DEBUG
244
+ end
245
+ end
246
+
247
+ def test_default_queue_name
248
+ assert_equal 'controller_integration_test/test_controller', TestController.queue_name
249
+ assert @connection.queue_exists?('controller_integration_test/test_controller')
250
+ end
251
+
252
+ def test_custom_queue_name
253
+ assert_equal 'test/custom/queue/name', CustomNameController.queue_name
254
+ assert @connection.queue_exists?('test/custom/queue/name')
255
+ end
256
+
257
+ def test_custom_exchange
258
+ dispatcher = CustomExchangeController.dispatcher
259
+ queue = dispatcher.queues.first
260
+ refute_nil dispatcher
261
+ refute_nil queue
262
+ assert_equal '', CustomExchangeController.queue_name
263
+ assert_match /^amq.gen/, queue.name
264
+ refute queue.durable?
265
+ assert queue.exclusive?
266
+ assert queue.server_named?
267
+ refute_nil queue.channel.exchanges['test.notifications']
268
+ refute queue.channel.exchanges['test.notifications'].durable?
269
+ end
270
+
271
+ def test_binding_on_setup
272
+ assert BeforeSetupBindingController.routing_keys.include?('custom.topic')
273
+ end
274
+ end