semian 0.2.0 → 0.3.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: 1da1ae8c9c9376d81347eae49e8755aee743acf1
4
- data.tar.gz: dc3b57ca4747df54e05b072c3df52c7e731088a9
3
+ metadata.gz: fc4fd1a356e755e2aa60896da8d30394e545d44a
4
+ data.tar.gz: ea85970b77be890bf35c69a9d047525280f60e31
5
5
  SHA512:
6
- metadata.gz: 880796b7330efc3566fbe66a1d1756f99617cc496640ba58d4277e83f401ea65ed5619ed60869ef67049c91d87f620b28f04fdd0015ceb09e5ac4306a33d2a73
7
- data.tar.gz: 179a799c1b5d3e07abeb550ee940c527da01781ceceb6c1fd04dd2d42ca5971bde46a886449d4a1739da18944a25323aba593a064526f9ad464a51cdc4f19527
6
+ metadata.gz: 455a0fb4fef078ea9a7c2a5c5ee63584492726aa32e98d2a062a274d7680d6abb2366120722ffb20aa3069bc2efb282ac0906dc20af5dd656df8a588e5cfde63
7
+ data.tar.gz: bfe6ab61e67e6c5f6426bd0bf18b5710c50711f82966136bb8da420a2da2c091191886f57b834f130babff966620e3f717c16d6f65ed3e9d4dbd67f09c93ea74
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/.gitignore CHANGED
@@ -5,3 +5,4 @@
5
5
  *.gem
6
6
  /html/
7
7
  Gemfile.lock
8
+ vendor/
data/README.md CHANGED
@@ -1,83 +1,121 @@
1
1
  ## Semian [![Build Status](https://travis-ci.org/Shopify/semian.svg?branch=master)](https://travis-ci.org/Shopify/semian)
2
2
 
