Class: Faulty::Status

Inherits:
Struct
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ImmutableOptions

#dup_with, #setup

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_timeFloat (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.

Returns:

  • (Float)

    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_rateFloat (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.

Returns:

  • (Float)

    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.

Returns:

  • (:open, :closed, nil)

    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_atFloat? (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.

Returns:

  • (Float, nil)

    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
)

#optionsCircuit::Options (readonly)

Returns The options for the circuit.

Returns:



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_atFloat? (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.

Returns:

  • (Float, nil)

    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_sizeInteger (readonly)

Returns The number of samples used to calculate the failure rate.

Returns:

  • (Integer)

    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.

Returns:

  • (:open, :closed)

    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
)

#stubBoolean (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.

Returns:

  • (Boolean)

    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.

Parameters:

  • entries (Array<Array>)

    An array of entry tuples. See Circuit#history for details

  • hash (Hash)

    The status attributes minus failure_rate and sample_size

Returns:



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.

Returns:

  • (Boolean)

    True if the circuit can be run



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?

Returns:

  • (Boolean)

    True if closed



131
132
133
# File 'lib/faulty/status.rb', line 131

def closed?
  state == :closed
end

#defaultsObject



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

Returns:

  • (Boolean)

    True if the circuit fails the thresholds



199
200
201
202
203
# File 'lib/faulty/status.rb', line 199

def fails_threshold?
  return false if sample_size < options.sample_threshold

  failure_rate >= options.rate_threshold
end

#finalizeObject

Raises:

  • (ArgumentError)


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

Whether the circuit is half-open

This is mutually exclusive with #open? and #closed?

Returns:

  • (Boolean)

    True if half-open



140
141
142
# File 'lib/faulty/status.rb', line 140

def half_open?
  state == :open && opened_at + options.cool_down <= current_time
end

#locked_closed?Boolean

Whether the circuit is locked closed

Returns:

  • (Boolean)

    True if 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

Returns:

  • (Boolean)

    True if 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?

Returns:

  • (Boolean)

    True if open



122
123
124
# File 'lib/faulty/status.rb', line 122

def open?
  state == :open && opened_at + options.cool_down > current_time
end

#requiredObject



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.

Returns:

  • (Boolean)

    True if a reservation is in effect



174
175
176
177
178
# File 'lib/faulty/status.rb', line 174

def reserved?
  return false unless reserved_at

  state == :open && reserved_at + options.cool_down > current_time
end