semian 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|