semian 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|