semian 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, duration: @error_timeout)
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
- time_ms = @errors.last
99
- time_ms && (Time.at(time_ms / 1000) + @error_timeout < Time.now)
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, duration:, time: Time.now)
103
- # The sliding window stores the integer amount of milliseconds since epoch as a timestamp
104
- window.shift while window.first && window.first / 1000 + duration < time.to_i
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)
@@ -24,6 +24,8 @@ module Semian
24
24
  /Lost connection to MySQL server/i,
25
25
  /MySQL server has gone away/i,
26
26
  /Too many connections/i,
27
+ /closed MySQL connection/i,
28
+ /MySQL client is not connected/i,
27
29
  )
28
30
 
29
31
  ResourceBusyError = ::Mysql2::ResourceBusyError
@@ -2,35 +2,58 @@ module Semian
2
2
  class ProtectedResource
3
3
  extend Forwardable
4
4
 
5
- def_delegators :@resource, :destroy, :count, :semid, :tickets, :name
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
- def initialize(resource, circuit_breaker)
10
- @resource = resource
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
- @resource.destroy
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
- @circuit_breaker.acquire do
21
- begin
22
- @resource.acquire(timeout: timeout) do
23
- Semian.notify(:success, self, scope, adapter)
24
- yield self
25
- end
26
- rescue ::Semian::TimeoutError
27
- Semian.notify(:busy, self, scope, adapter)
28
- raise
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
@@ -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 alias is necessary because during a `pipelined` block
26
- # the client is replaced by an instance of `Redis::Pipeline` and there is
27
- # no way to access the original client.
28
- @semian_resource = client.semian_resource
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
@@ -2,19 +2,31 @@ module Semian
2
2
  class Resource #:nodoc:
3
3
  attr_reader :tickets, :name
4
4
 
5
- def initialize(name, tickets:, permissions: 0660, timeout: 0)
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
- @tickets = tickets
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, :pop, :shift, :first, :last
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 resize_to(size)
19
- raise ArgumentError.new('size must be larger than 0') if size < 1
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
- @window.shift while @window.size >= @max_size
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 destroy
39
- clear
63
+ def push(*)
64
+ @lock.synchronize { super }
40
65
  end
41
66
  end
42
67
  end
@@ -40,4 +40,11 @@ module Semian
40
40
  end
41
41
  end
42
42
  end
43
+
44
+ module ThreadSafe
45
+ class State < Simple::State
46
+ # These operations are already safe for a threaded environment since it's
47
+ # a simple assignment.
48
+ end
49
+ end
43
50
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Semian
2
- VERSION = '0.6.2'
2
+ VERSION = '0.7.0'
3
3
  end
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.6.2
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-02-16 00:00:00.000000000 Z
13
+ date: 2017-06-19 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: rake-compiler