semian 0.1.0 → 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Rakefile +3 -7
- data/lib/semian.rb +14 -2
- data/lib/semian/adapter.rb +56 -0
- data/lib/semian/circuit_breaker.rb +16 -20
- data/lib/semian/mysql2.rb +13 -29
- data/lib/semian/protected_resource.rb +4 -4
- data/lib/semian/redis.rb +55 -0
- data/lib/semian/unprotected_resource.rb +33 -0
- data/lib/semian/version.rb +1 -1
- data/semian.gemspec +1 -0
- data/test/{test_circuit_breaker.rb → circuit_breaker_test.rb} +40 -13
- data/test/helpers/background_helper.rb +25 -0
- data/test/{test_instrumentation.rb → instrumentation_test.rb} +1 -2
- data/test/{test_mysql2.rb → mysql2_test.rb} +17 -29
- data/test/redis_test.rb +167 -0
- data/test/{test_resource.rb → resource_test.rb} +1 -4
- data/test/test_helper.rb +28 -0
- data/test/unprotected_resource_test.rb +48 -0
- data/test/{test_unsupported.rb → unsupported_test.rb} +1 -2
- metadata +28 -8
- metadata.gz.sig +0 -0
- data/test/fixtures/toxiproxy.json +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1da1ae8c9c9376d81347eae49e8755aee743acf1
|
4
|
+
data.tar.gz: dc3b57ca4747df54e05b072c3df52c7e731088a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 880796b7330efc3566fbe66a1d1756f99617cc496640ba58d4277e83f401ea65ed5619ed60869ef67049c91d87f620b28f04fdd0015ceb09e5ac4306a33d2a73
|
7
|
+
data.tar.gz: 179a799c1b5d3e07abeb550ee940c527da01781ceceb6c1fd04dd2d42ca5971bde46a886449d4a1739da18944a25323aba593a064526f9ad464a51cdc4f19527
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Rakefile
CHANGED
@@ -29,20 +29,16 @@ else
|
|
29
29
|
task :build do; end
|
30
30
|
end
|
31
31
|
|
32
|
-
task :populate_proxy do
|
33
|
-
require 'toxiproxy'
|
34
|
-
Toxiproxy.populate(File.expand_path('../test/fixtures/toxiproxy.json', __FILE__))
|
35
|
-
end
|
36
|
-
|
37
32
|
# ==========================================================
|
38
33
|
# Testing
|
39
34
|
# ==========================================================
|
40
35
|
|
41
36
|
require 'rake/testtask'
|
42
37
|
Rake::TestTask.new 'test' do |t|
|
43
|
-
t.
|
38
|
+
t.libs = ['lib', 'test']
|
39
|
+
t.pattern = "test/*_test.rb"
|
44
40
|
end
|
45
|
-
task :test =>
|
41
|
+
task :test => :build
|
46
42
|
|
47
43
|
# ==========================================================
|
48
44
|
# Documentation
|
data/lib/semian.rb
CHANGED
@@ -77,9 +77,20 @@ module Semian
|
|
77
77
|
InternalError = Class.new(BaseError)
|
78
78
|
OpenCircuitError = Class.new(BaseError)
|
79
79
|
|
80
|
+
module AdapterError
|
81
|
+
def initialize(semian_identifier, *args)
|
82
|
+
super(*args)
|
83
|
+
@semian_identifier = semian_identifier
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
"[#{@semian_identifier}] #{super}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
80
91
|
attr_accessor :logger
|
81
92
|
|
82
|
-
self.logger = Logger.new(
|
93
|
+
self.logger = Logger.new(STDERR)
|
83
94
|
|
84
95
|
# Registers a resource.
|
85
96
|
#
|
@@ -136,11 +147,12 @@ end
|
|
136
147
|
require 'semian/resource'
|
137
148
|
require 'semian/circuit_breaker'
|
138
149
|
require 'semian/protected_resource'
|
150
|
+
require 'semian/unprotected_resource'
|
139
151
|
require 'semian/platform'
|
140
152
|
if Semian.supported_platform?
|
141
153
|
require 'semian/semian'
|
142
154
|
else
|
143
155
|
Semian::MAX_TICKETS = 0
|
144
|
-
|
156
|
+
Semian.logger.info("Semian is not supported on #{RUBY_PLATFORM} - all operations will no-op")
|
145
157
|
end
|
146
158
|
require 'semian/version'
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Semian
|
2
|
+
module Adapter
|
3
|
+
def semian_identifier
|
4
|
+
raise NotImplementedError.new("Semian adapters must implement a `semian_identifier` method")
|
5
|
+
end
|
6
|
+
|
7
|
+
def semian_resource
|
8
|
+
@semian_options ||= case semian_options
|
9
|
+
when false
|
10
|
+
UnprotectedResource.new(semian_identifier)
|
11
|
+
when nil
|
12
|
+
Semian.logger.info("Semian is not configured for #{self.class.name}: #{semian_identifier}")
|
13
|
+
UnprotectedResource.new(semian_identifier)
|
14
|
+
else
|
15
|
+
options = semian_options.dup
|
16
|
+
options.delete(:name)
|
17
|
+
::Semian.retrieve_or_register(semian_identifier, **options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def acquire_semian_resource(scope:, adapter:, &block)
|
24
|
+
return yield if resource_already_acquired?
|
25
|
+
semian_resource.acquire(scope: scope, adapter: adapter) do
|
26
|
+
mark_resource_as_acquired(&block)
|
27
|
+
end
|
28
|
+
rescue ::Semian::OpenCircuitError => error
|
29
|
+
raise self.class::CircuitOpenError.new(semian_identifier, error)
|
30
|
+
rescue ::Semian::BaseError => error
|
31
|
+
raise self.class::ResourceOccupiedError.new(semian_identifier, error)
|
32
|
+
end
|
33
|
+
|
34
|
+
def semian_options
|
35
|
+
return @semian_options if defined? @semian_options
|
36
|
+
options = raw_semian_options
|
37
|
+
@semian_options = options && options.map { |k, v| [k.to_sym, v] }.to_h
|
38
|
+
end
|
39
|
+
|
40
|
+
def raw_semian_options
|
41
|
+
raise NotImplementedError.new("Semian adapters must implement a `raw_semian_options` method")
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_already_acquired?
|
45
|
+
@resource_acquired
|
46
|
+
end
|
47
|
+
|
48
|
+
def mark_resource_as_acquired
|
49
|
+
previous = @resource_acquired
|
50
|
+
@resource_acquired = true
|
51
|
+
yield
|
52
|
+
ensure
|
53
|
+
@resource_acquired = previous
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -40,7 +40,7 @@ module Semian
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def mark_failed(error)
|
43
|
-
|
43
|
+
push_time(@errors, @error_count_threshold, duration: @error_timeout)
|
44
44
|
|
45
45
|
if closed?
|
46
46
|
open if error_threshold_reached?
|
@@ -51,14 +51,13 @@ module Semian
|
|
51
51
|
|
52
52
|
def mark_success
|
53
53
|
return unless half_open?
|
54
|
-
@
|
54
|
+
@successes += 1
|
55
55
|
close if success_threshold_reached?
|
56
56
|
end
|
57
57
|
|
58
58
|
def reset
|
59
|
-
@
|
60
|
-
@
|
61
|
-
@error_last_at = nil
|
59
|
+
@errors = []
|
60
|
+
@successes = 0
|
62
61
|
close
|
63
62
|
end
|
64
63
|
|
@@ -79,7 +78,7 @@ module Semian
|
|
79
78
|
def close
|
80
79
|
log_state_transition(:closed)
|
81
80
|
@state = :closed
|
82
|
-
@
|
81
|
+
@errors = []
|
83
82
|
end
|
84
83
|
|
85
84
|
def open?
|
@@ -98,35 +97,32 @@ module Semian
|
|
98
97
|
def half_open
|
99
98
|
log_state_transition(:half_open)
|
100
99
|
@state = :half_open
|
101
|
-
@
|
102
|
-
end
|
103
|
-
|
104
|
-
def increment_recent_errors
|
105
|
-
if error_timeout_expired?
|
106
|
-
@error_count = 0
|
107
|
-
end
|
108
|
-
|
109
|
-
@error_count += 1
|
110
|
-
@error_last_at = Time.now
|
100
|
+
@successes = 0
|
111
101
|
end
|
112
102
|
|
113
103
|
def success_threshold_reached?
|
114
|
-
@
|
104
|
+
@successes >= @success_count_threshold
|
115
105
|
end
|
116
106
|
|
117
107
|
def error_threshold_reached?
|
118
|
-
@
|
108
|
+
@errors.count == @error_count_threshold
|
119
109
|
end
|
120
110
|
|
121
111
|
def error_timeout_expired?
|
122
|
-
@
|
112
|
+
@errors.last && (@errors.last + @error_timeout < Time.now)
|
113
|
+
end
|
114
|
+
|
115
|
+
def push_time(window, max_size, duration:, time: Time.now)
|
116
|
+
window.shift while window.first && window.first + duration < time
|
117
|
+
window.shift if window.size == max_size
|
118
|
+
window << time
|
123
119
|
end
|
124
120
|
|
125
121
|
def log_state_transition(new_state)
|
126
122
|
return if @state.nil? || new_state == @state
|
127
123
|
|
128
124
|
str = "[#{self.class.name}] State transition from #{@state} to #{new_state}."
|
129
|
-
str << " success_count=#{@
|
125
|
+
str << " success_count=#{@successes} error_count=#{@errors.count}"
|
130
126
|
str << " success_count_threshold=#{@success_count_threshold} error_count_threshold=#{@error_count_threshold}"
|
131
127
|
str << " error_timeout=#{@error_timeout} error_last_at=\"#{@error_last_at}\""
|
132
128
|
Semian.logger.info(str)
|
data/lib/semian/mysql2.rb
CHANGED
@@ -1,16 +1,10 @@
|
|
1
1
|
require 'semian'
|
2
|
+
require 'semian/adapter'
|
2
3
|
require 'mysql2'
|
3
4
|
|
4
5
|
module Mysql2
|
5
6
|
class SemianError < Mysql2::Error
|
6
|
-
|
7
|
-
super(*args)
|
8
|
-
@semian_identifier = semian_identifier
|
9
|
-
end
|
10
|
-
|
11
|
-
def to_s
|
12
|
-
"[#{@semian_identifier}] #{super}"
|
13
|
-
end
|
7
|
+
include ::Semian::AdapterError
|
14
8
|
end
|
15
9
|
|
16
10
|
ResourceOccupiedError = Class.new(SemianError)
|
@@ -19,6 +13,11 @@ end
|
|
19
13
|
|
20
14
|
module Semian
|
21
15
|
module Mysql2
|
16
|
+
include Semian::Adapter
|
17
|
+
|
18
|
+
ResourceOccupiedError = ::Mysql2::ResourceOccupiedError
|
19
|
+
CircuitOpenError = ::Mysql2::CircuitOpenError
|
20
|
+
|
22
21
|
DEFAULT_HOST = 'localhost'
|
23
22
|
DEFAULT_PORT = 3306
|
24
23
|
|
@@ -33,8 +32,7 @@ module Semian
|
|
33
32
|
|
34
33
|
def semian_identifier
|
35
34
|
@semian_identifier ||= begin
|
36
|
-
|
37
|
-
unless name = semian_options['name'.freeze] || semian_options[:name]
|
35
|
+
unless name = semian_options && semian_options[:name]
|
38
36
|
host = query_options[:host] || DEFAULT_HOST
|
39
37
|
port = query_options[:port] || DEFAULT_PORT
|
40
38
|
name = "#{host}:#{port}"
|
@@ -44,32 +42,18 @@ module Semian
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def query(*args)
|
47
|
-
|
48
|
-
rescue ::Semian::OpenCircuitError => error
|
49
|
-
raise ::Mysql2::CircuitOpenError.new(semian_identifier, error)
|
50
|
-
rescue ::Semian::BaseError => error
|
51
|
-
raise ::Mysql2::ResourceOccupiedError.new(semian_identifier, error)
|
45
|
+
acquire_semian_resource(adapter: :mysql, scope: :query) { raw_query(*args) }
|
52
46
|
end
|
53
47
|
|
54
48
|
private
|
55
49
|
|
56
50
|
def connect(*args)
|
57
|
-
|
58
|
-
rescue ::Semian::OpenCircuitError => error
|
59
|
-
raise ::Mysql2::CircuitOpenError.new(semian_identifier, error)
|
60
|
-
rescue ::Semian::BaseError => error
|
61
|
-
raise ::Mysql2::ResourceOccupiedError.new(semian_identifier, error)
|
62
|
-
end
|
63
|
-
|
64
|
-
def semian_resource
|
65
|
-
@semian_resource ||= ::Semian.retrieve_or_register(semian_identifier, **semian_options)
|
51
|
+
acquire_semian_resource(adapter: :mysql, scope: :connection) { raw_connect(*args) }
|
66
52
|
end
|
67
53
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
options.delete(:name)
|
72
|
-
options
|
54
|
+
def raw_semian_options
|
55
|
+
return query_options[:semian] if query_options.key?(:semian)
|
56
|
+
return query_options['semian'.freeze] if query_options.key?('semian'.freeze)
|
73
57
|
end
|
74
58
|
end
|
75
59
|
end
|
@@ -12,20 +12,20 @@ module Semian
|
|
12
12
|
@circuit_breaker = circuit_breaker
|
13
13
|
end
|
14
14
|
|
15
|
-
def acquire(timeout: nil, scope: nil, &block)
|
15
|
+
def acquire(timeout: nil, scope: nil, adapter: nil, &block)
|
16
16
|
@circuit_breaker.acquire do
|
17
17
|
begin
|
18
18
|
@resource.acquire(timeout: timeout) do
|
19
|
-
Semian.notify(:success, self, scope)
|
19
|
+
Semian.notify(:success, self, scope, adapter)
|
20
20
|
yield self
|
21
21
|
end
|
22
22
|
rescue ::Semian::TimeoutError
|
23
|
-
Semian.notify(:occupied, self, scope)
|
23
|
+
Semian.notify(:occupied, self, scope, adapter)
|
24
24
|
raise
|
25
25
|
end
|
26
26
|
end
|
27
27
|
rescue ::Semian::OpenCircuitError
|
28
|
-
Semian.notify(:circuit_open, self, scope)
|
28
|
+
Semian.notify(:circuit_open, self, scope, adapter)
|
29
29
|
raise
|
30
30
|
end
|
31
31
|
|
data/lib/semian/redis.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'semian'
|
2
|
+
require 'semian/adapter'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class SemianError < Redis::BaseConnectionError
|
7
|
+
include ::Semian::AdapterError
|
8
|
+
end
|
9
|
+
|
10
|
+
ResourceOccupiedError = Class.new(SemianError)
|
11
|
+
CircuitOpenError = Class.new(SemianError)
|
12
|
+
end
|
13
|
+
|
14
|
+
module Semian
|
15
|
+
module Redis
|
16
|
+
include Semian::Adapter
|
17
|
+
|
18
|
+
ResourceOccupiedError = ::Redis::ResourceOccupiedError
|
19
|
+
CircuitOpenError = ::Redis::CircuitOpenError
|
20
|
+
|
21
|
+
# The naked methods are exposed as `raw_query` and `raw_connect` for instrumentation purpose
|
22
|
+
def self.included(base)
|
23
|
+
base.send(:alias_method, :raw_io, :io)
|
24
|
+
base.send(:remove_method, :io)
|
25
|
+
|
26
|
+
base.send(:alias_method, :raw_connect, :connect)
|
27
|
+
base.send(:remove_method, :connect)
|
28
|
+
end
|
29
|
+
|
30
|
+
def semian_identifier
|
31
|
+
@semian_identifier ||= begin
|
32
|
+
name = semian_options && semian_options[:name]
|
33
|
+
name ||= "#{location}/#{db}"
|
34
|
+
:"redis_#{name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def io(&block)
|
39
|
+
acquire_semian_resource(adapter: :redis, scope: :query) { raw_io(&block) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def connect
|
43
|
+
acquire_semian_resource(adapter: :redis, scope: :connection) { raw_connect }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def raw_semian_options
|
49
|
+
return options[:semian] if options.key?(:semian)
|
50
|
+
return options['semian'.freeze] if options.key?('semian'.freeze)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
::Redis::Client.include(Semian::Redis)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Semian
|
2
|
+
# This class acts as a replacement for `ProtectedResource` when
|
3
|
+
# the semian configuration of an `Adatper` is missing or explicitly disabled
|
4
|
+
class UnprotectedResource
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def tickets
|
12
|
+
-1
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy
|
16
|
+
end
|
17
|
+
|
18
|
+
def acquire(*)
|
19
|
+
yield self
|
20
|
+
end
|
21
|
+
|
22
|
+
def count
|
23
|
+
0
|
24
|
+
end
|
25
|
+
|
26
|
+
def semid
|
27
|
+
0
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/semian/version.rb
CHANGED
data/semian.gemspec
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
require 'semian'
|
3
|
-
require 'timecop'
|
1
|
+
require 'test_helper'
|
4
2
|
|
5
3
|
class TestCircuitBreaker < MiniTest::Unit::TestCase
|
6
4
|
SomeError = Class.new(StandardError)
|
@@ -84,31 +82,60 @@ class TestCircuitBreaker < MiniTest::Unit::TestCase
|
|
84
82
|
assert_circuit_closed
|
85
83
|
end
|
86
84
|
|
85
|
+
def test_errors_more_than_duration_apart_doesnt_open_circuit
|
86
|
+
Timecop.travel(Time.now - 6) do
|
87
|
+
trigger_error!
|
88
|
+
assert_circuit_closed
|
89
|
+
end
|
90
|
+
|
91
|
+
trigger_error!
|
92
|
+
assert_circuit_closed
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_sparse_errors_dont_open_circuit
|
96
|
+
resource = Semian.register(:three, tickets: 1, exceptions: [SomeError], error_threshold: 3, error_timeout: 5, success_threshold: 1)
|
97
|
+
|
98
|
+
Timecop.travel(-6) do
|
99
|
+
trigger_error!(resource)
|
100
|
+
assert_circuit_closed(resource)
|
101
|
+
end
|
102
|
+
|
103
|
+
Timecop.travel(-1) do
|
104
|
+
trigger_error!(resource)
|
105
|
+
assert_circuit_closed(resource)
|
106
|
+
end
|
107
|
+
|
108
|
+
trigger_error!(resource)
|
109
|
+
assert_circuit_closed(resource)
|
110
|
+
ensure
|
111
|
+
Semian.destroy(:three)
|
112
|
+
end
|
113
|
+
|
87
114
|
private
|
88
115
|
|
89
|
-
def open_circuit!
|
90
|
-
2.times { trigger_error! }
|
116
|
+
def open_circuit!(resource = @resource)
|
117
|
+
2.times { trigger_error!(resource) }
|
91
118
|
end
|
92
119
|
|
93
|
-
def half_open_cicuit!
|
120
|
+
def half_open_cicuit!(resource = @resource)
|
94
121
|
Timecop.travel(Time.now - 10) do
|
95
|
-
open_circuit!
|
122
|
+
open_circuit!(resource)
|
96
123
|
end
|
97
124
|
end
|
98
125
|
|
99
|
-
def trigger_error!
|
100
|
-
|
126
|
+
def trigger_error!(resource = @resource)
|
127
|
+
resource.with_fallback(42) { raise SomeError }
|
101
128
|
end
|
102
129
|
|
103
|
-
def assert_circuit_closed
|
130
|
+
def assert_circuit_closed(resource = @resource)
|
104
131
|
block_called = false
|
105
|
-
|
132
|
+
resource.with_fallback(42) { block_called = true }
|
106
133
|
assert block_called, 'Expected the circuit to be closed, but it was open'
|
107
134
|
end
|
108
135
|
|
109
|
-
def assert_circuit_opened
|
136
|
+
def assert_circuit_opened(resource = @resource)
|
110
137
|
block_called = false
|
111
|
-
|
138
|
+
resource.with_fallback(42) { block_called = true }
|
112
139
|
refute block_called, 'Expected the circuit to be open, but it was closed'
|
113
140
|
end
|
114
141
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module BackgroundHelper
|
2
|
+
attr_writer :threads
|
3
|
+
|
4
|
+
def teardown
|
5
|
+
threads.each { |t| t.kill }
|
6
|
+
self.threads = []
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def background(&block)
|
12
|
+
thread = Thread.new(&block)
|
13
|
+
threads << thread
|
14
|
+
thread.join(0.1)
|
15
|
+
thread
|
16
|
+
end
|
17
|
+
|
18
|
+
def threads
|
19
|
+
@threads ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def yield_to_background
|
23
|
+
threads.each(&:join)
|
24
|
+
end
|
25
|
+
end
|
@@ -1,7 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
require 'semian/mysql2'
|
3
|
-
require 'toxiproxy'
|
4
|
-
require 'timecop'
|
1
|
+
require 'test_helper'
|
5
2
|
|
6
3
|
class TestMysql2 < MiniTest::Unit::TestCase
|
7
4
|
ERROR_TIMEOUT = 5
|
@@ -15,17 +12,11 @@ class TestMysql2 < MiniTest::Unit::TestCase
|
|
15
12
|
error_timeout: ERROR_TIMEOUT,
|
16
13
|
}
|
17
14
|
|
18
|
-
attr_writer :threads
|
19
15
|
def setup
|
20
16
|
@proxy = Toxiproxy[:semian_test_mysql]
|
21
17
|
Semian.destroy(:mysql_testing)
|
22
18
|
end
|
23
19
|
|
24
|
-
def teardown
|
25
|
-
threads.each { |t| t.kill }
|
26
|
-
self.threads = []
|
27
|
-
end
|
28
|
-
|
29
20
|
def test_semian_identifier
|
30
21
|
assert_equal :mysql_foo, FakeMysql.new(semian: {name: 'foo'}).semian_identifier
|
31
22
|
assert_equal :'mysql_localhost:3306', FakeMysql.new.semian_identifier
|
@@ -33,18 +24,24 @@ class TestMysql2 < MiniTest::Unit::TestCase
|
|
33
24
|
assert_equal :'mysql_example.com:42', FakeMysql.new(host: 'example.com', port: 42).semian_identifier
|
34
25
|
end
|
35
26
|
|
27
|
+
def test_semian_can_be_disabled
|
28
|
+
resource = Mysql2::Client.new(semian: false).semian_resource
|
29
|
+
assert_instance_of Semian::UnprotectedResource, resource
|
30
|
+
end
|
31
|
+
|
36
32
|
def test_connect_instrumentation
|
37
33
|
notified = false
|
38
|
-
subscriber = Semian.subscribe do |event, resource, scope|
|
34
|
+
subscriber = Semian.subscribe do |event, resource, scope, adapter|
|
39
35
|
notified = true
|
40
36
|
assert_equal :success, event
|
41
37
|
assert_equal Semian[:mysql_testing], resource
|
42
|
-
assert_equal :
|
38
|
+
assert_equal :connection, scope
|
39
|
+
assert_equal :mysql, adapter
|
43
40
|
end
|
44
41
|
|
45
42
|
connect_to_mysql!
|
46
43
|
|
47
|
-
assert notified, 'No
|
44
|
+
assert notified, 'No notifications has been emitted'
|
48
45
|
ensure
|
49
46
|
Semian.unsubscribe(subscriber)
|
50
47
|
end
|
@@ -95,16 +92,17 @@ class TestMysql2 < MiniTest::Unit::TestCase
|
|
95
92
|
client = connect_to_mysql!
|
96
93
|
|
97
94
|
notified = false
|
98
|
-
subscriber = Semian.subscribe do |event, resource, scope|
|
95
|
+
subscriber = Semian.subscribe do |event, resource, scope, adapter|
|
99
96
|
notified = true
|
100
97
|
assert_equal :success, event
|
101
98
|
assert_equal Semian[:mysql_testing], resource
|
102
99
|
assert_equal :query, scope
|
100
|
+
assert_equal :mysql, adapter
|
103
101
|
end
|
104
102
|
|
105
103
|
client.query('SELECT 1 + 1;')
|
106
104
|
|
107
|
-
assert notified, 'No
|
105
|
+
assert notified, 'No notifications has been emitted'
|
108
106
|
ensure
|
109
107
|
Semian.unsubscribe(subscriber)
|
110
108
|
end
|
@@ -157,22 +155,12 @@ class TestMysql2 < MiniTest::Unit::TestCase
|
|
157
155
|
end
|
158
156
|
end
|
159
157
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
thread = Thread.new(&block)
|
164
|
-
threads << thread
|
165
|
-
thread.join(0.1)
|
166
|
-
thread
|
167
|
-
end
|
168
|
-
|
169
|
-
def threads
|
170
|
-
@threads ||= []
|
158
|
+
def test_unconfigured
|
159
|
+
client = Mysql2::Client.new(host: '127.0.0.1', port: '13306')
|
160
|
+
assert_equal 2, client.query('SELECT 1 + 1 as sum;').to_a.first['sum']
|
171
161
|
end
|
172
162
|
|
173
|
-
|
174
|
-
threads.each(&:join)
|
175
|
-
end
|
163
|
+
private
|
176
164
|
|
177
165
|
def connect_to_mysql!(semian_options = {})
|
178
166
|
Mysql2::Client.new(host: '127.0.0.1', port: '13306', semian: SEMIAN_OPTIONS.merge(semian_options))
|
data/test/redis_test.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestRedis < MiniTest::Unit::TestCase
|
4
|
+
ERROR_TIMEOUT = 5
|
5
|
+
ERROR_THRESHOLD = 1
|
6
|
+
SEMIAN_OPTIONS = {
|
7
|
+
name: :testing,
|
8
|
+
tickets: 1,
|
9
|
+
timeout: 0,
|
10
|
+
error_threshold: ERROR_THRESHOLD,
|
11
|
+
success_threshold: 2,
|
12
|
+
error_timeout: ERROR_TIMEOUT,
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_writer :threads
|
16
|
+
def setup
|
17
|
+
@proxy = Toxiproxy[:semian_test_redis]
|
18
|
+
Semian.destroy(:redis_testing)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_semian_identifier
|
22
|
+
assert_equal :redis_foo, Redis.new(semian: {name: 'foo'}).client.semian_identifier
|
23
|
+
assert_equal :'redis_127.0.0.1:6379/0', Redis.new.client.semian_identifier
|
24
|
+
assert_equal :'redis_example.com:42/0', Redis.new(host: 'example.com', port: 42).client.semian_identifier
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_semian_can_be_disabled
|
28
|
+
resource = Redis.new(semian: false).client.semian_resource
|
29
|
+
assert_instance_of Semian::UnprotectedResource, resource
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_connect_instrumentation
|
33
|
+
notified = false
|
34
|
+
subscriber = Semian.subscribe do |event, resource, scope, adapter|
|
35
|
+
notified = true
|
36
|
+
assert_equal :success, event
|
37
|
+
assert_equal Semian[:redis_testing], resource
|
38
|
+
assert_equal :connection, scope
|
39
|
+
assert_equal :redis, adapter
|
40
|
+
end
|
41
|
+
|
42
|
+
connect_to_redis!
|
43
|
+
|
44
|
+
assert notified, 'No notifications has been emitted'
|
45
|
+
ensure
|
46
|
+
Semian.unsubscribe(subscriber)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_resource_acquisition_for_connect
|
50
|
+
client = connect_to_redis!
|
51
|
+
|
52
|
+
Semian[:redis_testing].acquire do
|
53
|
+
assert_raises Redis::ResourceOccupiedError do
|
54
|
+
connect_to_redis!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_resource_timeout_on_connect
|
60
|
+
@proxy.downstream(:latency, latency: 500).apply do
|
61
|
+
background { connect_to_redis! }
|
62
|
+
|
63
|
+
assert_raises Redis::ResourceOccupiedError do
|
64
|
+
connect_to_redis!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_circuit_breaker_on_connect
|
70
|
+
@proxy.downstream(:latency, latency: 500).apply do
|
71
|
+
background { connect_to_redis! }
|
72
|
+
|
73
|
+
ERROR_THRESHOLD.times do
|
74
|
+
assert_raises Redis::ResourceOccupiedError do
|
75
|
+
connect_to_redis!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
yield_to_background
|
81
|
+
|
82
|
+
assert_raises Redis::CircuitOpenError do
|
83
|
+
connect_to_redis!
|
84
|
+
end
|
85
|
+
|
86
|
+
Timecop.travel(ERROR_TIMEOUT + 1) do
|
87
|
+
connect_to_redis!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_query_instrumentation
|
92
|
+
client = connect_to_redis!
|
93
|
+
|
94
|
+
notified = false
|
95
|
+
subscriber = Semian.subscribe do |event, resource, scope, adapter|
|
96
|
+
notified = true
|
97
|
+
assert_equal :success, event
|
98
|
+
assert_equal Semian[:redis_testing], resource
|
99
|
+
assert_equal :query, scope
|
100
|
+
assert_equal :redis, adapter
|
101
|
+
end
|
102
|
+
|
103
|
+
client.get('foo')
|
104
|
+
|
105
|
+
assert notified, 'No notifications has been emitted'
|
106
|
+
ensure
|
107
|
+
Semian.unsubscribe(subscriber)
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_resource_acquisition_for_query
|
111
|
+
client = connect_to_redis!
|
112
|
+
|
113
|
+
Semian[:redis_testing].acquire do
|
114
|
+
assert_raises Redis::ResourceOccupiedError do
|
115
|
+
client.get('foo')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_resource_timeout_on_query
|
121
|
+
client = connect_to_redis!
|
122
|
+
client2 = connect_to_redis!
|
123
|
+
|
124
|
+
@proxy.downstream(:latency, latency: 500).apply do
|
125
|
+
background { client2.get('foo') }
|
126
|
+
|
127
|
+
assert_raises Redis::ResourceOccupiedError do
|
128
|
+
client.get('foo')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_circuit_breaker_on_query
|
134
|
+
client = connect_to_redis!
|
135
|
+
client2 = connect_to_redis!
|
136
|
+
|
137
|
+
client.set('foo', 2)
|
138
|
+
|
139
|
+
@proxy.downstream(:latency, latency: 1000).apply do
|
140
|
+
background { client2.get('foo') }
|
141
|
+
|
142
|
+
ERROR_THRESHOLD.times do
|
143
|
+
assert_raises Redis::ResourceOccupiedError do
|
144
|
+
client.get('foo')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
yield_to_background
|
150
|
+
|
151
|
+
assert_raises Redis::CircuitOpenError do
|
152
|
+
client.get('foo')
|
153
|
+
end
|
154
|
+
|
155
|
+
Timecop.travel(ERROR_TIMEOUT + 1) do
|
156
|
+
assert_equal '2', client.get('foo')
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def connect_to_redis!(semian_options = {})
|
163
|
+
redis = Redis.new(host: '127.0.0.1', port: 16379, reconnect_attempts: 0, db: 1, semian: SEMIAN_OPTIONS.merge(semian_options))
|
164
|
+
redis.client.connect
|
165
|
+
redis
|
166
|
+
end
|
167
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'semian'
|
3
|
+
require 'semian/mysql2'
|
4
|
+
require 'semian/redis'
|
5
|
+
require 'toxiproxy'
|
6
|
+
require 'timecop'
|
7
|
+
require 'tempfile'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
require 'helpers/background_helper'
|
11
|
+
|
12
|
+
Semian.logger = Logger.new(nil)
|
13
|
+
Toxiproxy.populate([
|
14
|
+
{
|
15
|
+
name: 'semian_test_mysql',
|
16
|
+
upstream: 'localhost:3306',
|
17
|
+
listen: 'localhost:13306',
|
18
|
+
},
|
19
|
+
{
|
20
|
+
name: 'semian_test_redis',
|
21
|
+
upstream: 'localhost:6379',
|
22
|
+
listen: 'localhost:16379',
|
23
|
+
},
|
24
|
+
])
|
25
|
+
|
26
|
+
class MiniTest::Unit::TestCase
|
27
|
+
include BackgroundHelper
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class UnprotectedResourceTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@resource = Semian::UnprotectedResource.new(:foo)
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_resource_name
|
9
|
+
assert_equal :foo, @resource.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_resource_tickets
|
13
|
+
assert_equal -1, @resource.tickets
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_resource_count
|
17
|
+
assert_equal 0, @resource.count
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_resource_semid
|
21
|
+
assert_equal 0, @resource.semid
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_resource_reset
|
25
|
+
@resource.reset
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_resource_destroy
|
29
|
+
@resource.destroy
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_resource_acquire
|
33
|
+
acquired = false
|
34
|
+
@resource.acquire do
|
35
|
+
acquired = true
|
36
|
+
end
|
37
|
+
assert acquired
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_resource_acquire_with_timeout
|
41
|
+
acquired = false
|
42
|
+
@resource.acquire(timeout: 2) do
|
43
|
+
acquired = true
|
44
|
+
end
|
45
|
+
assert acquired
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
@@ -31,7 +31,7 @@ cert_chain:
|
|
31
31
|
fl3hbtVFTqbOlwL9vy1fudXcolIE/ZTcxQ+er07ZFZdKCXayR9PPs64heamfn0fp
|
32
32
|
TConQSX2BnZdhIEYW+cKzEC/bLc=
|
33
33
|
-----END CERTIFICATE-----
|
34
|
-
date: 2015-02-
|
34
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
35
35
|
dependencies:
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake-compiler
|
@@ -75,6 +75,20 @@ dependencies:
|
|
75
75
|
- - ">="
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: redis
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
type: :development
|
86
|
+
prerelease: false
|
87
|
+
version_requirements: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
78
92
|
- !ruby/object:Gem::Dependency
|
79
93
|
name: toxiproxy
|
80
94
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,21 +121,27 @@ files:
|
|
107
121
|
- ext/semian/extconf.rb
|
108
122
|
- ext/semian/semian.c
|
109
123
|
- lib/semian.rb
|
124
|
+
- lib/semian/adapter.rb
|
110
125
|
- lib/semian/circuit_breaker.rb
|
111
126
|
- lib/semian/instrumentable.rb
|
112
127
|
- lib/semian/mysql2.rb
|
113
128
|
- lib/semian/platform.rb
|
114
129
|
- lib/semian/protected_resource.rb
|
130
|
+
- lib/semian/redis.rb
|
115
131
|
- lib/semian/resource.rb
|
132
|
+
- lib/semian/unprotected_resource.rb
|
116
133
|
- lib/semian/version.rb
|
117
134
|
- scripts/install_toxiproxy.sh
|
118
135
|
- semian.gemspec
|
119
|
-
- test/
|
120
|
-
- test/
|
121
|
-
- test/
|
122
|
-
- test/
|
123
|
-
- test/
|
124
|
-
- test/
|
136
|
+
- test/circuit_breaker_test.rb
|
137
|
+
- test/helpers/background_helper.rb
|
138
|
+
- test/instrumentation_test.rb
|
139
|
+
- test/mysql2_test.rb
|
140
|
+
- test/redis_test.rb
|
141
|
+
- test/resource_test.rb
|
142
|
+
- test/test_helper.rb
|
143
|
+
- test/unprotected_resource_test.rb
|
144
|
+
- test/unsupported_test.rb
|
125
145
|
homepage: https://github.com/csfrancis/semian
|
126
146
|
licenses:
|
127
147
|
- MIT
|
metadata.gz.sig
CHANGED
Binary file
|