tochtli 0.5.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 (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