3
- Semian is a Ruby implementation of the Bulkhead resource isolation pattern,
4
- using SysV semaphores. Bulkheading controls access to external resources,
5
- protecting against resource or network latency, by allowing otherwise slow
6
- queries to fail fast.
7
-
8
- Downtime is easy to detect. Requests fail when querying the resource, usually
9
- fast. Reliably detecting higher than normal latency is more difficult. Strict
10
- timeouts is one solution, but picking those are hard and usually needs to be
11
- done per query or section of your application.
12
-
13
- Semian takes a different approach. Instead of asking the question: "How long can
14
- my query execute?" it raises the question "How long do I want to wait before
15
- starting to execute my query?".
16
-
17
- Imagine that your database is very slow. Requests that hit the slow database are
18
- processed in your workers and end up timing out at the worker level. However,
19
- other requests don't touch the slow database. These requests will start to queue
20
- up behind the requests to the slow database, possibly never being served
21
- because the client disconnects due to slowness. You're now effectively down,
22
- because a single external resource is slow.
23
-
24
- Semian solves this problem with resource tickets. Every time a worker addresses
25
- an external resource, it takes a ticket for the duration of the query. When the
26
- query returns, it puts the ticket back into the pool. If you have `n` tickets,
27
- and the `n + 1` worker tries to acquire a ticket to query the resource it'll
28
- wait for `timeout` seconds to see if a ticket comes available, otherwise it'll
29
- raise `Semian::TimeoutError`.
30
-
31
- By failing fast, this solves the problem of one slow database taking your
32
- platform down. The busyness of the external resource determines the `timeout`
33
- and ticket count. You can also rescue `Semian::TimeoutError` to provide fallback
34
- values, such as showing an error message to the user.
35
-
36
- A subset of workers will still be tied up on the slow database, meaning you are
37
- under capacity with a slow external resource. However, at most you'll have
38
- `ticket count` workers occupied. This is a small price to pay. By implementing
39
- the circuit breaker pattern on top of Semian, you can avoid that. That may be
40
- built into Semian in the future.
41
-
42
- Under the hood, Semian is implemented with SysV semaphores. In a threaded web
43
- server, the semaphore could be in-process. Semian was written with forked web
44
- servers in mind, such as Unicorn, but Semian can be used perfectly fine in a
45
- threaded web server.
46
-
47
- ### Usage
48
-
49
- In a master process, register a resource with a specified number of tickets
50
- (number of concurrent clients):
3
+ Semian is a latency and fault tolerance library for protecting your Ruby
4
+ applications against misbehaving external services. It allows you to fail fast
5
+ so you can handle errors gracefully. The patterns are inspired by
6
+ [Hystrix][hystrix] and [Release It][release-it]. Semian is an extraction from
7
+ [Shopify][shopify] where it's been running successfully in production since
8
+ October, 2014.
9
+
10
+ For an overview of building resilient Ruby application, see [the blog post on
11
+ Toxiproxy and Semian][resiliency-blog-post]. We recommend using
12
+ [Toxiproxy][toxiproxy] to test for resiliency.
13
+
14
+ # Usage
15
+
16
+ Install by adding the gem to your `Gemfile` and require the [adapters](#adapters) you need:
51
17
 
52
18
  ```ruby
53
- Semian.register(:mysql_master tickets: 3, timeout: 0.5, error_threshold: 3, error_timeout: 10, success_threshold: 2)
19
+ gem 'semian', require: %w(semian semian/mysql2 semian/redis)
54
20
  ```
55
21
 
56
- Then in your child processes, you can use the resource:
22
+ We recommend this pattern of requiring adapters directly from the `Gemfile`.
23
+ This makes ensures Semian adapters is loaded as early as possible, to also
24
+ protect your application during boot. Please see the [adapter configuration
25
+ section](#configuration) on how to configure adapters.
26
+
27
+ ## Adapters
28
+
29
+ The following adapters are in Semian and work against the public gems:
30
+
31
+ * [`semian/mysql2`][mysql-semian-adapter] (~> 0.3.16)
32
+ * [`semian/redis`][redis-semian-adapter] (~> 3.2.1)
33
+
34
+ ### Configuration
35
+
36
+ When instantiating a resource it now needs to be configured for Semian. This is
37
+ done by passing `semian` as an argument when initializing the client. Examples
38
+ built in adapters:
57
39
 
58
40
  ```ruby
59
- Semian[:mysql_master].acquire do
60
- # Query the database. If three other workers are querying this resource at the
61
- # same time, this block will block for up to 0.5s waiting for another worker
62
- # to release a ticket. Otherwise, it'll raise `Semian::TimeoutError`.
63
- end
41
+ # MySQL2 client
42
+ # In Rails this means having a Semian key in database.yml for each db.
43
+ client = Mysql2::Client.new(host: "localhost", username: "root", semian: {
44
+ name: "master",
45
+ tickets: 8, # See the Understanding Semian section on picking these values
46
+ success_threshold: 2,
47
+ error_threshold: 3,
48
+ error_timeout: 10
49
+ })
50
+
51
+ # Redis client
52
+ client = Redis.new(semian: {
53
+ name: "inventory",
54
+ tickets: 4,
55
+ success_threshold: 2,
56
+ error_threshold: 4,
57
+ error_timeout: 20
58
+ })
64
59
  ```
65
60
 
66
- If you have a process that doesn't `fork`, you can still use the same namespace
67
- to control access to a shared resource:
61
+ ### Creating an adapter
62
+
63
+ To create a Semian adapter you must implement the following methods:
64
+
65
+ 1. [`include Semian::Adapter`][semian-adapter]. Use the helpers to wrap the
66
+ resource. This takes care of situations such as monitoring, nested resources,
67
+ unsupported platforms, creating the Semian resource if it doesn't already
68
+ exist and so on.
69
+ 2. `#semian_identifier`. This is responsible for returning a symbol that
70
+ represents every unique resource, for example `redis_master` or
71
+ `mysql_shard_1`. This is usually assembled from a `name` attribute on the
72
+ Semian configuration hash, but could also be `<host>:<port>`.
73
+ 3. `connect`. The name of this method varies. You must override the driver's
74
+ connect method with one that wraps the connect call with
75
+ `Semian::Resource#acquire`. You should do this at the lowest possible level.
76
+ 4. `query`. Same as `connect` but for queries on the resource.
77
+ 5. Define exceptions `ResourceBusyError` and `CircuitOpenError`. These are
78
+ raised when the request was rejected early because the resource is out of
79
+ tickets or because the circuit breaker is open (see [Understanding
80
+ Semian](#understanding-semian). They should inherit from the base exception
81
+ class from the raw driver. For example `Mysql2::Error` or
82
+ `Redis::BaseConnectionError` for the MySQL and Redis drivers. This makes it
83
+ easy to `rescue` and handle them gracefully in application code, by
84
+ `rescue`ing the base class.
85
+
86
+ The best resource is looking at the [already implemented adapters](#adapters).
87
+
88
+ ## Monitoring
89
+
90
+ With [`Semian::Instrumentable`][semian-instrumentable] clients can monitor
91
+ Semian internals. For example to instrument just events with
92
+ [`statsd-instrument`][statsd-instrument]:
68
93
 
69
94
  ```ruby
70
- Semian.register(:mysql_master, timeout: 0.5)
71
-
72
- Semian[:mysql_master].acquire do
73
- # Query the resource
95
+ # `event` is `success`, `busy`, `circuit_open`.
96
+ # `resource` is the `Semian::Resource` object
97
+ # `scope` is `connection` or `query` (others can be instrumented too from the adapter)
98
+ # `adapter` is the name of the adapter (mysql2, redis, ..)
99
+ Semian.subscribe do |event, resource, scope, adapter|
100
+ StatsD.increment("Shopify.#{adapter}.semian.#{event}", 1, tags: [
101
+ "resource:#{resource.name}",
102
+ "total_tickets:#{resource.tickets}",
103
+ "type:#{scope}",
104
+ ])
74
105
  end
75
106
  ```
76
107
 
77
- ### Install
108
+ # Understanding Semian
78
109
 
79
- In your Gemfile:
110
+ Coming soon!
80
111
 
81
- ```ruby
82
- gem "semian"
83
- ```
112
+ [hystrix]: https://github.com/Netflix/Hystrix
113
+ [release-it]: https://pragprog.com/book/mnee/release-it
114
+ [shopify]: http://www.shopify.com/
115
+ [mysql-semian-adapter]: https://github.com/Shopify/semian/blob/master/lib/semian/mysql2.rb
116
+ [redis-semian-adapter]: https://github.com/Shopify/semian/blob/master/lib/semian/redis.rb
117
+ [semian-adapter]: https://github.com/Shopify/semian/blob/master/lib/semian/adapter.rb
118
+ [semian-instrumentable]: https://github.com/Shopify/semian/blob/master/lib/semian/instrumentable.rb
119
+ [statsd-instrument]: http://github.com/shopify/statsd-instrument
120
+ [resiliency-blog-post]: http://www.shopify.com/technology/16906928-building-and-testing-resilient-ruby-on-rails-applications
121
+ [toxiproxy]: https://github.com/Shopify/toxiproxy
data/Rakefile CHANGED
@@ -18,7 +18,7 @@ end
18
18
 
19
19
  $:.unshift File.expand_path("../lib", __FILE__)
20
20
  require 'semian/platform'
21
- if Semian.supported_platform?
21
+ if Semian.sysv_semaphores_supported?
22
22
  require 'rake/extensiontask'
23
23
  Rake::ExtensionTask.new('semian', GEMSPEC) do |ext|
24
24
  ext.ext_dir = 'ext/semian'
@@ -2,7 +2,7 @@ $:.unshift File.expand_path("../../../lib", __FILE__)
2
2
 
3
3
  require 'semian/platform'
4
4
 
5
- unless Semian.supported_platform?
5
+ unless Semian.sysv_semaphores_supported?
6
6
  File.write "Makefile", <<MAKEFILE
7
7
  all:
8
8
  clean:
data/lib/semian.rb CHANGED
@@ -77,14 +77,19 @@ module Semian
77
77
  InternalError = Class.new(BaseError)
78
78
  OpenCircuitError = Class.new(BaseError)
79
79
 
80
+ def semaphores_enabled?
81
+ !ENV['SEMIAN_SEMAPHORES_DISABLED']
82
+ end
83
+
80
84
  module AdapterError
81
- def initialize(semian_identifier, *args)
82
- super(*args)
83
- @semian_identifier = semian_identifier
84
- end
85
+ attr_accessor :semian_identifier
85
86
 
86
87
  def to_s
87
- "[#{@semian_identifier}] #{super}"
88
+ if @semian_identifier
89
+ "[#{@semian_identifier}] #{super}"
90
+ else
91
+ super
92
+ end
88
93
  end
89
94
  end
90
95
 
@@ -149,10 +154,11 @@ require 'semian/circuit_breaker'
149
154
  require 'semian/protected_resource'
150
155
  require 'semian/unprotected_resource'
151
156
  require 'semian/platform'
152
- if Semian.supported_platform?
157
+ if Semian.sysv_semaphores_supported? && Semian.semaphores_enabled?
153
158
  require 'semian/semian'
154
159
  else
155
160
  Semian::MAX_TICKETS = 0
156
- Semian.logger.info("Semian is not supported on #{RUBY_PLATFORM} - all operations will no-op")
161
+ Semian.logger.info("Semian sysv semaphores are not supported on #{RUBY_PLATFORM} - all operations will no-op") unless Semian.sysv_semaphores_supported?
162
+ Semian.logger.info("Semian semaphores are disabled, is this what you really want? - all operations will no-op") unless Semian.semaphores_enabled?
157
163
  end
158
164
  require 'semian/version'
@@ -14,6 +14,8 @@ module Semian
14
14
  else
15
15
  options = semian_options.dup
16
16
  options.delete(:name)
17
+ options[:exceptions] ||= []
18
+ options[:exceptions] += resource_exceptions
17
19
  ::Semian.retrieve_or_register(semian_identifier, **options)
18
20
  end
19
21
  end
@@ -28,7 +30,10 @@ module Semian
28
30
  rescue ::Semian::OpenCircuitError => error
29
31
  raise self.class::CircuitOpenError.new(semian_identifier, error)
30
32
  rescue ::Semian::BaseError => error
31
- raise self.class::ResourceOccupiedError.new(semian_identifier, error)
33
+ raise self.class::ResourceBusyError.new(semian_identifier, error)
34
+ rescue *resource_exceptions => error
35
+ error.semian_identifier = semian_identifier if error.respond_to?(:semian_identifier=)
36
+ raise
32
37
  end
33
38
 
34
39
  def semian_options
@@ -41,6 +46,10 @@ module Semian
41
46
  raise NotImplementedError.new("Semian adapters must implement a `raw_semian_options` method")
42
47
  end
43
48
 
49
+ def resource_exceptions
50
+ []
51
+ end
52
+
44
53
  def resource_already_acquired?
45
54
  @resource_acquired
46
55
  end
@@ -25,14 +25,6 @@ module Semian
25
25
  result
26
26
  end
27
27
 
28
- def with_fallback(fallback, &block)
29
- acquire(&block)
30
- rescue *@exceptions
31
- evaluate_fallback(fallback)
32
- rescue OpenCircuitError
33
- evaluate_fallback(fallback)
34
- end
35
-
36
28
  def request_allowed?
37
29
  return true if closed?
38
30
  half_open if error_timeout_expired?
@@ -63,14 +55,6 @@ module Semian
63
55
 
64
56
  private
65
57
 
66
- def evaluate_fallback(fallback_value_or_block)
67
- if fallback_value_or_block.respond_to?(:call)
68
- fallback_value_or_block.call
69
- else
70
- fallback_value_or_block
71
- end
72
- end
73
-
74
58
  def closed?
75
59
  state == :closed
76
60
  end
data/lib/semian/mysql2.rb CHANGED
@@ -3,11 +3,16 @@ require 'semian/adapter'
3
3
  require 'mysql2'
4
4
 
5
5
  module Mysql2
6
+ Mysql2::Error.include(::Semian::AdapterError)
7
+
6
8
  class SemianError < Mysql2::Error
7
- include ::Semian::AdapterError
9
+ def initialize(semian_identifier, *args)
10
+ super(*args)
11
+ @semian_identifier = semian_identifier
12
+ end
8
13
  end
9
14
 
10
- ResourceOccupiedError = Class.new(SemianError)
15
+ ResourceBusyError = Class.new(SemianError)
11
16
  CircuitOpenError = Class.new(SemianError)
12
17
  end
13
18
 
@@ -15,7 +20,13 @@ module Semian
15
20
  module Mysql2
16
21
  include Semian::Adapter
17
22
 
18
- ResourceOccupiedError = ::Mysql2::ResourceOccupiedError
23
+ CONNECTION_ERROR = Regexp.union(
24
+ /Can't connect to MySQL server on/i,
25
+ /Lost connection to MySQL server during query/i,
26
+ /MySQL server has gone away/i,
27
+ )
28
+
29
+ ResourceBusyError = ::Mysql2::ResourceBusyError
19
30
  CircuitOpenError = ::Mysql2::CircuitOpenError
20
31
 
21
32
  DEFAULT_HOST = 'localhost'
@@ -51,6 +62,16 @@ module Semian
51
62
  acquire_semian_resource(adapter: :mysql, scope: :connection) { raw_connect(*args) }
52
63
  end
53
64
 
65
+ def acquire_semian_resource(*)
66
+ super
67
+ rescue ::Mysql2::Error => error
68
+ if error.message =~ CONNECTION_ERROR
69
+ semian_resource.mark_failed(error)
70
+ error.semian_identifier = semian_identifier
71
+ end
72
+ raise
73
+ end
74
+
54
75
  def raw_semian_options
55
76
  return query_options[:semian] if query_options.key?(:semian)
56
77
  return query_options['semian'.freeze] if query_options.key?('semian'.freeze)
@@ -1,6 +1,6 @@
1
1
  module Semian
2
2
  # Determines if Semian supported on the current platform.
3
- def self.supported_platform?
3
+ def self.sysv_semaphores_supported?
4
4
  /linux/.match(RUBY_PLATFORM)
5
5
  end
6
6
  end
@@ -5,7 +5,7 @@ module Semian
5
5
  extend Forwardable
6
6
 
7
7
  def_delegators :@resource, :destroy, :count, :semid, :tickets, :name
8
- def_delegators :@circuit_breaker, :reset
8
+ def_delegators :@circuit_breaker, :reset, :mark_failed, :request_allowed?
9
9
 
10
10
  def initialize(resource, circuit_breaker)
11
11
  @resource = resource
@@ -20,7 +20,7 @@ module Semian
20
20
  yield self
21
21
  end
22
22
  rescue ::Semian::TimeoutError
23
- Semian.notify(:occupied, self, scope, adapter)
23
+ Semian.notify(:busy, self, scope, adapter)
24
24
  raise
25
25
  end
26
26
  end
@@ -28,9 +28,5 @@ module Semian
28
28
  Semian.notify(:circuit_open, self, scope, adapter)
29
29
  raise
30
30
  end
31
-
32
- def with_fallback(fallback, &block)
33
- @circuit_breaker.with_fallback(fallback) { @resource.acquire(&block) }
34
- end
35
31
  end
36
32
  end
data/lib/semian/redis.rb CHANGED
@@ -3,19 +3,36 @@ require 'semian/adapter'
3
3
  require 'redis'
4
4
 
5
5
  class Redis
6
+ Redis::BaseConnectionError.include(::Semian::AdapterError)
7
+ ::Errno::EINVAL.include(::Semian::AdapterError)
8
+
6
9
  class SemianError < Redis::BaseConnectionError
7
- include ::Semian::AdapterError
10
+ def initialize(semian_identifier, *args)
11
+ super(*args)
12
+ @semian_identifier = semian_identifier
13
+ end
8
14
  end
9
15
 
10
- ResourceOccupiedError = Class.new(SemianError)
16
+ ResourceBusyError = Class.new(SemianError)
11
17
  CircuitOpenError = Class.new(SemianError)
18
+
19
+ # This memoized alias is necessary because during a `pipelined` block
20
+ # the client is replaced by an instance of `Redis::Pipeline` and there is
21
+ # no way to access the original client.
22
+ def semian_resource
23
+ @semian_resource ||= @client.semian_resource
24
+ end
25
+
26
+ def semian_identifier
27
+ semian_resource.name
28
+ end
12
29
  end
13
30
 
14
31
  module Semian
15
32
  module Redis
16
33
  include Semian::Adapter
17
34
 
18
- ResourceOccupiedError = ::Redis::ResourceOccupiedError
35
+ ResourceBusyError = ::Redis::ResourceBusyError
19
36
  CircuitOpenError = ::Redis::CircuitOpenError
20
37
 
21
38
  # The naked methods are exposed as `raw_query` and `raw_connect` for instrumentation purpose
@@ -45,6 +62,13 @@ module Semian
45
62
 
46
63
  private
47
64
 
65
+ def resource_exceptions
66
+ [
67
+ ::Redis::BaseConnectionError,
68
+ ::Errno::EINVAL, # Hiredis bug: https://github.com/redis/hiredis-rb/issues/21
69
+ ]
70
+ end
71
+
48
72
  def raw_semian_options
49
73
  return options[:semian] if options.key?(:semian)
50
74
  return options['semian'.freeze] if options.key?('semian'.freeze)
@@ -29,5 +29,12 @@ module Semian
29
29
 
30
30
  def reset
31
31
  end
32
+
33
+ def request_allowed?
34
+ true
35
+ end
36
+
37
+ def mark_failed(error)
38
+ end
32
39
  end
33
40
  end
@@ -1,3 +1,3 @@
1
1
  module Semian
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -9,35 +9,6 @@ class TestCircuitBreaker < MiniTest::Unit::TestCase
9
9
  @resource = Semian[:testing]
10
10
  end
11
11
 
12
- def test_with_fallback_value_returns_the_value
13
- result = @resource.with_fallback(42) do
14
- raise SomeError
15
- end
16
- assert_equal 42, result
17
- end
18
-
19
- def test_with_fallback_block_call_the_block
20
- result = @resource.with_fallback(-> { 42 }) do
21
- raise SomeError
22
- end
23
- assert_equal 42, result
24
- end
25
-
26
- def test_unknown_exceptions_are_not_rescued
27
- assert_raises RuntimeError do
28
- @resource.with_fallback(42) do
29
- raise RuntimeError
30
- end
31
- end
32
- end
33
-
34
- def test_all_semian_exceptions_are_rescued
35
- result = @resource.with_fallback(42) do
36
- raise Semian::BaseError
37
- end
38
- assert_equal 42, result
39
- end
40
-
41
12
  def test_acquire_yield_when_the_circuit_is_closed
42
13
  block_called = false
43
14
  @resource.acquire { block_called = true }
@@ -124,18 +95,23 @@ class TestCircuitBreaker < MiniTest::Unit::TestCase
124
95
  end
125
96
 
126
97
  def trigger_error!(resource = @resource)
127
- resource.with_fallback(42) { raise SomeError }
98
+ resource.acquire { raise SomeError }
99
+ rescue SomeError
128
100
  end
129
101
 
130
102
  def assert_circuit_closed(resource = @resource)
131
103
  block_called = false
132
- resource.with_fallback(42) { block_called = true }
104
+ resource.acquire { block_called = true }
133
105
  assert block_called, 'Expected the circuit to be closed, but it was open'
134
106
  end
135
107
 
136
108
  def assert_circuit_opened(resource = @resource)
137
- block_called = false
138
- resource.with_fallback(42) { block_called = true }
139
- refute block_called, 'Expected the circuit to be open, but it was closed'
109
+ open = false
110
+ begin
111
+ resource.acquire { }
112
+ rescue Semian::OpenCircuitError
113
+ open = true
114
+ end
115
+ assert open, 'Expected the circuit to be open, but it was closed'
140
116
  end
141
117
  end
@@ -6,8 +6,8 @@ class TestInstrumentation < MiniTest::Unit::TestCase
6
6
  Semian.register(:testing, tickets: 1, error_threshold: 1, error_timeout: 5, success_threshold: 1)
7
7
  end
8
8
 
9
- def test_occupied_instrumentation
10
- assert_notify(:success, :occupied) do
9
+ def test_busy_instrumentation
10
+ assert_notify(:success, :busy) do
11
11
  Semian[:testing].acquire do
12
12
  assert_raises Semian::TimeoutError do
13
13
  Semian[:testing].acquire {}
@@ -17,7 +17,7 @@ class TestInstrumentation < MiniTest::Unit::TestCase
17
17
  end
18
18
 
19
19
  def test_circuit_open_instrumentation
20
- assert_notify(:success, :occupied) do
20
+ assert_notify(:success, :busy) do
21
21
  Semian[:testing].acquire do
22
22
  assert_raises Semian::TimeoutError do
23
23
  Semian[:testing].acquire {}
data/test/mysql2_test.rb CHANGED
@@ -29,6 +29,29 @@ class TestMysql2 < MiniTest::Unit::TestCase
29
29
  assert_instance_of Semian::UnprotectedResource, resource
30
30
  end
31
31
 
32
+ def test_connection_errors_open_the_circuit
33
+ @proxy.downstream(:latency, latency: 1200).apply do
34
+ ERROR_THRESHOLD.times do
35
+ assert_raises ::Mysql2::Error do
36
+ connect_to_mysql!
37
+ end
38
+ end
39
+
40
+ assert_raises ::Mysql2::CircuitOpenError do
41
+ connect_to_mysql!
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_query_errors_does_not_open_the_circuit
47
+ client = connect_to_mysql!
48
+ (ERROR_THRESHOLD * 2).times do
49
+ assert_raises ::Mysql2::Error do
50
+ client.query('ERROR!')
51
+ end
52
+ end
53
+ end
54
+
32
55
  def test_connect_instrumentation
33
56
  notified = false
34
57
  subscriber = Semian.subscribe do |event, resource, scope, adapter|
@@ -50,17 +73,37 @@ class TestMysql2 < MiniTest::Unit::TestCase
50
73
  client = connect_to_mysql!
51
74
 
52
75
  Semian[:mysql_testing].acquire do
53
- assert_raises Mysql2::ResourceOccupiedError do
76
+ error = assert_raises Mysql2::ResourceBusyError do
54
77
  connect_to_mysql!
55
78
  end
79
+ assert_equal :mysql_testing, error.semian_identifier
80
+ end
81
+ end
82
+
83
+ def test_network_errors_are_tagged_with_the_resource_identifier
84
+ client = connect_to_mysql!
85
+ @proxy.down do
86
+ error = assert_raises ::Mysql2::Error do
87
+ client.query('SELECT 1 + 1;')
88
+ end
89
+ assert_equal client.semian_identifier, error.semian_identifier
90
+ end
91
+ end
92
+
93
+ def test_other_mysql_errors_are_not_tagged_with_the_resource_identifier
94
+ client = connect_to_mysql!
95
+
96
+ error = assert_raises Mysql2::Error do
97
+ client.query('SYNTAX ERROR!')
56
98
  end
99
+ assert_nil error.semian_identifier
57
100
  end
58
101
 
59
102
  def test_resource_timeout_on_connect
60
103
  @proxy.downstream(:latency, latency: 500).apply do
61
104
  background { connect_to_mysql! }
62
105
 
63
- assert_raises Mysql2::ResourceOccupiedError do
106
+ assert_raises Mysql2::ResourceBusyError do
64
107
  connect_to_mysql!
65
108
  end
66
109
  end
@@ -71,7 +114,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
71
114
  background { connect_to_mysql! }
72
115
 
73
116
  ERROR_THRESHOLD.times do
74
- assert_raises Mysql2::ResourceOccupiedError do
117
+ assert_raises Mysql2::ResourceBusyError do
75
118
  connect_to_mysql!
76
119
  end
77
120
  end
@@ -111,7 +154,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
111
154
  client = connect_to_mysql!
112
155
 
113
156
  Semian[:mysql_testing].acquire do
114
- assert_raises Mysql2::ResourceOccupiedError do
157
+ assert_raises Mysql2::ResourceBusyError do
115
158
  client.query('SELECT 1 + 1;')
116
159
  end
117
160
  end
@@ -124,7 +167,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
124
167
  @proxy.downstream(:latency, latency: 500).apply do
125
168
  background { client2.query('SELECT 1 + 1;') }
126
169
 
127
- assert_raises Mysql2::ResourceOccupiedError do
170
+ assert_raises Mysql2::ResourceBusyError do
128
171
  client.query('SELECT 1 + 1;')
129
172
  end
130
173
  end
@@ -138,7 +181,7 @@ class TestMysql2 < MiniTest::Unit::TestCase
138
181
  background { client2.query('SELECT 1 + 1;') }
139
182
 
140
183
  ERROR_THRESHOLD.times do
141
- assert_raises Mysql2::ResourceOccupiedError do
184
+ assert_raises Mysql2::ResourceBusyError do
142
185
  client.query('SELECT 1 + 1;')
143
186
  end
144
187
  end
@@ -163,7 +206,12 @@ class TestMysql2 < MiniTest::Unit::TestCase
163
206
  private
164
207
 
165
208
  def connect_to_mysql!(semian_options = {})
166
- Mysql2::Client.new(host: '127.0.0.1', port: '13306', semian: SEMIAN_OPTIONS.merge(semian_options))
209
+ Mysql2::Client.new(
210
+ connect_timeout: 1,
211
+ host: '127.0.0.1',
212
+ port: '13306',
213
+ semian: SEMIAN_OPTIONS.merge(semian_options),
214
+ )
167
215
  end
168
216
 
169
217
  class FakeMysql < Mysql2::Client
data/test/redis_test.rb CHANGED
@@ -24,11 +24,43 @@ class TestRedis < MiniTest::Unit::TestCase
24
24
  assert_equal :'redis_example.com:42/0', Redis.new(host: 'example.com', port: 42).client.semian_identifier
25
25
  end
26
26
 
27
+ def test_client_alias
28
+ redis = connect_to_redis!
29
+ assert_equal redis.client.semian_resource, redis.semian_resource
30
+ assert_equal redis.client.semian_identifier, redis.semian_identifier
31
+ end
32
+
27
33
  def test_semian_can_be_disabled
28
34
  resource = Redis.new(semian: false).client.semian_resource
29
35
  assert_instance_of Semian::UnprotectedResource, resource
30
36
  end
31
37
 
38
+ def test_connection_errors_open_the_circuit
39
+ client = connect_to_redis!
40
+
41
+ @proxy.downstream(:latency, latency: 600).apply do
42
+ ERROR_THRESHOLD.times do
43
+ assert_raises ::Redis::TimeoutError do
44
+ client.get('foo')
45
+ end
46
+ end
47
+
48
+ assert_raises ::Redis::CircuitOpenError do
49
+ client.get('foo')
50
+ end
51
+ end
52
+ end
53
+
54
+ def test_command_errors_does_not_open_the_circuit
55
+ client = connect_to_redis!
56
+ client.hset('my_hash', 'foo', 'bar')
57
+ (ERROR_THRESHOLD * 2).times do
58
+ assert_raises Redis::CommandError do
59
+ client.get('my_hash')
60
+ end
61
+ end
62
+ end
63
+
32
64
  def test_connect_instrumentation
33
65
  notified = false
34
66
  subscriber = Semian.subscribe do |event, resource, scope, adapter|
@@ -50,17 +82,36 @@ class TestRedis < MiniTest::Unit::TestCase
50
82
  client = connect_to_redis!
51
83
 
52
84
  Semian[:redis_testing].acquire do
53
- assert_raises Redis::ResourceOccupiedError do
85
+ error = assert_raises Redis::ResourceBusyError do
86
+ connect_to_redis!
87
+ end
88
+ assert_equal :redis_testing, error.semian_identifier
89
+ end
90
+ end
91
+
92
+ def test_redis_connection_errors_are_tagged_with_the_resource_identifier
93
+ @proxy.downstream(:latency, latency: 600).apply do
94
+ error = assert_raises ::Redis::TimeoutError do
54
95
  connect_to_redis!
55
96
  end
97
+ assert_equal :redis_testing, error.semian_identifier
98
+ end
99
+ end
100
+
101
+ def test_other_redis_errors_are_not_tagged_with_the_resource_identifier
102
+ client = connect_to_redis!
103
+ client.set('foo', 'bar')
104
+ error = assert_raises ::Redis::CommandError do
105
+ client.hget('foo', 'bar')
56
106
  end
107
+ refute error.respond_to?(:semian_identifier)
57
108
  end
58
109
 
59
110
  def test_resource_timeout_on_connect
60
111
  @proxy.downstream(:latency, latency: 500).apply do
61
112
  background { connect_to_redis! }
62
113
 
63
- assert_raises Redis::ResourceOccupiedError do
114
+ assert_raises Redis::ResourceBusyError do
64
115
  connect_to_redis!
65
116
  end
66
117
  end
@@ -71,7 +122,7 @@ class TestRedis < MiniTest::Unit::TestCase
71
122
  background { connect_to_redis! }
72
123
 
73
124
  ERROR_THRESHOLD.times do
74
- assert_raises Redis::ResourceOccupiedError do
125
+ assert_raises Redis::ResourceBusyError do
75
126
  connect_to_redis!
76
127
  end
77
128
  end
@@ -111,7 +162,7 @@ class TestRedis < MiniTest::Unit::TestCase
111
162
  client = connect_to_redis!
112
163
 
113
164
  Semian[:redis_testing].acquire do
114
- assert_raises Redis::ResourceOccupiedError do
165
+ assert_raises Redis::ResourceBusyError do
115
166
  client.get('foo')
116
167
  end
117
168
  end
@@ -124,7 +175,7 @@ class TestRedis < MiniTest::Unit::TestCase
124
175
  @proxy.downstream(:latency, latency: 500).apply do
125
176
  background { client2.get('foo') }
126
177
 
127
- assert_raises Redis::ResourceOccupiedError do
178
+ assert_raises Redis::ResourceBusyError do
128
179
  client.get('foo')
129
180
  end
130
181
  end
@@ -140,7 +191,7 @@ class TestRedis < MiniTest::Unit::TestCase
140
191
  background { client2.get('foo') }
141
192
 
142
193
  ERROR_THRESHOLD.times do
143
- assert_raises Redis::ResourceOccupiedError do
194
+ assert_raises Redis::ResourceBusyError do
144
195
  client.get('foo')
145
196
  end
146
197
  end
@@ -160,7 +211,14 @@ class TestRedis < MiniTest::Unit::TestCase
160
211
  private
161
212
 
162
213
  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))
214
+ redis = Redis.new(
215
+ host: '127.0.0.1',
216
+ port: 16379,
217
+ reconnect_attempts: 0,
218
+ db: 1,
219
+ timeout: 0.5,
220
+ semian: SEMIAN_OPTIONS.merge(semian_options)
221
+ )
164
222
  redis.client.connect
165
223
  redis
166
224
  end
@@ -19,4 +19,13 @@ class TestSemian < MiniTest::Unit::TestCase
19
19
  assert defined?(Semian::InternalError)
20
20
  assert defined?(Semian::Resource)
21
21
  end
22
+
23
+ def test_disabled_via_env_var
24
+ ENV['SEMIAN_SEMAPHORES_DISABLED'] = '1'
25
+
26
+ refute Semian.semaphores_enabled?
27
+ ensure
28
+ ENV.delete('SEMIAN_SEMAPHORES_DISABLED')
29
+ end
30
+
22
31
  end
@@ -5,6 +5,11 @@ class UnprotectedResourceTest < MiniTest::Unit::TestCase
5
5
  @resource = Semian::UnprotectedResource.new(:foo)
6
6
  end
7
7
 
8
+ def test_interface_is_the_same
9
+ diff = Semian::ProtectedResource.public_instance_methods - Semian::UnprotectedResource.public_instance_methods
10
+ assert_equal [], diff
11
+ end
12
+
8
13
  def test_resource_name
9
14
  assert_equal :foo, @resource.name
10
15
  end
@@ -45,4 +50,11 @@ class UnprotectedResourceTest < MiniTest::Unit::TestCase
45
50
  assert acquired
46
51
  end
47
52
 
53
+ def test_request_is_allowed
54
+ assert @resource.request_allowed?
55
+ end
56
+
57
+ def test_mark_failed
58
+ @resource.mark_failed(:error)
59
+ end
48
60
  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.2.0
4
+ version: 0.3.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-19 00:00:00.000000000 Z
34
+ date: 2015-03-11 00:00:00.000000000 Z
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake-compiler
@@ -139,9 +139,9 @@ files:
139
139
  - test/mysql2_test.rb
140
140
  - test/redis_test.rb
141
141
  - test/resource_test.rb
142
+ - test/semian_test.rb
142
143
  - test/test_helper.rb
143
144
  - test/unprotected_resource_test.rb
144
- - test/unsupported_test.rb
145
145
  homepage: https://github.com/csfrancis/semian
146
146
  licenses:
147
147
  - MIT
metadata.gz.sig CHANGED
Binary file