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 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
- ]