semian 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ module Semian
2
+ module Simple
3
+ class Integer #:nodoc:
4
+ attr_accessor :value
5
+
6
+ def initialize
7
+ reset
8
+ end
9
+
10
+ def increment(val = 1)
11
+ @value += val
12
+ end
13
+
14
+ def reset
15
+ @value = 0
16
+ end
17
+
18
+ def destroy
19
+ reset
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module Semian
2
+ module Simple
3
+ class SlidingWindow #:nodoc:
4
+ extend Forwardable
5
+
6
+ def_delegators :@window, :size, :pop, :shift, :first, :last
7
+ attr_reader :max_size
8
+
9
+ # A sliding window is a structure that stores the most @max_size recent timestamps
10
+ # like this: if @max_size = 4, current time is 10, @window =[5,7,9,10].
11
+ # Another push of (11) at 11 sec would make @window [7,9,10,11], shifting off 5.
12
+
13
+ def initialize(max_size:)
14
+ @max_size = max_size
15
+ @window = []
16
+ end
17
+
18
+ def resize_to(size)
19
+ raise ArgumentError.new('size must be larger than 0') if size < 1
20
+ @max_size = size
21
+ @window.shift while @window.size > @max_size
22
+ self
23
+ end
24
+
25
+ def push(value)
26
+ @window.shift while @window.size >= @max_size
27
+ @window << value
28
+ self
29
+ end
30
+
31
+ alias_method :<<, :push
32
+
33
+ def clear
34
+ @window.clear
35
+ self
36
+ end
37
+
38
+ def destroy
39
+ clear
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ module Semian
2
+ module Simple
3
+ class State #:nodoc:
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ attr_reader :value
9
+
10
+ def open?
11
+ value == :open
12
+ end
13
+
14
+ def closed?
15
+ value == :closed
16
+ end
17
+
18
+ def half_open?
19
+ value == :half_open
20
+ end
21
+
22
+ def open
23
+ @value = :open
24
+ end
25
+
26
+ def close
27
+ @value = :closed
28
+ end
29
+
30
+ def half_open
31
+ @value = :half_open
32
+ end
33
+
34
+ def reset
35
+ close
36
+ end
37
+
38
+ def destroy
39
+ reset
40
+ end
41
+ end
42
+ end
43
+ end
@@ -34,7 +34,10 @@ module Semian
34
34
  true
35
35
  end
36
36
 
37
- def mark_failed(error)
37
+ def mark_failed(_error)
38
+ end
39
+
40
+ def mark_success
38
41
  end
39
42
  end
40
43
  end
@@ -1,3 +1,3 @@
1
1
  module Semian
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -0,0 +1 @@
1
+ classification: library
@@ -6,9 +6,9 @@ if which toxiproxy > /dev/null; then
6
6
  fi
7
7
 
8
8
  if which apt-get > /dev/null; then
9
- echo "Installing toxiproxy-1.0.0.deb"
10
- wget -O /tmp/toxiproxy-1.0.0.deb https://github.com/Shopify/toxiproxy/releases/download/v1.0.0/toxiproxy_1.0.0_amd64.deb
11
- sudo dpkg -i /tmp/toxiproxy-1.0.0.deb
9
+ echo "Installing toxiproxy"
10
+ wget -O /tmp/toxiproxy.deb https://github.com/Shopify/toxiproxy/releases/download/v2.0.0rc1/toxiproxy_2.0.0rc1_amd64.deb
11
+ sudo dpkg -i /tmp/toxiproxy.deb
12
12
  sudo service toxiproxy start
13
13
  exit 0
14
14
  fi
@@ -1,4 +1,4 @@
1
- $:.unshift File.expand_path('../lib', __FILE__)
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
2
 
3
3
  require 'semian/version'
4
4
  require 'semian/platform'
@@ -11,9 +11,9 @@ Gem::Specification.new do |s|
11
11
  A Ruby C extention that is used to control access to shared resources
12
12
  across process boundaries with SysV semaphores.
13
13
  DOC
14
- s.homepage = 'https://github.com/csfrancis/semian'
14
+ s.homepage = 'https://github.com/shopify/semian'
15
15
  s.authors = ['Scott Francis', 'Simon Eskildsen']
16
- s.email = 'scott.francis@shopify.com'
16
+ s.email = 'scott.francis@shopify.com'
17
17
  s.license = 'MIT'
18
18
 
19
19
  s.files = `git ls-files`.split("\n")
