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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +1 -0
- data/README.md +104 -66
- data/Rakefile +1 -1
- data/ext/semian/extconf.rb +1 -1
- data/lib/semian.rb +13 -7
- data/lib/semian/adapter.rb +10 -1
- data/lib/semian/circuit_breaker.rb +0 -16
- data/lib/semian/mysql2.rb +24 -3
- data/lib/semian/platform.rb +1 -1
- data/lib/semian/protected_resource.rb +2 -6
- data/lib/semian/redis.rb +27 -3
- data/lib/semian/unprotected_resource.rb +7 -0
- data/lib/semian/version.rb +1 -1
- data/test/circuit_breaker_test.rb +10 -34
- data/test/instrumentation_test.rb +3 -3
- data/test/mysql2_test.rb +55 -7
- data/test/redis_test.rb +65 -7
- data/test/{unsupported_test.rb → semian_test.rb} +9 -0
- data/test/unprotected_resource_test.rb +12 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc4fd1a356e755e2aa60896da8d30394e545d44a
|
4
|
+
data.tar.gz: ea85970b77be890bf35c69a9d047525280f60e31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -1,83 +1,121 @@
|
|
1
1
|
## Semian [](https://travis-ci.org/Shopify/semian)
|
2
2
|
|
3
|
-
Semian is a
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
19
|
+
gem 'semian', require: %w(semian semian/mysql2 semian/redis)
|
54
20
|
```
|
55
21
|
|
56
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
108
|
+
# Understanding Semian
|
78
109
|
|
79
|
-
|
110
|
+
Coming soon!
|
80
111
|
|
81
|
-
|
82
|
-
|
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.
|
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'
|
data/ext/semian/extconf.rb
CHANGED
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
|
-
|
82
|
-
super(*args)
|
83
|
-
@semian_identifier = semian_identifier
|
84
|
-
end
|
85
|
+
attr_accessor :semian_identifier
|
85
86
|
|
86
87
|
def to_s
|
87
|
-
|
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.
|
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
|
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'
|
data/lib/semian/adapter.rb
CHANGED
@@ -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::
|
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
|
-
|
9
|
+
def initialize(semian_identifier, *args)
|
10
|
+
super(*args)
|
11
|
+
@semian_identifier = semian_identifier
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
|
-
|
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
|
-
|
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)
|
data/lib/semian/platform.rb
CHANGED
@@ -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(:
|
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
|
-
|
10
|
+
def initialize(semian_identifier, *args)
|
11
|
+
super(*args)
|
12
|
+
@semian_identifier = semian_identifier
|
13
|
+
end
|
8
14
|
end
|
9
15
|
|
10
|
-
|
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
|
-
|
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)
|
data/lib/semian/version.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
138
|
-
|
139
|
-
|
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
|
10
|
-
assert_notify(:success, :
|
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, :
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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(
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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(
|
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.
|
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-
|
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
|