Class: Faulty::Status
- Inherits:
-
Struct
- Object
- Struct
- Faulty::Status
- Includes:
- ImmutableOptions
- Defined in:
- lib/faulty/status.rb,
lib/faulty/status.rb
Overview
The status of a circuit
Includes information like the state and locks. Also calculates whether a circuit can be run, or if it has failed a threshold.
Constant Summary collapse
- STATES =
The allowed state values
%i[ open closed ].freeze
- LOCKS =
The allowed lock values
%i[ open closed ].freeze
Instance Attribute Summary collapse
-
#current_time ⇒ Float
readonly
The point in time captured when this status was built.
-
#failure_rate ⇒ Float
readonly
A number from 0 to 1 representing the percentage of failures for the circuit.
-
#lock ⇒ :open, ...
readonly
If the circuit is locked, the state that it is locked in.
-
#opened_at ⇒ Float?
readonly
If the circuit is open, the timestamp (current_time) that it was opened.
-
#options ⇒ Circuit::Options
readonly
The options for the circuit.
-
#reserved_at ⇒ Float?
readonly
If a half-open test run was reserved, the timestamp (current_time) of that reservation.
-
#sample_size ⇒ Integer
readonly
The number of samples used to calculate the failure rate.
-
#state ⇒ :open, :closed
readonly
The stored circuit state.
-
#stub ⇒ Boolean
readonly
True if this status is a stub and not calculated from the storage backend.
Class Method Summary collapse
-
.from_entries(entries, **hash) ⇒ Status
Create a new
Statusfrom a list of circuit runs.
Instance Method Summary collapse
-
#can_run? ⇒ Boolean
Whether the circuit can be run.
-
#closed? ⇒ Boolean
Whether the circuit is closed.
- #defaults ⇒ Object
-
#fails_threshold? ⇒ Boolean
Whether the circuit fails the sample size and rate thresholds.
- #finalize ⇒ Object
-
#half_open? ⇒ Boolean
Whether the circuit is half-open.
-
#initialize(hash) ⇒ Status
constructor
A new instance of Status.
-
#locked_closed? ⇒ Boolean
Whether the circuit is locked closed.
-
#locked_open? ⇒ Boolean
Whether the circuit is locked open.
-
#open? ⇒ Boolean
Whether the circuit is open.
- #required ⇒ Object
-
#reserved? ⇒ Boolean
Whether a half-open test run is currently reserved.
Methods included from ImmutableOptions
Constructor Details
#initialize(hash) ⇒ Status
Returns a new instance of Status.
65 66 67 68 |
# File 'lib/faulty/status.rb', line 65 def initialize(hash, &) @current_time = hash[:current_time] || Faulty.current_time super(hash.except(:current_time), &) end |
Instance Attribute Details
#current_time ⇒ Float (readonly)
Returns The point in time captured when this status was built.
All predicates (open?, half_open?, reserved?) reason about
this same instant so they are mutually consistent. Held as an
instance variable rather than a struct field so it does not leak
into to_h, ==, or members — those should reflect persisted
circuit state, not a transient predicate-consistency snapshot.
63 64 65 |
# File 'lib/faulty/status.rb', line 63 def current_time @current_time end |
#failure_rate ⇒ Float (readonly)
Returns A number from 0 to 1 representing the percentage of failures for the circuit. For exmaple 0.5 represents a 50% failure rate.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#lock ⇒ :open, ... (readonly)
Returns If the circuit is locked, the state that
it is locked in. Default nil.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#opened_at ⇒ Float? (readonly)
Returns If the circuit is open, the timestamp (Faulty.current_time)
that it was opened. This is not necessarily reset when the circuit
is closed. Default nil.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#options ⇒ Circuit::Options (readonly)
Returns The options for the circuit.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#reserved_at ⇒ Float? (readonly)
Returns If a half-open test run was reserved, the
timestamp (Faulty.current_time) of that reservation. Cleared when
the circuit is closed.
Not reset by Faulty::Storage::Interface#reopen; the value naturally expires
via cool_down. Default nil.
Only meaningful when #state is :open. If a backend race or bug
produces an inconsistent shape (state == :closed with a non-nil
reserved_at), it is normalized to nil at construction.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#sample_size ⇒ Integer (readonly)
Returns The number of samples used to calculate the failure rate.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#state ⇒ :open, :closed (readonly)
Returns The stored circuit state. This is always open
or closed. Half-open is calculated from the current time. For that
reason, calling state directly should be avoided. Instead use the
status methods #open?, #closed?, and #half_open?.
Default :closed.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
#stub ⇒ Boolean (readonly)
True if this status is a stub and not calculated from
the storage backend. Used by Faulty::Storage::FaultTolerantProxy when
returning the status for an offline storage backend. Default false.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/faulty/status.rb', line 43 Status = Struct.new( :state, :lock, :opened_at, :reserved_at, :failure_rate, :sample_size, :options, :stub ) |
Class Method Details
.from_entries(entries, **hash) ⇒ Status
Create a new Status from a list of circuit runs
For storage backends that store entries, this automatically calculates failure_rate and sample size.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/faulty/status.rb', line 92 def self.from_entries(entries, **hash) current_time = Faulty.current_time window_start = current_time - hash[:options].evaluation_window size = entries.size i = 0 failures = 0 sample_size = 0 # This is a hot loop, and while is slightly faster than each while i < size time, success = entries[i] i += 1 next unless time > window_start sample_size += 1 failures += 1 unless success end new(hash.merge( sample_size: sample_size, failure_rate: sample_size.zero? ? 0.0 : failures.to_f / sample_size, current_time: current_time )) end |
Instance Method Details
#can_run? ⇒ Boolean
Whether the circuit can be run
Takes the circuit state, locks and cooldown into account. Locks are
operator overrides and take precedence over both state and reservation,
so a locked_closed? circuit always runs and a locked_open? circuit
never runs.
188 189 190 191 192 193 194 |
# File 'lib/faulty/status.rb', line 188 def can_run? return false if locked_open? return true if locked_closed? return false if reserved? closed? || half_open? end |
#closed? ⇒ Boolean
Whether the circuit is closed
This is mutually exclusive with #open? and #half_open?
131 132 133 |
# File 'lib/faulty/status.rb', line 131 def closed? state == :closed end |
#defaults ⇒ Object
225 226 227 228 229 230 231 232 |
# File 'lib/faulty/status.rb', line 225 def defaults { state: :closed, failure_rate: 0.0, sample_size: 0, stub: false } end |
#fails_threshold? ⇒ Boolean
Whether the circuit fails the sample size and rate thresholds
199 200 201 202 203 |
# File 'lib/faulty/status.rb', line 199 def fails_threshold? return false if sample_size < .sample_threshold failure_rate >= .rate_threshold end |
#finalize ⇒ Object
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/faulty/status.rb', line 205 def finalize raise ArgumentError, "state must be a symbol in #{self.class}::STATES" unless STATES.include?(state) unless lock.nil? || LOCKS.include?(lock) raise ArgumentError, "lock must be a symbol in #{self.class}::LOCKS or nil" end raise ArgumentError, 'opened_at is required if state is open' if state == :open && opened_at.nil? # `reserved_at` is only meaningful while the circuit is open. Backends # are expected to clear it on close, but if a brief race or backend bug # leaves a stale value paired with `state == :closed`, normalize it # here so downstream code can rely on the invariant without checking # `state` first. Sanitizing rather than raising avoids turning a # transient backend inconsistency into a production crash. self.reserved_at = nil if state == :closed && !reserved_at.nil? end |
#half_open? ⇒ Boolean
140 141 142 |
# File 'lib/faulty/status.rb', line 140 def half_open? state == :open && opened_at + .cool_down <= current_time end |
#locked_closed? ⇒ Boolean
Whether the circuit is locked closed
154 155 156 |
# File 'lib/faulty/status.rb', line 154 def locked_closed? lock == :closed end |
#locked_open? ⇒ Boolean
Whether the circuit is locked open
147 148 149 |
# File 'lib/faulty/status.rb', line 147 def locked_open? lock == :open end |
#open? ⇒ Boolean
Whether the circuit is open
This is mutually exclusive with #closed? and #half_open?
122 123 124 |
# File 'lib/faulty/status.rb', line 122 def open? state == :open && opened_at + .cool_down > current_time end |
#required ⇒ Object
221 222 223 |
# File 'lib/faulty/status.rb', line 221 def required %i[state failure_rate sample_size options stub] end |
#reserved? ⇒ Boolean
Whether a half-open test run is currently reserved
Process-agnostic: returns true whenever an unexpired reservation exists
on this circuit, regardless of who made it. The "did someone else reserve
this?" interpretation only applies when this predicate is read on a
status snapshot taken before the caller attempts Faulty::Storage::Interface#reserve;
a caller introspecting their own status after a successful reserve will
also see true here.
The reservation expires after cool_down to handle the case where the
process that made the reservation crashes before resolving the circuit.
Side effect: a legitimately-slow test run that exceeds cool_down
loses exclusivity (another process may reserve and run concurrently).
See the "How it Works" section of the README for the full trade-off.
174 175 176 177 178 |
# File 'lib/faulty/status.rb', line 174 def reserved? return false unless reserved_at state == :open && reserved_at + .cool_down > current_time end |