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.
- checksums.yaml +7 -0
- data/.travis.yml +14 -0
- data/Gemfile +32 -0
- data/History.md +138 -0
- data/README.md +46 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/assets/communication.png +0 -0
- data/assets/layers.png +0 -0
- data/examples/01-screencap-service/Gemfile +3 -0
- data/examples/01-screencap-service/README.md +5 -0
- data/examples/01-screencap-service/client.rb +15 -0
- data/examples/01-screencap-service/common.rb +15 -0
- data/examples/01-screencap-service/server.rb +26 -0
- data/examples/02-log-analyzer/Gemfile +3 -0
- data/examples/02-log-analyzer/README.md +5 -0
- data/examples/02-log-analyzer/client.rb +95 -0
- data/examples/02-log-analyzer/common.rb +33 -0
- data/examples/02-log-analyzer/sample.log +10001 -0
- data/examples/02-log-analyzer/server.rb +133 -0
- data/lib/tochtli.rb +177 -0
- data/lib/tochtli/active_record_connection_cleaner.rb +9 -0
- data/lib/tochtli/application.rb +135 -0
- data/lib/tochtli/base_client.rb +135 -0
- data/lib/tochtli/base_controller.rb +360 -0
- data/lib/tochtli/controller_manager.rb +99 -0
- data/lib/tochtli/engine.rb +15 -0
- data/lib/tochtli/message.rb +114 -0
- data/lib/tochtli/rabbit_client.rb +36 -0
- data/lib/tochtli/rabbit_connection.rb +249 -0
- data/lib/tochtli/reply_queue.rb +129 -0
- data/lib/tochtli/simple_validation.rb +23 -0
- data/lib/tochtli/test.rb +9 -0
- data/lib/tochtli/test/client.rb +28 -0
- data/lib/tochtli/test/controller.rb +66 -0
- data/lib/tochtli/test/integration.rb +78 -0
- data/lib/tochtli/test/memory_cache.rb +22 -0
- data/lib/tochtli/test/test_case.rb +191 -0
- data/lib/tochtli/test/test_unit.rb +22 -0
- data/lib/tochtli/version.rb +3 -0
- data/log_generator.rb +11 -0
- data/test/base_client_test.rb +68 -0
- data/test/controller_functional_test.rb +87 -0
- data/test/controller_integration_test.rb +274 -0
- data/test/controller_manager_test.rb +75 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/config/application.rb +36 -0
- data/test/dummy/config/boot.rb +4 -0
- data/test/dummy/config/database.yml +3 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/rabbit.yml +4 -0
- data/test/dummy/db/.gitkeep +0 -0
- data/test/dummy/log/.gitkeep +0 -0
- data/test/key_matcher_test.rb +100 -0
- data/test/log/.gitkeep +0 -0
- data/test/message_test.rb +80 -0
- data/test/rabbit_client_test.rb +71 -0
- data/test/rabbit_connection_test.rb +151 -0
- data/test/test_helper.rb +32 -0
- data/test/version_test.rb +8 -0
- data/tochtli.gemspec +129 -0
- 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
|
data/log_generator.rb
ADDED
@@ -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
|