@@ -22,5 +22,6 @@ Gem::Specification.new do |s|
22
22
  s.add_development_dependency 'timecop'
23
23
  s.add_development_dependency 'mysql2'
24
24
  s.add_development_dependency 'redis'
25
+ s.add_development_dependency 'thin'
25
26
  s.add_development_dependency 'toxiproxy'
26
27
  end
@@ -4,7 +4,11 @@ class TestCircuitBreaker < MiniTest::Unit::TestCase
4
4
  SomeError = Class.new(StandardError)
5
5
 
6
6
  def setup
7
- Semian.destroy(:testing) rescue nil
7
+ begin
8
+ Semian.destroy(:testing)
9
+ rescue
10
+ nil
11
+ end
8
12
  Semian.register(:testing, tickets: 1, exceptions: [SomeError], error_threshold: 2, error_timeout: 5, success_threshold: 1)
9
13
  @resource = Semian[:testing]
10
14
  end
@@ -108,7 +112,7 @@ class TestCircuitBreaker < MiniTest::Unit::TestCase
108
112
  def assert_circuit_opened(resource = @resource)
109
113
  open = false
110
114
  begin
111
- resource.acquire { }
115
+ resource.acquire {}
112
116
  rescue Semian::OpenCircuitError
113
117
  open = true
114
118
  end
@@ -2,7 +2,7 @@ module BackgroundHelper
2
2
  attr_writer :threads
3
3
 
4
4
  def teardown
5
- threads.each { |t| t.kill }
5
+ threads.each(&:kill)
6
6
  self.threads = []
7
7
  end
8
8
 
@@ -50,7 +50,7 @@ class TestInstrumentation < MiniTest::Unit::TestCase
50
50
 
51
51
  def assert_notify(*expected_events)
52
52
  events = []
53
- subscription = Semian.subscribe do |event, resource|
53
+ subscription = Semian.subscribe do |event, _resource|
54
54
  events << event
55
55
  end
56
56
  yield
@@ -70,7 +70,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
70
70
  end
71
71
 
72
72
  def test_resource_acquisition_for_connect
73
- client = connect_to_mysql!
73
+ connect_to_mysql!
74
74
 
75
75
  Semian[:mysql_testing].acquire do
76
76
  error = assert_raises Mysql2::ResourceBusyError do
@@ -160,6 +160,61 @@ class TestMysql2 < MiniTest::Unit::TestCase
160
160
  end
161
161
  end
162
162
 
163
+ def test_semian_allows_rollback
164
+ client = connect_to_mysql!
165
+
166
+ client.query('START TRANSACTION;')
167
+
168
+ Semian[:mysql_testing].acquire do
169
+ client.query('ROLLBACK;')
170
+ end
171
+ end
172
+
173
+ def test_semian_allows_commit
174
+ client = connect_to_mysql!
175
+
176
+ client.query('START TRANSACTION;')
177
+
178
+ Semian[:mysql_testing].acquire do
179
+ client.query('COMMIT;')
180
+ end
181
+ end
182
+
183
+ def test_query_whitelisted_returns_false_for_binary_sql
184
+ client = connect_to_mysql!
185
+
186
+ q = "INSERT IGNORE INTO `theme_template_bodies` (`cityhash`, `body`, `created_at`) VALUES ('716374049952273167', \
187
+ '\xB1\x01\xD0{\\\"current\\\":{\\\"bg_color\\\":\\\"#ff0000\\\"},\\\"presets\\\":{\\\"sandbox>,\\0\\07\x05\x01\x01, \
188
+ grey_bg\\\":6M\\0\\06\x05\x01\x01!\fblueJ!\\0\x01l\x04ff\x01!\bredJ \\0$ff0000\\\"}}}', '2015-11-06 19:08:03.498432')"
189
+ refute client.send(:query_whitelisted?, q)
190
+ end
191
+
192
+ def test_semian_allows_rollback_to_safepoint
193
+ client = connect_to_mysql!
194
+
195
+ client.query('START TRANSACTION;')
196
+ client.query('SAVEPOINT foobar;')
197
+
198
+ Semian[:mysql_testing].acquire do
199
+ client.query('ROLLBACK TO foobar;')
200
+ end
201
+
202
+ client.query('ROLLBACK;')
203
+ end
204
+
205
+ def test_semian_allows_release_savepoint
206
+ client = connect_to_mysql!
207
+
208
+ client.query('START TRANSACTION;')
209
+ client.query('SAVEPOINT foobar;')
210
+
211
+ Semian[:mysql_testing].acquire do
212
+ client.query('RELEASE SAVEPOINT foobar;')
213
+ end
214
+
215
+ client.query('ROLLBACK;')
216
+ end
217
+
163
218
  def test_resource_timeout_on_query
