semian 0.0.8 → 0.1.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/.travis.yml +4 -1
- data/README.md +1 -1
- data/Rakefile +7 -6
- data/ext/semian/semian.c +7 -13
- data/lib/semian/circuit_breaker.rb +135 -0
- data/lib/semian/instrumentable.rb +22 -0
- data/lib/semian/mysql2.rb +77 -0
- data/lib/semian/platform.rb +2 -2
- data/lib/semian/protected_resource.rb +36 -0
- data/lib/semian/resource.rb +26 -0
- data/lib/semian/version.rb +2 -2
- data/lib/semian.rb +80 -27
- data/scripts/install_toxiproxy.sh +25 -0
- data/semian.gemspec +3 -0
- data/test/fixtures/toxiproxy.json +7 -0
- data/test/test_circuit_breaker.rb +114 -0
- data/test/test_instrumentation.rb +62 -0
- data/test/test_mysql2.rb +186 -0
- data/test/{test_semian.rb → test_resource.rb} +73 -55
- data/test/test_unsupported.rb +7 -3
- data.tar.gz.sig +0 -0
- metadata +55 -4
- metadata.gz.sig +1 -2
- data/lib/semian/unsupported.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 219d1e2e6ab5b6bbc6c90e0e418e47a38a6415cd
|
4
|
+
data.tar.gz: dfe7b83c1f95e26d0df6daa2cde3a914e98d2520
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79f643b811ba718f89a1021c2227b210e4b3045cb5ebc919e6017dcc0334d65efe31a7d30695abf1287f24894794f2c513128e6b07b52553eb0e9cac4cb11e65
|
7
|
+
data.tar.gz: 267d1b19a1c8c6ab7e9786b639a0c326dc9e4669636dfb20a19f36f43386b669029b172ea3f38bf790cc173f7cef9470c1c5b488a694a2c73eb0385ae4ceaa5c
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -50,7 +50,7 @@ In a master process, register a resource with a specified number of tickets
|
|
50
50
|
(number of concurrent clients):
|
51
51
|
|
52
52
|
```ruby
|
53
|
-
Semian.register(:mysql_master
|
53
|
+
Semian.register(:mysql_master tickets: 3, timeout: 0.5, error_threshold: 3, error_timeout: 10, success_threshold: 2)
|
54
54
|
```
|
55
55
|
|
56
56
|
Then in your child processes, you can use the resource:
|
data/Rakefile
CHANGED
@@ -29,19 +29,20 @@ else
|
|
29
29
|
task :build do; end
|
30
30
|
end
|
31
31
|
|
32
|
+
task :populate_proxy do
|
33
|
+
require 'toxiproxy'
|
34
|
+
Toxiproxy.populate(File.expand_path('../test/fixtures/toxiproxy.json', __FILE__))
|
35
|
+
end
|
36
|
+
|
32
37
|
# ==========================================================
|
33
38
|
# Testing
|
34
39
|
# ==========================================================
|
35
40
|
|
36
41
|
require 'rake/testtask'
|
37
42
|
Rake::TestTask.new 'test' do |t|
|
38
|
-
t.
|
39
|
-
FileList['test/test_semian.rb']
|
40
|
-
else
|
41
|
-
FileList['test/test_unsupported.rb']
|
42
|
-
end
|
43
|
+
t.pattern = "test/test_*.rb"
|
43
44
|
end
|
44
|
-
task :test => :build
|
45
|
+
task :test => [:build, :populate_proxy]
|
45
46
|
|
46
47
|
# ==========================================================
|
47
48
|
# Documentation
|
data/ext/semian/semian.c
CHANGED
@@ -449,10 +449,10 @@ semian_resource_id(VALUE self)
|
|
449
449
|
|
450
450
|
void Init_semian()
|
451
451
|
{
|
452
|
-
VALUE cSemian, cResource
|
452
|
+
VALUE cSemian, cResource;
|
453
453
|
struct seminfo info_buf;
|
454
454
|
|
455
|
-
cSemian =
|
455
|
+
cSemian = rb_const_get(rb_cObject, rb_intern("Semian"));
|
456
456
|
|
457
457
|
/*
|
458
458
|
* Document-class: Semian::Resource
|
@@ -462,25 +462,19 @@ void Init_semian()
|
|
462
462
|
*
|
463
463
|
* You should not create this class directly, it will be created indirectly via Semian.register.
|
464
464
|
*/
|
465
|
-
cResource =
|
466
|
-
|
467
|
-
/* Document-class: Semian::BaseError
|
468
|
-
*
|
469
|
-
* Base error class for all other Semian errors.
|
470
|
-
*/
|
471
|
-
eBaseError = rb_define_class_under(cSemian, "BaseError", rb_eStandardError);
|
465
|
+
cResource = rb_const_get(cSemian, rb_intern("Resource"));
|
472
466
|
|
473
467
|
/* Document-class: Semian::SyscallError
|
474
468
|
*
|
475
469
|
* Represents a Semian error that was caused by an underlying syscall failure.
|
476
470
|
*/
|
477
|
-
eSyscall =
|
471
|
+
eSyscall = rb_const_get(cSemian, rb_intern("SyscallError"));
|
478
472
|
|
479
473
|
/* Document-class: Semian::TimeoutError
|
480
474
|
*
|
481
475
|
* Raised when a Semian operation timed out.
|
482
476
|
*/
|
483
|
-
eTimeout =
|
477
|
+
eTimeout = rb_const_get(cSemian, rb_intern("TimeoutError"));
|
484
478
|
|
485
479
|
/* Document-class: Semian::InternalError
|
486
480
|
*
|
@@ -492,10 +486,10 @@ void Init_semian()
|
|
492
486
|
* using the <code>ipcrm</code> command line tool. Semian will re-initialize
|
493
487
|
* the semaphore in this case.
|
494
488
|
*/
|
495
|
-
eInternal =
|
489
|
+
eInternal = rb_const_get(cSemian, rb_intern("InternalError"));
|
496
490
|
|
497
491
|
rb_define_alloc_func(cResource, semian_resource_alloc);
|
498
|
-
rb_define_method(cResource, "
|
492
|
+
rb_define_method(cResource, "_initialize", semian_resource_initialize, 4);
|
499
493
|
rb_define_method(cResource, "acquire", semian_resource_acquire, -1);
|
500
494
|
rb_define_method(cResource, "count", semian_resource_count, 0);
|
501
495
|
rb_define_method(cResource, "semid", semian_resource_id, 0);
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Semian
|
2
|
+
class CircuitBreaker
|
3
|
+
attr_reader :state
|
4
|
+
|
5
|
+
def initialize(exceptions:, success_threshold:, error_threshold:, error_timeout:)
|
6
|
+
@success_count_threshold = success_threshold
|
7
|
+
@error_count_threshold = error_threshold
|
8
|
+
@error_timeout = error_timeout
|
9
|
+
@exceptions = exceptions
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def acquire(&block)
|
14
|
+
raise OpenCircuitError unless request_allowed?
|
15
|
+
|
16
|
+
result = nil
|
17
|
+
begin
|
18
|
+
result = yield
|
19
|
+
rescue *@exceptions => error
|
20
|
+
mark_failed(error)
|
21
|
+
raise error
|
22
|
+
else
|
23
|
+
mark_success
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
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
|
+
def request_allowed?
|
37
|
+
return true if closed?
|
38
|
+
half_open if error_timeout_expired?
|
39
|
+
!open?
|
40
|
+
end
|
41
|
+
|
42
|
+
def mark_failed(error)
|
43
|
+
increment_recent_errors
|
44
|
+
|
45
|
+
if closed?
|
46
|
+
open if error_threshold_reached?
|
47
|
+
elsif half_open?
|
48
|
+
open
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def mark_success
|
53
|
+
return unless half_open?
|
54
|
+
@success_count += 1
|
55
|
+
close if success_threshold_reached?
|
56
|
+
end
|
57
|
+
|
58
|
+
def reset
|
59
|
+
@success_count = 0
|
60
|
+
@error_count = 0
|
61
|
+
@error_last_at = nil
|
62
|
+
close
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def evaluate_fallback(fallback_value_or_block)
|
68
|
+
if fallback_value_or_block.respond_to?(:call)
|
69
|
+
fallback_value_or_block.call
|
70
|
+
else
|
71
|
+
fallback_value_or_block
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def closed?
|
76
|
+
state == :closed
|
77
|
+
end
|
78
|
+
|
79
|
+
def close
|
80
|
+
log_state_transition(:closed)
|
81
|
+
@state = :closed
|
82
|
+
@error_count = 0
|
83
|
+
end
|
84
|
+
|
85
|
+
def open?
|
86
|
+
state == :open
|
87
|
+
end
|
88
|
+
|
89
|
+
def open
|
90
|
+
log_state_transition(:open)
|
91
|
+
@state = :open
|
92
|
+
end
|
93
|
+
|
94
|
+
def half_open?
|
95
|
+
state == :half_open
|
96
|
+
end
|
97
|
+
|
98
|
+
def half_open
|
99
|
+
log_state_transition(:half_open)
|
100
|
+
@state = :half_open
|
101
|
+
@success_count = 0
|
102
|
+
end
|
103
|
+
|
104
|
+
def increment_recent_errors
|
105
|
+
if error_timeout_expired?
|
106
|
+
@error_count = 0
|
107
|
+
end
|
108
|
+
|
109
|
+
@error_count += 1
|
110
|
+
@error_last_at = Time.now
|
111
|
+
end
|
112
|
+
|
113
|
+
def success_threshold_reached?
|
114
|
+
@success_count >= @success_count_threshold
|
115
|
+
end
|
116
|
+
|
117
|
+
def error_threshold_reached?
|
118
|
+
@error_count >= @error_count_threshold
|
119
|
+
end
|
120
|
+
|
121
|
+
def error_timeout_expired?
|
122
|
+
@error_last_at && (@error_last_at + @error_timeout < Time.now)
|
123
|
+
end
|
124
|
+
|
125
|
+
def log_state_transition(new_state)
|
126
|
+
return if @state.nil? || new_state == @state
|
127
|
+
|
128
|
+
str = "[#{self.class.name}] State transition from #{@state} to #{new_state}."
|
129
|
+
str << " success_count=#{@success_count} error_count=#{@error_count}"
|
130
|
+
str << " success_count_threshold=#{@success_count_threshold} error_count_threshold=#{@error_count_threshold}"
|
131
|
+
str << " error_timeout=#{@error_timeout} error_last_at=\"#{@error_last_at}\""
|
132
|
+
Semian.logger.info(str)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Semian
|
2
|
+
module Instrumentable
|
3
|
+
def subscribe(name = rand, &block)
|
4
|
+
subscribers[name] = block
|
5
|
+
name
|
6
|
+
end
|
7
|
+
|
8
|
+
def unsubscribe(name)
|
9
|
+
subscribers.delete(name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def notify(*args)
|
13
|
+
subscribers.values.each { |subscriber| subscriber.call(*args) }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def subscribers
|
19
|
+
@subscribers ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'semian'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
module Mysql2
|
5
|
+
class SemianError < Mysql2::Error
|
6
|
+
def initialize(semian_identifier, *args)
|
7
|
+
super(*args)
|
8
|
+
@semian_identifier = semian_identifier
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"[#{@semian_identifier}] #{super}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ResourceOccupiedError = Class.new(SemianError)
|
17
|
+
CircuitOpenError = Class.new(SemianError)
|
18
|
+
end
|
19
|
+
|
20
|
+
module Semian
|
21
|
+
module Mysql2
|
22
|
+
DEFAULT_HOST = 'localhost'
|
23
|
+
DEFAULT_PORT = 3306
|
24
|
+
|
25
|
+
# The naked methods are exposed as `raw_query` and `raw_connect` for instrumentation purpose
|
26
|
+
def self.included(base)
|
27
|
+
base.send(:alias_method, :raw_query, :query)
|
28
|
+
base.send(:remove_method, :query)
|
29
|
+
|
30
|
+
base.send(:alias_method, :raw_connect, :connect)
|
31
|
+
base.send(:remove_method, :connect)
|
32
|
+
end
|
33
|
+
|
34
|
+
def semian_identifier
|
35
|
+
@semian_identifier ||= begin
|
36
|
+
semian_options = query_options[:semian] || {}
|
37
|
+
unless name = semian_options['name'.freeze] || semian_options[:name]
|
38
|
+
host = query_options[:host] || DEFAULT_HOST
|
39
|
+
port = query_options[:port] || DEFAULT_PORT
|
40
|
+
name = "#{host}:#{port}"
|
41
|
+
end
|
42
|
+
:"mysql_#{name}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def query(*args)
|
47
|
+
semian_resource.acquire(scope: :query) { raw_query(*args) }
|
48
|
+
rescue ::Semian::OpenCircuitError => error
|
49
|
+
raise ::Mysql2::CircuitOpenError.new(semian_identifier, error)
|
50
|
+
rescue ::Semian::BaseError => error
|
51
|
+
raise ::Mysql2::ResourceOccupiedError.new(semian_identifier, error)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def connect(*args)
|
57
|
+
semian_resource.acquire(scope: :connect) { raw_connect(*args) }
|
58
|
+
rescue ::Semian::OpenCircuitError => error
|
59
|
+
raise ::Mysql2::CircuitOpenError.new(semian_identifier, error)
|
60
|
+
rescue ::Semian::BaseError => error
|
61
|
+
raise ::Mysql2::ResourceOccupiedError.new(semian_identifier, error)
|
62
|
+
end
|
63
|
+
|
64
|
+
def semian_resource
|
65
|
+
@semian_resource ||= ::Semian.retrieve_or_register(semian_identifier, **semian_options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def semian_options
|
69
|
+
options = query_options[:semian] || {}
|
70
|
+
options = options.map { |k, v| [k.to_sym, v] }.to_h
|
71
|
+
options.delete(:name)
|
72
|
+
options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
::Mysql2::Client.include(Semian::Mysql2)
|
data/lib/semian/platform.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Semian
|
4
|
+
class ProtectedResource
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators :@resource, :destroy, :count, :semid, :tickets, :name
|
8
|
+
def_delegators :@circuit_breaker, :reset
|
9
|
+
|
10
|
+
def initialize(resource, circuit_breaker)
|
11
|
+
@resource = resource
|
12
|
+
@circuit_breaker = circuit_breaker
|
13
|
+
end
|
14
|
+
|
15
|
+
def acquire(timeout: nil, scope: nil, &block)
|
16
|
+
@circuit_breaker.acquire do
|
17
|
+
begin
|
18
|
+
@resource.acquire(timeout: timeout) do
|
19
|
+
Semian.notify(:success, self, scope)
|
20
|
+
yield self
|
21
|
+
end
|
22
|
+
rescue ::Semian::TimeoutError
|
23
|
+
Semian.notify(:occupied, self, scope)
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
rescue ::Semian::OpenCircuitError
|
28
|
+
Semian.notify(:circuit_open, self, scope)
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_fallback(fallback, &block)
|
33
|
+
@circuit_breaker.with_fallback(fallback) { @resource.acquire(&block) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Semian
|
2
|
+
class Resource #:nodoc:
|
3
|
+
attr_reader :tickets, :name
|
4
|
+
|
5
|
+
def initialize(name, tickets: , permissions: 0660, timeout: 0)
|
6
|
+
_initialize(name, tickets, permissions, timeout) if respond_to?(:_initialize)
|
7
|
+
@name = name
|
8
|
+
@tickets = tickets
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroy
|
12
|
+
end
|
13
|
+
|
14
|
+
def acquire(*)
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
|
18
|
+
def count
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
def semid
|
23
|
+
0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/semian/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.0
|
1
|
+
module Semian
|
2
|
+
VERSION = '0.1.0'
|
3
3
|
end
|
data/lib/semian.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'semian/instrumentable'
|
3
|
+
|
1
4
|
#
|
2
5
|
# === Overview
|
3
6
|
#
|
@@ -25,16 +28,28 @@
|
|
25
28
|
# the timeout period has elapsed, the client will be unable to access the service and
|
26
29
|
# an error will be raised.
|
27
30
|
#
|
31
|
+
# Resources also integrate a circuit breaker in order to fail faster and to let the
|
32
|
+
# resource the time to recover. If `error_threshold` errors happen in the span of `error_timeout`
|
33
|
+
# then the circuit will be opened and every attempt to acquire the resource will immediately fail.
|
34
|
+
#
|
35
|
+
# Once in open state, after `error_timeout` is elapsed, the ciruit will transition in the half-open state.
|
36
|
+
# In that state a single error will fully re-open the circuit, and the circuit will transition back to the closed
|
37
|
+
# state only after the resource is acquired `success_threshold` consecutive times.
|
38
|
+
#
|
28
39
|
# A resource is registered by using the Semian.register method.
|
29
40
|
#
|
30
41
|
# ==== Examples
|
31
42
|
#
|
32
43
|
# ===== Registering a resource
|
33
44
|
#
|
34
|
-
# Semian.register
|
45
|
+
# Semian.register(:mysql_shard0, tickets: 10, timeout: 0.5, error_threshold: 3, error_timeout: 10, success_threshold: 2)
|
35
46
|
#
|
36
47
|
# This registers a new resource called <code>:mysql_shard0</code> that has 10 tickets andd a default timeout of 500 milliseconds.
|
37
48
|
#
|
49
|
+
# After 3 failures in the span of 10 seconds the circuit will be open.
|
50
|
+
# After an additional 10 seconds it will transition to half-open.
|
51
|
+
# And finally after 2 successulf acquisitions of the resource it will transition back to the closed state.
|
52
|
+
#
|
38
53
|
# ===== Using a resource
|
39
54
|
#
|
40
55
|
# Semian[:mysql_shard0].acquire do
|
@@ -52,42 +67,80 @@
|
|
52
67
|
#
|
53
68
|
# This is the same as the previous example, but overrides the timeout from the default value of 500 milliseconds to 1 second.
|
54
69
|
#
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
#
|
59
|
-
# +name+: Name of the resource - this can be either a string or symbol.
|
60
|
-
#
|
61
|
-
# +tickets+: Number of tickets. If this value is 0, the ticket count will not be set,
|
62
|
-
# but the resource must have been previously registered otherwise an error will be raised.
|
63
|
-
#
|
64
|
-
# +permissions+: Octal permissions of the resource.
|
65
|
-
#
|
66
|
-
# +timeout+: Default timeout in seconds.
|
67
|
-
#
|
68
|
-
# Returns the registered resource.
|
69
|
-
def register(name, tickets: 0, permissions: 0660, timeout: 1)
|
70
|
-
resource = Resource.new(name, tickets, permissions, timeout)
|
71
|
-
resources[name] = resource
|
72
|
-
end
|
70
|
+
module Semian
|
71
|
+
extend self
|
72
|
+
extend Instrumentable
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
BaseError = Class.new(StandardError)
|
75
|
+
SyscallError = Class.new(BaseError)
|
76
|
+
TimeoutError = Class.new(BaseError)
|
77
|
+
InternalError = Class.new(BaseError)
|
78
|
+
OpenCircuitError = Class.new(BaseError)
|
79
|
+
|
80
|
+
attr_accessor :logger
|
78
81
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
self.logger = Logger.new(nil)
|
83
|
+
|
84
|
+
# Registers a resource.
|
85
|
+
#
|
86
|
+
# +name+: Name of the resource - this can be either a string or symbol.
|
87
|
+
#
|
88
|
+
# +tickets+: Number of tickets. If this value is 0, the ticket count will not be set,
|
89
|
+
# but the resource must have been previously registered otherwise an error will be raised.
|
90
|
+
#
|
91
|
+
# +permissions+: Octal permissions of the resource.
|
92
|
+
#
|
93
|
+
# +timeout+: Default timeout in seconds.
|
94
|
+
#
|
95
|
+
# +error_threshold+: The number of errors that will trigger the circuit opening.
|
96
|
+
#
|
97
|
+
# +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
|
98
|
+
#
|
99
|
+
# +success_threshold+: The number of consecutive success after which an half-open circuit will be fully closed.
|
100
|
+
#
|
101
|
+
# +exceptions+: An array of exception classes that should be accounted as resource errors.
|
102
|
+
#
|
103
|
+
# Returns the registered resource.
|
104
|
+
def register(name, tickets:, permissions: 0660, timeout: 0, error_threshold:, error_timeout:, success_threshold:, exceptions: [])
|
105
|
+
circuit_breaker = CircuitBreaker.new(
|
106
|
+
success_threshold: success_threshold,
|
107
|
+
error_threshold: error_threshold,
|
108
|
+
error_timeout: error_timeout,
|
109
|
+
exceptions: Array(exceptions) + [::Semian::BaseError],
|
110
|
+
)
|
111
|
+
resource = Resource.new(name, tickets: tickets, permissions: permissions, timeout: timeout)
|
112
|
+
resources[name] = ProtectedResource.new(resource, circuit_breaker)
|
113
|
+
end
|
114
|
+
|
115
|
+
def retrieve_or_register(name, **args)
|
116
|
+
self[name] || register(name, **args)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Retrieves a resource by name.
|
120
|
+
def [](name)
|
121
|
+
resources[name]
|
122
|
+
end
|
123
|
+
|
124
|
+
def destroy(name)
|
125
|
+
if resource = resources.delete(name)
|
126
|
+
resource.destroy
|
82
127
|
end
|
83
128
|
end
|
129
|
+
|
130
|
+
# Retrieves a hash of all registered resources.
|
131
|
+
def resources
|
132
|
+
@resources ||= {}
|
133
|
+
end
|
84
134
|
end
|
85
135
|
|
136
|
+
require 'semian/resource'
|
137
|
+
require 'semian/circuit_breaker'
|
138
|
+
require 'semian/protected_resource'
|
86
139
|
require 'semian/platform'
|
87
140
|
if Semian.supported_platform?
|
88
141
|
require 'semian/semian'
|
89
142
|
else
|
90
|
-
|
143
|
+
Semian::MAX_TICKETS = 0
|
91
144
|
$stderr.puts "Semian is not supported on #{RUBY_PLATFORM} - all operations will no-op"
|
92
145
|
end
|
93
146
|
require 'semian/version'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
set -e
|
2
|
+
|
3
|
+
if which toxiproxy > /dev/null; then
|
4
|
+
echo "Toxiproxy is already installed."
|
5
|
+
exit 0
|
6
|
+
fi
|
7
|
+
|
8
|
+
if which apt-get > /dev/null; then
|
9
|
+
echo "Installing toxiproxy-1.0.0.deb"
|
10
|
+
wget -O /tmp/toxiproxy-1.0.0.deb https://github.com/Shopify/toxiproxy/releases/download/v1.0.0/toxiproxy_1.0.0_amd64.deb
|
11
|
+
sudo dpkg -i /tmp/toxiproxy-1.0.0.deb
|
12
|
+
sudo service toxiproxy start
|
13
|
+
exit 0
|
14
|
+
fi
|
15
|
+
|
16
|
+
if which brew > /dev/null; then
|
17
|
+
echo "Installing toxiproxy from homebrew."
|
18
|
+
brew tap shopify/shopify
|
19
|
+
brew install toxiproxy
|
20
|
+
brew info toxiproxy
|
21
|
+
exit 0
|
22
|
+
fi
|
23
|
+
|
24
|
+
echo "Sorry, there is no toxiproxy package available for your system. You might need to build it from source."
|
25
|
+
exit 1
|
data/semian.gemspec
CHANGED
@@ -19,4 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.files = `git ls-files`.split("\n")
|
20
20
|
s.extensions = ['ext/semian/extconf.rb']
|
21
21
|
s.add_development_dependency 'rake-compiler', '~> 0.9'
|
22
|
+
s.add_development_dependency 'timecop'
|
23
|
+
s.add_development_dependency 'mysql2'
|
24
|
+
s.add_development_dependency 'toxiproxy'
|
22
25
|
end
|