Class: ResilientCall::Circuit

Inherits:
Object
  • Object
show all
Defined in:
lib/resilient_call/circuit.rb

Overview

Holds the state of a single named circuit and manages its transitions. The mutable state lives in the injected storage (memory by default, Redis for multi-process setups); this class owns only the transition rules.

Thread-safe within a process: every read-modify-write runs under an internal Mutex, so concurrent calls on the same instance stay consistent. Across processes, consistency is bounded by the storage backend (see Storage::Redis).

closed --(threshold failures)--> open --(reset_timeout elapsed)--> half_open
^                                                                     |
+------------------------- record_success! -------------------------+
half_open --(record_failure!)--> open  (restarts the timer)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, threshold: 5, reset_timeout: 60, storage: nil) ⇒ Circuit

Returns a new instance of Circuit.



21
22
23
24
25
26
27
28
# File 'lib/resilient_call/circuit.rb', line 21

def initialize(name, threshold: 5, reset_timeout: 60, storage: nil)
  @name          = name
  @threshold     = threshold
  @reset_timeout = reset_timeout
  @storage       = storage || Storage::Memory.new
  @last_failure  = nil
  @mutex         = Mutex.new
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



19
20
21
# File 'lib/resilient_call/circuit.rb', line 19

def name
  @name
end

#reset_timeoutObject (readonly)

Returns the value of attribute reset_timeout.



19
20
21
# File 'lib/resilient_call/circuit.rb', line 19

def reset_timeout
  @reset_timeout
end

#thresholdObject (readonly)

Returns the value of attribute threshold.



19
20
21
# File 'lib/resilient_call/circuit.rb', line 19

def threshold
  @threshold
end

Instance Method Details

#allow_request?Boolean

Whether a request may pass right now. An :open circuit whose reset_timeout has elapsed transitions to :half_open and lets a single probe through.

Returns:

  • (Boolean)


51
52
53
54
55
56
57
58
59
60
# File 'lib/resilient_call/circuit.rb', line 51

def allow_request?
  @mutex.synchronize do
    state = read_state

    case state[:status]
    when :closed, :half_open then true
    when :open               then attempt_half_open(state)
    end
  end
end

#failure_countObject



34
35
36
# File 'lib/resilient_call/circuit.rb', line 34

def failure_count
  read_field(:failure_count)
end

#last_failureObject

The full Exception object captured most recently in this process. State shared through Redis only carries its message/class, so a fresh process reading an open circuit sees nil here — use CircuitOpenError for details.



41
42
43
# File 'lib/resilient_call/circuit.rb', line 41

def last_failure
  @mutex.synchronize { @last_failure }
end

#opened_atObject



45
46
47
# File 'lib/resilient_call/circuit.rb', line 45

def opened_at
  read_field(:opened_at)
end

#record_failure!(error) ⇒ Object



71
72
73
74
75
76
77
78
79
80
# File 'lib/resilient_call/circuit.rb', line 71

def record_failure!(error)
  transition do |state|
    @last_failure = error
    state[:failure_count]       += 1
    state[:last_failure_message] = error.message
    state[:last_failure_class]   = error.class.name

    trip_open!(state) if should_open?(state)
  end
end

#record_success!Object



62
63
64
65
66
67
68
69
# File 'lib/resilient_call/circuit.rb', line 62

def record_success!
  transition do |state|
    case state[:status]
    when :half_open then state.merge!(status: :closed, failure_count: 0)
    when :closed    then state[:failure_count] = 0
    end
  end
end

#reset!Object



82
83
84
85
86
87
# File 'lib/resilient_call/circuit.rb', line 82

def reset!
  @mutex.synchronize do
    @last_failure = nil
    @storage.reset(@name)
  end
end

#stateObject



30
31
32
# File 'lib/resilient_call/circuit.rb', line 30

def state
  read_field(:status)
end

#update_config(threshold: nil, reset_timeout: nil) ⇒ Object

Lets the entry point inject configured thresholds onto a circuit that was created lazily by the registry with default values.



91
92
93
94
95
96
# File 'lib/resilient_call/circuit.rb', line 91

def update_config(threshold: nil, reset_timeout: nil)
  @mutex.synchronize do
    @threshold     = threshold     unless threshold.nil?
    @reset_timeout = reset_timeout unless reset_timeout.nil?
  end
end