164
219
  client = connect_to_mysql!
165
220
  client2 = connect_to_mysql!
@@ -216,6 +271,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
216
271
 
217
272
  class FakeMysql < Mysql2::Client
218
273
  private
274
+
219
275
  def connect(*)
220
276
  end
221
277
  end
@@ -0,0 +1,481 @@
1
+ require 'test_helper'
2
+ require 'semian/net_http'
3
+ require 'thin'
4
+
5
+ class TestNetHTTP < MiniTest::Unit::TestCase
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 = "localhost"
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
+ DEFAULT_SEMIAN_OPTIONS.merge(name: "#{host}_#{port}")
25
+ end
26
+
27
+ def test_with_server_raises_if_binding_fails
28
+ # Occurs when trying to bind to invalid addresses, like non-private
29
+ # addresses, or when the address is already bound to something else
30
+ with_server do
31
+ assert_raises RuntimeError do
32
+ with_server {}
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_semian_identifier
38
+ with_server do
39
+ with_semian_configuration do
40
+ Net::HTTP.start(HOSTNAME, TOXIC_PORT) do |http|
41
+ assert_equal "nethttp_#{HOSTNAME}_#{TOXIC_PORT}", http.semian_identifier
42
+ end
43
+ Net::HTTP.start("127.0.0.1", TOXIC_PORT) do |http|
44
+ assert_equal "nethttp_127.0.0.1_#{TOXIC_PORT}", http.semian_identifier
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def test_trigger_open
51
+ with_semian_configuration do
52
+ with_server do
53
+ open_circuit!
54
+ uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
55
+ assert_raises Net::CircuitOpenError do
56
+ Net::HTTP.get(uri)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def test_trigger_close_after_open
63
+ with_semian_configuration do
64
+ with_server do
65
+ open_circuit!
66
+ close_circuit!
67
+ end
68
+ end
69
+ end
70
+
71
+ def test_bulkheads_tickets_are_working
72
+ options = proc do |host, port|
73
+ {
74
+ tickets: 2,
75
+ success_threshold: 1,
76
+ error_threshold: 3,
77
+ error_timeout: 10,
78
+ name: "#{host}_#{port}",
79
+ }
80
+ end
81
+ with_semian_configuration(options) do
82
+ with_server do
83
+ http_1 = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
84
+ http_1.semian_resource.acquire do
85
+ http_2 = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
86
+ http_2.semian_resource.acquire do
87
+ assert_raises Net::ResourceBusyError do
88
+ Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/"))
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def test_get_is_protected
97
+ with_semian_configuration do
98
+ with_server do
99
+ open_circuit!
100
+ assert_raises Net::CircuitOpenError do
101
+ Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200"))
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def test_instance_get_is_protected
108
+ with_semian_configuration do
109
+ with_server do
110
+ open_circuit!
111
+ assert_raises Net::CircuitOpenError do
112
+ http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
113
+ http.get("/")
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def test_get_response_is_protected
120
+ with_semian_configuration do
121
+ with_server do
122
+ open_circuit!
123
+ assert_raises Net::CircuitOpenError do
124
+ uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
125
+ Net::HTTP.get_response(uri)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def test_post_form_is_protected
132
+ with_semian_configuration do
133
+ with_server do
134
+ open_circuit!
135
+ assert_raises Net::CircuitOpenError do
136
+ uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
137
+ Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def test_http_start_method_is_protected
144
+ with_semian_configuration do
145
+ with_server do
146
+ open_circuit!
147
+ uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
148
+ assert_raises Net::CircuitOpenError do
149
+ Net::HTTP.start(uri.host, uri.port) {}
150
+ end
151
+ close_circuit!
152
+ end
153
+ end
154
+ end
155
+
156
+ def test_http_action_request_inside_start_methods_are_protected
157
+ with_semian_configuration do
158
+ with_server do
159
+ uri = URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200")
160
+ Net::HTTP.start(uri.host, uri.port) do |http|
161
+ open_circuit!
162
+ get_subclasses(Net::HTTPRequest).each do |action|
163
+ assert_raises(Net::CircuitOpenError, "#{action.name} did not raise a Net::CircuitOpenError") do
164
+ request = action.new uri
165
+ http.request(request)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ def test_custom_raw_semian_options_work_with_lookup
174
+ with_server do
175
+ semian_config = {}
176
+ semian_config["development"] = {}
177
+ semian_config["development"]["nethttp_#{HOSTNAME}_#{TOXIC_PORT}"] = DEFAULT_SEMIAN_OPTIONS
178
+ sample_env = "development"
179
+
180
+ semian_configuration_proc = proc do |host, port|
181
+ semian_identifier = "nethttp_#{host}_#{port}"
182
+ semian_config[sample_env][semian_identifier].merge(name: "#{host}_#{port}")
183
+ end
184
+
185
+ with_semian_configuration(semian_configuration_proc) do
186
+ Net::HTTP.start(HOSTNAME, TOXIC_PORT) do |http|
187
+ assert_equal semian_config["development"][http.semian_identifier],
188
+ http.raw_semian_options.dup.tap { |o| o.delete(:name) }
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def test_custom_raw_semian_options_work_with_default_fallback
195
+ with_server do
196
+ semian_config = {}
197
+ semian_config["development"] = {}
198
+ semian_config["development"]["nethttp_default"] = DEFAULT_SEMIAN_OPTIONS
199
+ sample_env = "development"
200
+
201
+ semian_configuration_proc = proc do |host, port|
202
+ semian_identifier = "nethttp_#{host}_#{port}"
203
+ semian_identifier = "nethttp_default" unless semian_config[sample_env].key?(semian_identifier)
204
+ semian_config[sample_env][semian_identifier].merge(name: "default")
205
+ end
206
+ Semian["nethttp_default"].reset if Semian["nethttp_default"]
207
+ Semian.destroy("nethttp_default")
208
+ with_semian_configuration(semian_configuration_proc) do
209
+ Net::HTTP.start(HOSTNAME, PORT) do |http|
210
+ expected_config = semian_config["development"]["nethttp_default"].dup
211
+ assert_equal expected_config, http.raw_semian_options.dup.tap { |o| o.delete(:name) }
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ def test_custom_raw_semian_options_can_disable_using_nil
218
+ with_server do
219
+ semian_configuration_proc = proc { nil }
220
+ with_semian_configuration(semian_configuration_proc) do
221
+ http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
222
+ assert_equal true, http.disabled?
223
+ end
224
+ end
225
+ end
226
+
227
+ def test_use_custom_configuration_to_combine_endpoints_into_one_resource
228
+ semian_config = {}
229
+ semian_config["development"] = {}
230
+ semian_config["development"]["nethttp_default"] = DEFAULT_SEMIAN_OPTIONS
231
+ sample_env = "development"
232
+
233
+ semian_configuration_proc = proc do
234
+ semian_identifier = "nethttp_default"
235
+ semian_config[sample_env][semian_identifier].merge(name: "default")
236
+ end
237
+
238
+ with_semian_configuration(semian_configuration_proc) do
239
+ Semian["nethttp_default"].reset if Semian["nethttp_default"]
240
+ Semian.destroy("nethttp_default")
241
+ with_server do
242
+ open_circuit!
243
+ end
244
+ with_server(addresses: ["#{HOSTNAME}:#{PORT}", "#{HOSTNAME}:#{PORT + 100}"], reset_semian_state: false) do
245
+ assert_raises Net::CircuitOpenError do
246
+ Net::HTTP.get(URI("http://#{HOSTNAME}:#{TOXIC_PORT}/200"))
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def test_custom_raw_semian_options_can_disable_with_invalid_key
253
+ with_server do
254
+ semian_config = {}
255
+ semian_config["development"] = {}
256
+ semian_config["development"]["nethttp_#{HOSTNAME}_#{TOXIC_PORT}"] = DEFAULT_SEMIAN_OPTIONS
257
+ sample_env = "development"
258
+
259
+ semian_configuration_proc = proc do |host, port|
260
+ semian_identifier = "nethttp_#{host}_#{port}"
261
+ semian_config[sample_env][semian_identifier]
262
+ end
263
+ with_semian_configuration(semian_configuration_proc) do
264
+ http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
265
+ assert_equal false, http.disabled?
266
+
267
+ http = Net::HTTP.new(HOSTNAME, TOXIC_PORT + 100)
268
+ assert_equal true, http.disabled?
269
+ end
270
+ end
271
+ end
272
+
273
+ def test_adding_extra_errors_and_resetting_affects_exceptions_list
274
+ orig_errors = Semian::NetHTTP.exceptions.dup
275
+ Semian::NetHTTP.exceptions += [::OpenSSL::SSL::SSLError]
276
+ assert_equal(orig_errors + [::OpenSSL::SSL::SSLError], Semian::NetHTTP.exceptions)
277
+ Semian::NetHTTP.reset_exceptions
278
+ assert_equal(Semian::NetHTTP::DEFAULT_ERRORS, Semian::NetHTTP.exceptions)
279
+ ensure
280
+ Semian::NetHTTP.exceptions = orig_errors
281
+ end
282
+
283
+ def test_adding_custom_errors_do_trip_circuit
284
+ with_semian_configuration do
285
+ with_custom_errors([::OpenSSL::SSL::SSLError]) do
286
+ with_server do
287
+ http = Net::HTTP.new(HOSTNAME, TOXIC_PORT)
288
+ http.use_ssl = true
289
+ http.raw_semian_options[:error_threshold].times do
290
+ assert_raises ::OpenSSL::SSL::SSLError do
291
+ http.get("/200")
292
+ end
293
+ end
294
+ assert_raises Net::CircuitOpenError do
295
+ http.get("/200")
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ def test_multiple_different_endpoints_and_ports_are_tracked_differently
303
+ with_semian_configuration do
304
+ addresses = ["#{HOSTNAME}:#{PORT}", "#{HOSTNAME}:#{PORT + 100}"]
305
+ addresses.each do |address|
306
+ hostname, port = address.split(":")
307
+ port = port.to_i
308
+ reset_semian_resource(hostname: hostname, port: port)
309
+ end
310
+ with_server(addresses: addresses, reset_semian_state: false) do |hostname, port|
311
+ with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |name|
312
+ Net::HTTP.get(URI("http://#{hostname}:#{port + 1}/"))
313
+ open_circuit!(hostname: hostname, toxic_port: port + 1, toxic_name: name)
314
+ assert_raises Net::CircuitOpenError do
315
+ Net::HTTP.get(URI("http://#{hostname}:#{port + 1}/"))
316
+ end
317
+ end
318
+ end
319
+ with_server(addresses: ["127.0.0.1:#{PORT}"], reset_semian_state: false) do
320
+ # different endpoint, should not raise errors even though localhost == 127.0.0.1
321
+ Net::HTTP.get(URI("http://127.0.0.1:#{PORT + 1}/"))
322
+ end
323
+ end
324
+ end
325
+
326
+ def test_persistent_state_after_server_restart
327
+ with_semian_configuration do
328
+ with_server(addresses: ["#{HOSTNAME}:#{PORT + 100}"]) do |hostname, port|
329
+ with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |name|
330
+ open_circuit!(hostname: hostname, toxic_port: port + 1, toxic_name: name)
331
+ end
332
+ end
333
+ with_server(addresses: ["#{HOSTNAME}:#{PORT + 100}"], reset_semian_state: false) do |hostname, port|
334
+ with_toxic(hostname: hostname, upstream_port: port, toxic_port: port + 1) do |_|
335
+ assert_raises Net::CircuitOpenError do
336
+ Net::HTTP.get(URI("http://localhost:#{port + 1}/200"))
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+
343
+ private
344
+
345
+ def with_semian_configuration(options = DEFAULT_SEMIAN_CONFIGURATION)
346
+ orig_semian_options = Semian::NetHTTP.semian_configuration
347
+ Semian::NetHTTP.semian_configuration = options
348
+ yield
349
+ ensure
350
+ Semian::NetHTTP.semian_configuration = orig_semian_options
351
+ end
352
+
353
+ def with_custom_errors(errors)
354
+ orig_errors = Semian::NetHTTP.exceptions.dup
355
+ Semian::NetHTTP.exceptions += errors
356
+ yield
357
+ ensure
358
+ Semian::NetHTTP.exceptions = orig_errors
359
+ end
360
+
361
+ def get_subclasses(klass)
362
+ ObjectSpace.each_object(klass.singleton_class).to_a - [klass]
363
+ end
364
+
365
+ def open_circuit!(hostname: HOSTNAME, toxic_port: TOXIC_PORT, toxic_name: "semian_test_net_http")
366
+ Net::HTTP.start(hostname, toxic_port) do |http|
367
+ http.read_timeout = 0.1
368
+ uri = URI("http://#{hostname}:#{toxic_port}/200")
369
+ http.raw_semian_options[:error_threshold].times do
370
+ # Cause error error_threshold times so circuit opens
371
+ Toxiproxy[toxic_name].downstream(:latency, latency: 150).apply do
372
+ request = Net::HTTP::Get.new(uri)
373
+ assert_raises Net::ReadTimeout do
374
+ http.request(request)
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ def close_circuit!(hostname: HOSTNAME, toxic_port: TOXIC_PORT)
382
+ http = Net::HTTP.new(hostname, toxic_port)
383
+ Timecop.travel(http.raw_semian_options[:error_timeout])
384
+ # Cause successes success_threshold times so circuit closes
385
+ http.raw_semian_options[:success_threshold].times do
386
+ response = http.get("/200")
387
+ assert(200, response.code)
388
+ end
389
+ end
390
+
391
+ def with_server(addresses: ["#{HOSTNAME}:#{PORT}"], reset_semian_state: true)
392
+ addresses.each do |address|
393
+ hostname, port = address.split(":")
394
+ begin
395
+ server = nil
396
+ server_threw_error = false
397
+ server_thread = Thread.new do
398
+ Thin::Logging.silent = true
399
+ server = Thin::Server.new(hostname, port, RackServer)
400
+ begin
401
+ server.start
402
+ rescue StandardError
403
+ server_threw_error = true
404
+ raise
405
+ end
406
+ end
407
+
408
+ begin
409
+ poll_until_ready(hostname: hostname, port: port)
410
+ rescue RuntimeError
411
+ server_thread.kill
412
+ server_thread.join if server_threw_error
413
+ raise
414
+ end
415
+
416
+ assert(server.running?)
417
+ reset_semian_resource(hostname: hostname, port: port) if reset_semian_state
418
+ @proxy = Toxiproxy[:semian_test_net_http]
419
+ yield(hostname, port.to_i)
420
+ ensure
421
+ server_thread.kill
422
+ poll_until_gone(hostname: hostname, port: port)
423
+ end
424
+ end
425
+ end
426
+
427
+ def reset_semian_resource(hostname:, port:)
428
+ Semian["nethttp_#{hostname}_#{port}"].reset if Semian["nethttp_#{hostname}_#{port}"]
429
+ Semian["nethttp_#{hostname}_#{port.to_i + 1}"].reset if Semian["nethttp_#{hostname}_#{port.to_i + 1}"]
430
+ Semian.destroy("nethttp_#{hostname}_#{port}")
431
+ Semian.destroy("nethttp_#{hostname}_#{port.to_i + 1}")
432
+ end
433
+
434
+ def with_toxic(hostname: HOSTNAME, upstream_port: PORT, toxic_port: upstream_port + 1)
435
+ old_proxy = @proxy
436
+ name = "semian_test_net_http_#{hostname}_#{upstream_port}<-#{toxic_port}"
437
+ Toxiproxy.populate([
438
+ {
439
+ name: name,
440
+ upstream: "#{hostname}:#{upstream_port}",
441
+ listen: "#{hostname}:#{toxic_port}",
442
+ },
443
+ ])
444
+ @proxy = Toxiproxy[name]
445
+ yield(name)
446
+ rescue StandardError
447
+ ensure
448
+ @proxy = old_proxy
449
+ begin
450
+ Toxiproxy[name].destroy
451
+ rescue StandardError
452
+ end
453
+ end
454
+
455
+ def poll_until_ready(hostname: HOSTNAME, port: PORT, time_to_wait: 1)
456
+ start_time = Time.now.to_i
457
+ begin
458
+ TCPSocket.new(hostname, port).close
459
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET
460
+ if Time.now.to_i > start_time + time_to_wait
461
+ raise "Couldn't reach the service on hostname #{hostname} port #{port} after #{time_to_wait}s"
462
+ else
463
+ retry
464
+ end
465
+ end
466
+ end
467
+
468
+ def poll_until_gone(hostname: HOSTNAME, port: PORT, time_to_wait: 1)
469
+ start_time = Time.now.to_i
470
+ loop do
471
+ if Time.now.to_i > start_time + time_to_wait
472
+ raise "Could still reach the service on hostname #{hostname} port #{port} after #{time_to_wait}s"
473
+ end
474
+ begin
475
+ TCPSocket.new(hostname, port).close
476
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET
477
+ return true
478
+ end
479
+ end
480
+ end
481
+ end