semian 0.2.0 → 0.3.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: 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