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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 219d1e2e6ab5b6bbc6c90e0e418e47a38a6415cd
4
- data.tar.gz: dfe7b83c1f95e26d0df6daa2cde3a914e98d2520
3
+ metadata.gz: 1da1ae8c9c9376d81347eae49e8755aee743acf1
4
+ data.tar.gz: dc3b57ca4747df54e05b072c3df52c7e731088a9
5
5
  SHA512:
6
- metadata.gz: 79f643b811ba718f89a1021c2227b210e4b3045cb5ebc919e6017dcc0334d65efe31a7d30695abf1287f24894794f2c513128e6b07b52553eb0e9cac4cb11e65
7
- data.tar.gz: 267d1b19a1c8c6ab7e9786b639a0c326dc9e4669636dfb20a19f36f43386b669029b172ea3f38bf790cc173f7cef9470c1c5b488a694a2c73eb0385ae4ceaa5c
6
+ metadata.gz: 880796b7330efc3566fbe66a1d1756f99617cc496640ba58d4277e83f401ea65ed5619ed60869ef67049c91d87f620b28f04fdd0015ceb09e5ac4306a33d2a73
7
+ data.tar.gz: 179a799c1b5d3e07abeb550ee940c527da01781ceceb6c1fd04dd2d42ca5971bde46a886449d4a1739da18944a25323aba593a064526f9ad464a51cdc4f19527
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -7,3 +7,6 @@ before_install:
7
7
 
8
8
  rvm:
9
9
  - 2.1.1
10
+
11
+ services:
12
+ - redis-server
data/Gemfile CHANGED
@@ -1,2 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
+
4
+ group :debug do
5
+ gem 'byebug'
6
+ end
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.pattern = "test/test_*.rb"
38
+ t.libs = ['lib', 'test']
39
+ t.pattern = "test/*_test.rb"
44
40
  end
45
- task :test => [:build, :populate_proxy]
41
+ task :test => :build
46
42
 
47
43
  # ==========================================================
48
44
  # Documentation
@@ -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(nil)
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
- $stderr.puts "Semian is not supported on #{RUBY_PLATFORM} - all operations will no-op"
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
- increment_recent_errors
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
- @success_count += 1
54
+ @successes += 1
55
55
  close if success_threshold_reached?
56
56
  end
57
57
 
58
58
  def reset
59
- @success_count = 0
60
- @error_count = 0
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
- @error_count = 0
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
- @success_count = 0
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
- @success_count >= @success_count_threshold
104
+ @successes >= @success_count_threshold
115
105
  end
116
106
 
117
107
  def error_threshold_reached?
118
- @error_count >= @error_count_threshold
108
+ @errors.count == @error_count_threshold
119
109
  end
120
110
 
121
111
  def error_timeout_expired?
122
- @error_last_at && (@error_last_at + @error_timeout < Time.now)
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=#{@success_count} error_count=#{@error_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)
@@ -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
- def initialize(semian_identifier, *args)
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
- semian_options = query_options[:semian] || {}
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
- semian_resource.acquire(scope: :query) { raw_query(*args) }
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
- semian_resource.acquire(scope: :connect) { raw_connect(*args) }
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 semian_options
69
- options = query_options[:semian] || {}
70
- options = options.map { |k, v| [k.to_sym, v] }.to_h
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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Semian
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -21,5 +21,6 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency 'rake-compiler', '~> 0.9'
22
22
  s.add_development_dependency 'timecop'
23
23
  s.add_development_dependency 'mysql2'
24
+ s.add_development_dependency 'redis'
24
25
  s.add_development_dependency 'toxiproxy'
25
26
  end
@@ -1,6 +1,4 @@
1
- require 'minitest/autorun'
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
- @resource.with_fallback(42) { raise SomeError }
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
- @resource.with_fallback(42) { block_called = true }
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
- @resource.with_fallback(42) { block_called = true }
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,5 +1,4 @@
1
- require 'minitest/autorun'
2
- require 'semian'
1
+ require 'test_helper'
3
2
 
4
3
  class TestInstrumentation < MiniTest::Unit::TestCase
5
4
  def setup
@@ -1,7 +1,4 @@
1
- require 'minitest/autorun'
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 :connect, scope
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 notification have been emitted'
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 notification have been emitted'
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
- private
161
-
162
- def background(&block)
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
- def yield_to_background
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))
@@ -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
@@ -1,7 +1,4 @@
1
- require 'minitest/autorun'
2
- require 'semian'
3
- require 'tempfile'
4
- require 'fileutils'
1
+ require 'test_helper'
5
2
 
6
3
  class TestResource < MiniTest::Unit::TestCase
7
4
  def setup
@@ -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
@@ -1,5 +1,4 @@
1
- require 'minitest/autorun'
2
- require 'semian'
1
+ require 'test_helper'
3
2
 
4
3
  class TestSemian < MiniTest::Unit::TestCase
5
4
  def setup
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.1.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-13 00:00:00.000000000 Z
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/fixtures/toxiproxy.json
120
- - test/test_circuit_breaker.rb
121
- - test/test_instrumentation.rb
122
- - test/test_mysql2.rb
123
- - test/test_resource.rb
124
- - test/test_unsupported.rb
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
@@ -1,7 +0,0 @@
1
- [
2
- {
3
- "name": "semian_test_mysql",
4
- "upstream": "localhost:3306",
5
- "listen": "localhost:13306"
6
- }
7
- ]