semian 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ext/semian/extconf.rb +1 -1
- data/ext/semian/resource.c +134 -17
- data/ext/semian/resource.h +56 -3
- data/ext/semian/semian.c +6 -1
- data/ext/semian/sysv_semaphores.c +148 -43
- data/ext/semian/sysv_semaphores.h +49 -5
- data/ext/semian/tickets.c +64 -54
- data/ext/semian/tickets.h +2 -6
- data/ext/semian/types.h +4 -9
- data/lib/semian.rb +111 -17
- data/lib/semian/adapter.rb +5 -0
- data/lib/semian/circuit_breaker.rb +9 -7
- data/lib/semian/mysql2.rb +2 -0
- data/lib/semian/protected_resource.rb +37 -14
- data/lib/semian/redis.rb +8 -6
- data/lib/semian/resource.rb +27 -3
- data/lib/semian/simple_integer.rb +15 -0
- data/lib/semian/simple_sliding_window.rb +35 -10
- data/lib/semian/simple_state.rb +7 -0
- data/lib/semian/unprotected_resource.rb +12 -0
- data/lib/semian/version.rb +1 -1
- metadata +3 -2
@@ -4,6 +4,8 @@ module Semian
|
|
4
4
|
|
5
5
|
def_delegators :@state, :closed?, :open?, :half_open?
|
6
6
|
|
7
|
+
attr_reader :name
|
8
|
+
|
7
9
|
def initialize(name, exceptions:, success_threshold:, error_threshold:, error_timeout:, implementation:)
|
8
10
|
@name = name.to_sym
|
9
11
|
@success_count_threshold = success_threshold
|
@@ -41,7 +43,7 @@ module Semian
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def mark_failed(_error)
|
44
|
-
push_time(@errors
|
46
|
+
push_time(@errors)
|
45
47
|
if closed?
|
46
48
|
open if error_threshold_reached?
|
47
49
|
elsif half_open?
|
@@ -95,14 +97,14 @@ module Semian
|
|
95
97
|
end
|
96
98
|
|
97
99
|
def error_timeout_expired?
|
98
|
-
|
99
|
-
|
100
|
+
last_error_time = @errors.last
|
101
|
+
return false unless last_error_time
|
102
|
+
Time.at(last_error_time) + @error_timeout < Time.now
|
100
103
|
end
|
101
104
|
|
102
|
-
def push_time(window,
|
103
|
-
|
104
|
-
window
|
105
|
-
window << (time.to_f * 1000).to_i
|
105
|
+
def push_time(window, time: Time.now)
|
106
|
+
window.reject! { |err_time| err_time + @error_timeout < time.to_i }
|
107
|
+
window << time.to_i
|
106
108
|
end
|
107
109
|
|
108
110
|
def log_state_transition(new_state)
|
data/lib/semian/mysql2.rb
CHANGED
@@ -2,35 +2,58 @@ module Semian
|
|
2
2
|
class ProtectedResource
|
3
3
|
extend Forwardable
|
4
4
|
|
5
|
-
def_delegators :@
|
5
|
+
def_delegators :@bulkhead, :destroy, :count, :semid, :tickets, :registered_workers
|
6
6
|
def_delegators :@circuit_breaker, :reset, :mark_failed, :mark_success, :request_allowed?,
|
7
7
|
:open?, :closed?, :half_open?
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
attr_reader :bulkhead, :circuit_breaker, :name
|
10
|
+
|
11
|
+
def initialize(name, bulkhead, circuit_breaker)
|
12
|
+
@name = name
|
13
|
+
@bulkhead = bulkhead
|
11
14
|
@circuit_breaker = circuit_breaker
|
12
15
|
end
|
13
16
|
|
14
17
|
def destroy
|
15
|
-
@
|
16
|
-
@circuit_breaker.destroy
|
18
|
+
@bulkhead.destroy unless @bulkhead.nil?
|
19
|
+
@circuit_breaker.destroy unless @circuit_breaker.nil?
|
17
20
|
end
|
18
21
|
|
19
22
|
def acquire(timeout: nil, scope: nil, adapter: nil)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
acquire_circuit_breaker(scope, adapter) do
|
24
|
+
acquire_bulkhead(timeout, scope, adapter) do
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def acquire_circuit_breaker(scope, adapter)
|
33
|
+
if @circuit_breaker.nil?
|
34
|
+
yield self
|
35
|
+
else
|
36
|
+
@circuit_breaker.acquire do
|
37
|
+
yield self
|
29
38
|
end
|
30
39
|
end
|
31
40
|
rescue ::Semian::OpenCircuitError
|
32
41
|
Semian.notify(:circuit_open, self, scope, adapter)
|
33
42
|
raise
|
34
43
|
end
|
44
|
+
|
45
|
+
def acquire_bulkhead(timeout, scope, adapter)
|
46
|
+
if @bulkhead.nil?
|
47
|
+
yield self
|
48
|
+
else
|
49
|
+
@bulkhead.acquire(timeout: timeout) do
|
50
|
+
Semian.notify(:success, self, scope, adapter)
|
51
|
+
yield self
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue ::Semian::TimeoutError
|
55
|
+
Semian.notify(:busy, self, scope, adapter)
|
56
|
+
raise
|
57
|
+
end
|
35
58
|
end
|
36
59
|
end
|
data/lib/semian/redis.rb
CHANGED
@@ -15,17 +15,19 @@ class Redis
|
|
15
15
|
ResourceBusyError = Class.new(SemianError)
|
16
16
|
CircuitOpenError = Class.new(SemianError)
|
17
17
|
|
18
|
-
attr_reader :semian_resource
|
19
|
-
|
20
18
|
alias_method :_original_initialize, :initialize
|
21
19
|
|
22
20
|
def initialize(*args, &block)
|
23
21
|
_original_initialize(*args, &block)
|
24
22
|
|
25
|
-
# This
|
26
|
-
#
|
27
|
-
#
|
28
|
-
@
|
23
|
+
# This reference is necessary because during a `pipelined` block the client
|
24
|
+
# is replaced by an instance of `Redis::Pipeline` and there is no way to
|
25
|
+
# access the original client which references the Semian resource.
|
26
|
+
@original_client = client
|
27
|
+
end
|
28
|
+
|
29
|
+
def semian_resource
|
30
|
+
@original_client.semian_resource
|
29
31
|
end
|
30
32
|
|
31
33
|
def semian_identifier
|
data/lib/semian/resource.rb
CHANGED
@@ -2,19 +2,31 @@ module Semian
|
|
2
2
|
class Resource #:nodoc:
|
3
3
|
attr_reader :tickets, :name
|
4
4
|
|
5
|
-
|
5
|
+
class << Semian::Resource
|
6
|
+
# Ensure that there can only be one resource of a given type
|
7
|
+
def instance(*args)
|
8
|
+
Semian.resources[args.first] ||= new(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(name, tickets: nil, quota: nil, permissions: 0660, timeout: 0)
|
6
13
|
if Semian.semaphores_enabled?
|
7
|
-
initialize_semaphore(name, tickets, permissions, timeout) if respond_to?(:initialize_semaphore)
|
14
|
+
initialize_semaphore(name, tickets, quota, permissions, timeout) if respond_to?(:initialize_semaphore)
|
8
15
|
else
|
9
16
|
Semian.issue_disabled_semaphores_warning
|
10
17
|
end
|
11
18
|
@name = name
|
12
|
-
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset_registered_workers!
|
13
22
|
end
|
14
23
|
|
15
24
|
def destroy
|
16
25
|
end
|
17
26
|
|
27
|
+
def unregister_worker
|
28
|
+
end
|
29
|
+
|
18
30
|
def acquire(*)
|
19
31
|
yield self
|
20
32
|
end
|
@@ -23,8 +35,20 @@ module Semian
|
|
23
35
|
0
|
24
36
|
end
|
25
37
|
|
38
|
+
def tickets
|
39
|
+
0
|
40
|
+
end
|
41
|
+
|
42
|
+
def registered_workers
|
43
|
+
0
|
44
|
+
end
|
45
|
+
|
26
46
|
def semid
|
27
47
|
0
|
28
48
|
end
|
49
|
+
|
50
|
+
def key
|
51
|
+
'0x00000000'
|
52
|
+
end
|
29
53
|
end
|
30
54
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
1
3
|
module Semian
|
2
4
|
module Simple
|
3
5
|
class Integer #:nodoc:
|
@@ -20,4 +22,17 @@ module Semian
|
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
25
|
+
|
26
|
+
module ThreadSafe
|
27
|
+
class Integer < Simple::Integer
|
28
|
+
def initialize(*)
|
29
|
+
super
|
30
|
+
@lock = Mutex.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def increment(*)
|
34
|
+
@lock.synchronize { super }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
23
38
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
1
3
|
module Semian
|
2
4
|
module Simple
|
3
5
|
class SlidingWindow #:nodoc:
|
4
6
|
extend Forwardable
|
5
7
|
|
6
|
-
def_delegators :@window, :size, :
|
8
|
+
def_delegators :@window, :size, :last
|
7
9
|
attr_reader :max_size
|
8
10
|
|
9
11
|
# A sliding window is a structure that stores the most @max_size recent timestamps
|
@@ -15,28 +17,51 @@ module Semian
|
|
15
17
|
@window = []
|
16
18
|
end
|
17
19
|
|
18
|
-
def
|
19
|
-
|
20
|
-
@max_size = size
|
21
|
-
@window.shift while @window.size > @max_size
|
22
|
-
self
|
20
|
+
def reject!(&block)
|
21
|
+
@window.reject!(&block)
|
23
22
|
end
|
24
23
|
|
25
24
|
def push(value)
|
26
|
-
@
|
25
|
+
resize_to(@max_size - 1) # make room
|
27
26
|
@window << value
|
28
27
|
self
|
29
28
|
end
|
30
|
-
|
31
29
|
alias_method :<<, :push
|
32
30
|
|
33
31
|
def clear
|
34
32
|
@window.clear
|
35
33
|
self
|
36
34
|
end
|
35
|
+
alias_method :destroy, :clear
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def resize_to(size)
|
40
|
+
@window = @window.last(size) if @window.size >= size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ThreadSafe
|
46
|
+
class SlidingWindow < Simple::SlidingWindow
|
47
|
+
def initialize(*)
|
48
|
+
super
|
49
|
+
@lock = Mutex.new
|
50
|
+
end
|
51
|
+
|
52
|
+
# #size, #last, and #clear are not wrapped in a mutex. For the first two,
|
53
|
+
# the worst-case is a thread-switch at a timing where they'd receive an
|
54
|
+
# out-of-date value--which could happen with a mutex as well.
|
55
|
+
#
|
56
|
+
# As for clear, it's an all or nothing operation. Doesn't matter if we
|
57
|
+
# have the lock or not.
|
58
|
+
|
59
|
+
def reject!(*)
|
60
|
+
@lock.synchronize { super }
|
61
|
+
end
|
37
62
|
|
38
|
-
def
|
39
|
-
|
63
|
+
def push(*)
|
64
|
+
@lock.synchronize { super }
|
40
65
|
end
|
41
66
|
end
|
42
67
|
end
|
data/lib/semian/simple_state.rb
CHANGED
@@ -8,6 +8,10 @@ module Semian
|
|
8
8
|
@name = name
|
9
9
|
end
|
10
10
|
|
11
|
+
def registered_workers
|
12
|
+
0
|
13
|
+
end
|
14
|
+
|
11
15
|
def tickets
|
12
16
|
-1
|
13
17
|
end
|
@@ -51,5 +55,13 @@ module Semian
|
|
51
55
|
|
52
56
|
def mark_success
|
53
57
|
end
|
58
|
+
|
59
|
+
def bulkhead
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def circuit_breaker
|
64
|
+
nil
|
65
|
+
end
|
54
66
|
end
|
55
67
|
end
|
data/lib/semian/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
8
8
|
- Simon Eskildsen
|
9
|
+
- Dale Hamel
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2017-
|
13
|
+
date: 2017-06-19 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: rake-compiler
|