semian 0.6.0 → 0.6.1

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