semian 0.6.2 → 0.7.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.
@@ -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