semian 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,515 +0,0 @@
1
- require 'test_helper'
2
- require 'semian/net_http'
3
- require 'thin'
4
-
5
- class TestNetHTTP < Minitest::Test
6
- class RackServer
7
- def self.call(env)
8
- response_code = env['REQUEST_URI'].delete("/")
9
- response_code = '200' if response_code == ""
10
- [response_code, {'Content-Type' => 'text/html'}, ['Success']]
11
- end
12
- end
13
-
14
- HOSTNAME = "127.0.0.1"
15
- PORT = 31_050
16
- TOXIC_PORT = PORT + 1
17
- DEFAULT_SEMIAN_OPTIONS = {
18
- tickets: 3,
19
- success_threshold: 1,
20
- error_threshold: 3,
21
- error_timeout: 10,
22
- }.freeze
23
- DEFAULT_SEMIAN_CONFIGURATION = proc do |host, port|
24
- next nil if host == "127.0.0.1" && port == 8474 # disable if toxiproxy
25
- DEFAULT_SEMIAN_OPTIONS.merge(name: "#{host}_#{port}")
26
- end
27
-
28
- def test_with_server_raises_if_binding_fails
29
- # Occurs when trying to bind to invalid addresses, like non-private
30
- # addresses, or when the address is already bound to something else
31
- with_server do
32
- assert_raises RuntimeError do
33
- with_server {}
34
- end
35
- end
36
- end
37
-
38
- def test_semian_identifier
39
- with_server do
40
- with_semian_configuration do
41
- Net::HTTP.start(HOSTNAME, TOXIC_PORT) do |http|
42
- assert_equal "nethttp_#{HOSTNAME}_#{TOXIC_PORT}", http.semian_identifier
43
- end
44
- Net::HTTP.start("127.0.0.1", TOXIC_PORT) do |http|
45
- assert_equal "nethttp_127.0.0.1_#{TOXIC_PORT}", http.semian_identifier
46
- end
47
- end
48
- end
49
- end
50
-
51
- def test_trigger_open
52
- with_semian_configuration do
53
- with_server do
54
- open_circuit!
55
- uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
56
- assert_raises Net::CircuitOpenError do
57
- Net::HTTP.get(uri)
58
- end
59
- end
60
- end
61
- end
62
-
63
- def test_trigger_close_after_open
64
- with_semian_configuration do
65
- with_server do
66
- open_circuit!
67
- close_circuit!
68
- end
69
- end
70
- end
71
-
72
- def test_bulkheads_tickets_are_working
73
- options = proc do |host, port|
74
- {
75
- tickets: 2,
76
- success_threshold: 1,
77
- error_threshold: 3,
78
- error_timeout: 10,
79
- name: "#{host}_#{port}",
80
- }
81
- end
82
- with_semian_configuration(options) do
83
- with_server do
84
- http_1 = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
85
- http_1.semian_resource.acquire do
86
- http_2 = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
87
- http_2.semian_resource.acquire do
88
- assert_raises Net::ResourceBusyError do
89
- Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/"))
90
- end
91
- end
92
- end
93
- end
94
- end
95
- end
96
-
97
- def test_get_is_protected
98
- with_semian_configuration do
99
- with_server do
100
- open_circuit!
101
- assert_raises Net::CircuitOpenError do
102
- Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200"))
103
- end
104
- end
105
- end
106
- end
107
-
108
- def test_instance_get_is_protected
109
- with_semian_configuration do
110
- with_server do
111
- open_circuit!
112
- assert_raises Net::CircuitOpenError do
113
- http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
114
- http.get("/")
115
- end
116
- end
117
- end
118
- end
119
-
120
- def test_get_response_is_protected
121
- with_semian_configuration do
122
- with_server do
123
- open_circuit!
124
- assert_raises Net::CircuitOpenError do
125
- uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
126
- Net::HTTP.get_response(uri)
127
- end
128
- end
129
- end
130
- end
131
-
132
- def test_post_form_is_protected
133
- with_semian_configuration do
134
- with_server do
135
- open_circuit!
136
- assert_raises Net::CircuitOpenError do
137
- uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
138
- Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
139
- end
140
- end
141
- end
142
- end
143
-
144
- def test_http_start_method_is_protected
145
- with_semian_configuration do
146
- with_server do
147
- open_circuit!
148
- uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
149
- assert_raises Net::CircuitOpenError do
150
- Net::HTTP.start(uri.host, uri.port) {}
151
- end
152
- close_circuit!
153
- end
154
- end
155
- end
156
-
157
- def test_http_action_request_inside_start_methods_are_protected
158
- with_semian_configuration do
159
- with_server do
160
- uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
161
- Net::HTTP.start(uri.host, uri.port) do |http|
162
- open_circuit!
163
- get_subclasses(Net::HTTPRequest).each do |action|
164
- assert_raises(Net::CircuitOpenError, "#{action.name} did not raise a Net::CircuitOpenError") do
165
- request = action.new uri
166
- http.request(request)
167
- end
168
- end
169
- end
170
- end
171
- end
172
- end
173
-
174
- def test_custom_raw_semian_options_work_with_lookup
175
- with_server do
176
- semian_config = {}
177
- semian_config["development"] = {}
178
- semian_config["development"]["nethttp_#{HOSTNAME}_#{TOXIC_PORT}"] = DEFAULT_SEMIAN_OPTIONS
179
- sample_env = "development"
180
-
181
- semian_configuration_proc = proc do |host, port|
182
- semian_identifier = "nethttp_#{host}_#{port}"
183
- semian_config[sample_env][semian_identifier].merge(name: "#{host}_#{port}")
184
- end
185
-
186
- with_semian_configuration(semian_configuration_proc) do
187
- Net::HTTP.start(HOSTNAME, TOXIC_PORT) do |http|
188
- assert_equal semian_config["development"][http.semian_identifier],
189
- http.raw_semian_options.dup.tap { |o| o.delete(:name) }
190
- end
191
- end
192
- end
193
- end
194
-
195
- def test_custom_raw_semian_options_can_only_assign_once
196
- semian_configuration_proc = proc do |host, port|
197
- DEFAULT_SEMIAN_OPTIONS.merge(name: "#{host}_#{port}")
198
- end
199
- with_semian_configuration(semian_configuration_proc) do
200
- assert_raises(Semian::NetHTTP::SemianConfigurationChangedError) do
201
- Semian::NetHTTP.semian_configuration = semian_configuration_proc
202
- end
203
- end
204
- end
205
-
206
- def test_custom_raw_semian_options_work_with_default_fallback
207
- with_server do
208
- semian_config = {}
209
- semian_config["development"] = {}
210
- semian_config["development"]["nethttp_default"] = DEFAULT_SEMIAN_OPTIONS
211
- sample_env = "development"
212
-
213
- semian_configuration_proc = proc do |host, port|
214
- semian_identifier = "nethttp_#{host}_#{port}"
215
- semian_identifier = "nethttp_default" unless semian_config[sample_env].key?(semian_identifier)
216
- semian_config[sample_env][semian_identifier].merge(name: "default")
217
- end
218
- Semian["nethttp_default"].reset if Semian["nethttp_default"]
219
- Semian.destroy("nethttp_default")
220
- with_semian_configuration(semian_configuration_proc) do
221
- Net::HTTP.start(HOSTNAME, PORT) do |http|
222
- expected_config = semian_config["development"]["nethttp_default"].dup
223
- assert_equal expected_config, http.raw_semian_options.dup.tap { |o| o.delete(:name) }
224
- end
225
- end
226
- end
227
- end
228
-
229
- def test_custom_raw_semian_options_can_disable_using_nil
230
- with_server do
231
- semian_configuration_proc = proc { nil }
232
- with_semian_configuration(semian_configuration_proc) do
233
- http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
234
- assert_equal true, http.disabled?
235
- end
236
- end
237
- end
238
-
239
- def test_use_custom_configuration_to_combine_endpoints_into_one_resource
240
- semian_config = {}
241
- semian_config["development"] = {}
242
- semian_config["development"]["nethttp_default"] = DEFAULT_SEMIAN_OPTIONS
243
- sample_env = "development"
244
-
245
- semian_configuration_proc = proc do |host, port|
246
- next nil if host == "127.0.0.1" && port == 8474 # # disable if toxiproxy
247
- semian_identifier = "nethttp_default"
248
- semian_config[sample_env][semian_identifier].merge(name: "default")
249
- end
250
-
251
- with_semian_configuration(semian_configuration_proc) do
252
- Semian["nethttp_default"].reset if Semian["nethttp_default"]
253
- Semian.destroy("nethttp_default")
254
- with_server do
255
- open_circuit!
256
- end
257
- with_server(addresses: ["#{HOSTNAME}:#{PORT}", "#{HOSTNAME}:#{PORT + 100}"], reset_semian_state: false) do
258
- assert_raises Net::CircuitOpenError do
259
- Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200"))
260
- end
261
- end
262
- end
263
- end
264
-
265
- def test_custom_raw_semian_options_can_disable_with_invalid_key
266
- with_server do
267
- semian_config = {}
268
- semian_config["development"] = {}
269
- semian_config["development"]["nethttp_#{HOSTNAME}_#{TOXIC_PORT}"] = DEFAULT_SEMIAN_OPTIONS
270
- sample_env = "development"
271
-
272
- semian_configuration_proc = proc do |host, port|
273
- semian_identifier = "nethttp_#{host}_#{port}"
274
- semian_config[sample_env][semian_identifier]
275
- end
276
- with_semian_configuration(semian_configuration_proc) do
277
- http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
278
- assert_equal false, http.disabled?
279
-
280
- http = Net::HTTP.new(HOSTNAME, TOXIC_PORT + 100)
281
- assert_equal true, http.disabled?
282
- end
283
- end
284
- end
285
-
286
- def test_adding_extra_errors_and_resetting_affects_exceptions_list
287
- orig_errors = Semian::NetHTTP.exceptions.dup
288
- Semian::NetHTTP.exceptions += [::OpenSSL::SSL::SSLError]
289
- assert_equal(orig_errors + [::OpenSSL::SSL::SSLError], Semian::NetHTTP.exceptions)
290
- Semian::NetHTTP.reset_exceptions
291
- assert_equal(Semian::NetHTTP::DEFAULT_ERRORS, Semian::NetHTTP.exceptions)
292
- ensure
293
- Semian::NetHTTP.exceptions = orig_errors
294
- end
295
-
296
- def test_adding_custom_errors_do_trip_circuit
297
- with_semian_configuration do
298
- with_custom_errors([::OpenSSL::SSL::SSLError]) do
299
- with_server do
300
- http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
301
- http.use_ssl = true
302
- http.raw_semian_options[:error_threshold].times do
303
- assert_raises ::OpenSSL::SSL::SSLError do
304
- http.get("/200")
305
- end
306
- end
307
- assert_raises Net::CircuitOpenError do
308
- http.get("/200")
309
- end
310
- end
311
- end
312
- end
313
- end
314
-
315
- def test_multiple_different_endpoints_and_ports_are_tracked_differently
316
- with_semian_configuration do
317
- addresses = ["#{HOSTNAME}:#{PORT}", "#{HOSTNAME}:#{PORT + 100}"]
318
- addresses.each do |address|
319
- hostname, port = address.split(":")
320
- port = port.to_i
321
- reset_semian_resource(hostname: hostname, port: port)
322
- end
323
- with_server(addresses: addresses, reset_semian_state: false) do |hostname, port|
324
- with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |name|
325
- Net::HTTP.get(URI("http://#{hostname}:#{port + 1}/"))
326
- open_circuit!(hostname: hostname, toxic_port: port + 1, toxic_name: name)
327
- assert_raises Net::CircuitOpenError do
328
- Net::HTTP.get(URI("http://#{hostname}:#{port + 1}/"))
329
- end
330
- end
331
- end
332
- with_server(addresses: ["127.0.0.1:#{PORT}"], reset_semian_state: false) do
333
- # different endpoint, should not raise errors even though localhost == 127.0.0.1
334
- Net::HTTP.get(URI("http://127.0.0.1:#{PORT + 1}/"))
335
- end
336
- end
337
- end
338
-
339
- def test_persistent_state_after_server_restart
340
- with_semian_configuration do
341
- with_server(addresses: ["#{HOSTNAME}:#{PORT + 100}"]) do |hostname, port|
342
- with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |name|
343
- open_circuit!(hostname: hostname, toxic_port: port + 1, toxic_name: name)
344
- end
345
- end
346
- with_server(addresses: ["#{HOSTNAME}:#{PORT + 100}"], reset_semian_state: false) do |hostname, port|
347
- with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |_|
348
- assert_raises Net::CircuitOpenError do
349
- Net::HTTP.get(URI("http://#{HOSTNAME}:#{port + 1}/200"))
350
- end
351
- end
352
- end
353
- end
354
- end
355
-
356
- private
357
-
358
- def with_semian_configuration(options = DEFAULT_SEMIAN_CONFIGURATION)
359
- orig_semian_options = Semian::NetHTTP.semian_configuration
360
- Semian::NetHTTP.instance_variable_set(:@semian_configuration, nil)
361
- mutated_objects = {}
362
- Semian::NetHTTP.send(:alias_method, :orig_semian_resource, :semian_resource)
363
- Semian::NetHTTP.send(:alias_method, :orig_raw_semian_options, :raw_semian_options)
364
- Semian::NetHTTP.send(:define_method, :semian_resource) do
365
- mutated_objects[self] = [@semian_resource, @raw_semian_options] unless mutated_objects.key?(self)
366
- orig_semian_resource
367
- end
368
- Semian::NetHTTP.send(:define_method, :raw_semian_options) do
369
- mutated_objects[self] = [@semian_resource, @raw_semian_options] unless mutated_objects.key?(self)
370
- orig_raw_semian_options
371
- end
372
-
373
- Semian::NetHTTP.semian_configuration = options
374
- yield
375
- ensure
376
- Semian::NetHTTP.instance_variable_set(:@semian_configuration, nil)
377
- Semian::NetHTTP.semian_configuration = orig_semian_options
378
- Semian::NetHTTP.send(:alias_method, :semian_resource, :orig_semian_resource)
379
- Semian::NetHTTP.send(:alias_method, :raw_semian_options, :orig_raw_semian_options)
380
- Semian::NetHTTP.send(:undef_method, :orig_semian_resource, :orig_raw_semian_options)
381
- mutated_objects.each do |instance, (res, opt)| # Sadly, only way to fully restore cached properties
382
- instance.instance_variable_set(:@semian_resource, res)
383
- instance.instance_variable_set(:@raw_semian_options, opt)
384
- end
385
- end
386
-
387
- def with_custom_errors(errors)
388
- orig_errors = Semian::NetHTTP.exceptions.dup
389
- Semian::NetHTTP.exceptions += errors
390
- yield
391
- ensure
392
- Semian::NetHTTP.exceptions = orig_errors
393
- end
394
-
395
- def get_subclasses(klass)
396
- ObjectSpace.each_object(klass.singleton_class).to_a - [klass]
397
- end
398
-
399
- def open_circuit!(hostname: HOSTNAME, toxic_port: TOXIC_PORT, toxic_name: "semian_test_net_http")
400
- Net::HTTP.start(hostname, toxic_port) do |http|
401
- http.read_timeout = 0.1
402
- uri = URI("http://#{hostname}:#{toxic_port}/200")
403
- http.raw_semian_options[:error_threshold].times do
404
- # Cause error error_threshold times so circuit opens
405
- Toxiproxy[toxic_name].downstream(:latency, latency: 150).apply do
406
- request = Net::HTTP::Get.new(uri)
407
- assert_raises Net::ReadTimeout do
408
- http.request(request)
409
- end
410
- end
411
- end
412
- end
413
- end
414
-
415
- def close_circuit!(hostname: HOSTNAME, toxic_port: TOXIC_PORT)
416
- http = Net::HTTP.new(hostname, toxic_port)
417
- Timecop.travel(http.raw_semian_options[:error_timeout])
418
- # Cause successes success_threshold times so circuit closes
419
- http.raw_semian_options[:success_threshold].times do
420
- response = http.get("/200")
421
- assert(200, response.code)
422
- end
423
- end
424
-
425
- def with_server(addresses: ["#{HOSTNAME}:#{PORT}"], reset_semian_state: true)
426
- addresses.each do |address|
427
- hostname, port = address.split(":")
428
- begin
429
- server = nil
430
- server_threw_error = false
431
- server_thread = Thread.new do
432
- Thin::Logging.silent = true
433
- server = Thin::Server.new(hostname, port, RackServer)
434
- begin
435
- server.start
436
- rescue StandardError
437
- server_threw_error = true
438
- raise
439
- end
440
- end
441
-
442
- begin
443
- poll_until_ready(hostname: hostname, port: port)
444
- rescue RuntimeError
445
- server_thread.kill
446
- server_thread.join if server_threw_error
447
- raise
448
- end
449
-
450
- assert(server.running?)
451
- reset_semian_resource(hostname: hostname, port: port) if reset_semian_state
452
- @proxy = Toxiproxy[:semian_test_net_http]
453
- yield(hostname, port.to_i)
454
- ensure
455
- server_thread.kill
456
- poll_until_gone(hostname: hostname, port: port)
457
- end
458
- end
459
- end
460
-
461
- def reset_semian_resource(hostname:, port:)
462
- Semian["nethttp_#{hostname}_#{port}"].reset if Semian["nethttp_#{hostname}_#{port}"]
463
- Semian["nethttp_#{hostname}_#{port.to_i + 1}"].reset if Semian["nethttp_#{hostname}_#{port.to_i + 1}"]
464
- Semian.destroy("nethttp_#{hostname}_#{port}")
465
- Semian.destroy("nethttp_#{hostname}_#{port.to_i + 1}")
466
- end
467
-
468
- def with_toxic(hostname: HOSTNAME, upstream_port: PORT, toxic_port: upstream_port + 1)
469
- old_proxy = @proxy
470
- name = "semian_test_net_http_#{hostname}_#{upstream_port}<-#{toxic_port}"
471
- Toxiproxy.populate([
472
- {
473
- name: name,
474
- upstream: "#{hostname}:#{upstream_port}",
475
- listen: "#{hostname}:#{toxic_port}",
476
- },
477
- ])
478
- @proxy = Toxiproxy[name]
479
- yield(name)
480
- rescue StandardError
481
- ensure
482
- @proxy = old_proxy
483
- begin
484
- Toxiproxy[name].destroy
485
- rescue StandardError
486
- end
487
- end
488
-
489
- def poll_until_ready(hostname: HOSTNAME, port: PORT, time_to_wait: 1)
490
- start_time = Time.now.to_i
491
- begin
492
- TCPSocket.new(hostname, port).close
493
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET
494
- if Time.now.to_i > start_time + time_to_wait
495
- raise "Couldn't reach the service on hostname #{hostname} port #{port} after #{time_to_wait}s"
496
- else
497
- retry
498
- end
499
- end
500
- end
501
-
502
- def poll_until_gone(hostname: HOSTNAME, port: PORT, time_to_wait: 1)
503
- start_time = Time.now.to_i
504
- loop do
505
- if Time.now.to_i > start_time + time_to_wait
506
- raise "Could still reach the service on hostname #{hostname} port #{port} after #{time_to_wait}s"
507
- end
508
- begin
509
- TCPSocket.new(hostname, port).close
510
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET
511
- return true
512
- end
513
- end
514
- end
515
- end