Class: Faulty::Storage::Redis

Inherits:
Object
  • Object
show all
Defined in:
lib/faulty/storage/redis.rb

Overview

A storage backend for storing circuit state in Redis.

When using this or any networked backend, be sure to evaluate the risk, and set conservative timeouts so that the circuit storage does not cause cascading failures in your application when evaluating circuits. Always wrap this backend with a FaultTolerantProxy to limit the effect of these types of events.

Defined Under Namespace

Classes: Options

Constant Summary collapse

ENTRY_SEPARATOR =

Separates the time/status for history entry strings

':'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**options) {|Options| ... } ⇒ Redis

Returns a new instance of Redis.

Parameters:

  • options (Hash)

    Attributes for Options

Yields:

  • (Options)

    For setting options in a block



83
84
85
86
87
88
89
90
# File 'lib/faulty/storage/redis.rb', line 83

def initialize(**options, &)
  @options = Options.new(options, &)

  # Ensure JSON is available since we don't explicitly require it
  JSON # rubocop:disable Lint/Void

  check_client_options!
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



16
17
18
# File 'lib/faulty/storage/redis.rb', line 16

def options
  @options
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Reset all circuits

This does not empty the list of circuits as returned by #list. This is because that would be a thread-usafe operation that could result in circuits not being in the list.

This implmenentation resets circuits individually, and will be very slow for large numbers of circuits. It should not be used in production code.



281
282
283
# File 'lib/faulty/storage/redis.rb', line 281

def clear
  list.each { |c| reset(c) }
end

#close(circuit) ⇒ Boolean

Mark a circuit as closed

Returns:

  • (Boolean)

    True if the circuit transitioned from open to closed

See Also:



171
172
173
174
175
176
177
178
179
180
181
# File 'lib/faulty/storage/redis.rb', line 171

def close(circuit)
  key = state_key(circuit.name)
  ex = options.circuit_ttl
  result = watch_exec(key, ['open']) do |m|
    m.set(key, 'closed', ex: ex)
    m.del(entries_key(circuit.name))
    m.del(reserved_at_key(circuit.name))
  end

  result && result[0] == 'OK'
end

#entry(circuit, time, success, status) ⇒ Status?

Add an entry to storage

Parameters:

  • circuit (Circuit)

    The circuit that ran

  • time (Float)

    The unix timestamp for the run, from Faulty.current_time

  • success (Boolean)

    True if the run succeeded

  • status (Status, nil)

    The previous status. If given, this method must return an updated status object from the new entry data.

Returns:

  • (Status, nil)

    If status is not nil, the updated status object.

See Also:



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/faulty/storage/redis.rb', line 122

def entry(circuit, time, success, status)
  key = entries_key(circuit.name)
  result = pipe do |r|
    r.call([:sadd, list_key, circuit.name])
    r.expire(list_key, options.circuit_ttl + options.list_granularity) if options.circuit_ttl
    r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
    r.ltrim(key, 0, options.max_sample_size - 1)
    r.expire(key, options.sample_ttl) if options.sample_ttl
    r.lrange(key, 0, -1) if status
  end

  Status.from_entries(map_entries(result.last), **status.to_h) if status
end

#fault_tolerant?true

Redis storage is not fault-tolerant

Returns:

  • (true)


288
289
290
# File 'lib/faulty/storage/redis.rb', line 288

def fault_tolerant?
  false
end

#get_options(circuit) ⇒ Hash

Get the options stored for circuit

Returns:

  • (Hash)

    A hash of the options stored by #set_options. The keys must be symbols.

See Also:



97
98
99
100
101
102
# File 'lib/faulty/storage/redis.rb', line 97

def get_options(circuit)
  json = redis { |r| r.get(options_key(circuit.name)) }
  return if json.nil?

  JSON.parse(json, symbolize_names: true)
end

#history(circuit) ⇒ Array<Array>

Get the circuit history up to max_sample_size

Parameters:

  • circuit (Circuit)

    The circuit to get history for

Returns:

  • (Array<Array>)

    An array of history tuples

See Also:



258
259
260
261
# File 'lib/faulty/storage/redis.rb', line 258

def history(circuit)
  entries = redis { |r| r.lrange(entries_key(circuit.name), 0, -1) }
  map_entries(entries).reverse
end

#listArray<String>

List all unexpired circuits

Returns:

  • (Array<String>)


266
267
268
# File 'lib/faulty/storage/redis.rb', line 266

def list
  redis { |r| r.sunion(*all_list_keys) }
end

#lock(circuit, state) ⇒ void

This method returns an undefined value.

Lock a circuit open or closed

The circuit_ttl does not apply to locks

Parameters:

  • circuit (Circuit)

    The circuit to lock

  • state (:open, :closed)

    The state to lock the circuit in

See Also:



203
204
205
# File 'lib/faulty/storage/redis.rb', line 203

def lock(circuit, state)
  redis { |r| r.set(lock_key(circuit.name), state) }
end

#open(circuit, opened_at) ⇒ Boolean

Mark a circuit as open

Parameters:

  • circuit (Circuit)

    The circuit to open

  • opened_at (Float)

    The timestamp the circuit was opened at, from Faulty.current_time

Returns:

  • (Boolean)

    True if the circuit transitioned from closed to open

See Also:



141
142
143
144
145
146
147
148
149
150
# File 'lib/faulty/storage/redis.rb', line 141

def open(circuit, opened_at)
  key = state_key(circuit.name)
  ex = options.circuit_ttl
  result = watch_exec(key, ['closed', nil]) do |m|
    m.set(key, 'open', ex: ex)
    m.set(opened_at_key(circuit.name), opened_at, ex: ex)
  end

  result && result[0] == 'OK'
end

#reopen(circuit, opened_at, previous_opened_at) ⇒ Boolean

Mark a circuit as reopened

Parameters:

  • circuit (Circuit)

    The circuit to reopen

  • opened_at (Float)

    The timestamp the circuit was opened at, from Faulty.current_time

  • previous_opened_at (Float)

    The last known value of opened_at. Can be used to compare-and-set. Always non-nil — Circuit#failure! only enters the reopen branch when status.half_open? is true, which requires non-nil opened_at. Unlike previous_reserved_at on #reserve, there is no legitimate "no prior value" call path to reopen, so backends may treat this parameter as required and are not expected to handle nil.

Returns:

  • (Boolean)

    True if the opened_at time was updated

See Also:



157
158
159
160
161
162
163
164
# File 'lib/faulty/storage/redis.rb', line 157

def reopen(circuit, opened_at, previous_opened_at)
  key = opened_at_key(circuit.name)
  result = watch_exec(key, [previous_opened_at.to_s]) do |m|
    m.set(key, opened_at, ex: options.circuit_ttl)
  end

  result && result[0] == 'OK'
end

#reserve(circuit, reserved_at, previous_reserved_at) ⇒ Boolean

Reserve an exclusive run for this circuit

Parameters:

  • circuit (Circuit)

    The circuit to reserve

  • reserved_at (Float)

    The timestamp of this reservation, from Faulty.current_time

  • previous_reserved_at (Float, nil)

    The last known value of reserved_at, or nil for the first reservation in a new open cycle. Can be used to compare-and-set.

Returns:

  • (Boolean)

    True if the caller may proceed with the half-open test run; false if another caller already holds the reservation.

See Also:



188
189
190
191
192
193
194
# File 'lib/faulty/storage/redis.rb', line 188

def reserve(circuit, reserved_at, previous_reserved_at)
  key = reserved_at_key(circuit.name)
  result = watch_exec(key, [previous_reserved_at&.to_s]) do |m|
    m.set(key, reserved_at, ex: options.circuit_ttl)
  end
  result && result[0] == 'OK'
end

#reset(circuit) ⇒ void

This method returns an undefined value.

Reset a circuit

Parameters:

  • circuit (Circuit)

    The circuit to unlock

See Also:



221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/faulty/storage/redis.rb', line 221

def reset(circuit)
  name = circuit.is_a?(Circuit) ? circuit.name : circuit
  pipe do |r|
    r.del(
      entries_key(name),
      opened_at_key(name),
      reserved_at_key(name),
      lock_key(name),
      options_key(name)
    )
    r.set(state_key(name), 'closed', ex: options.circuit_ttl)
  end
end

#set_options(circuit, stored_options) ⇒ void

This method returns an undefined value.

Store the options for a circuit

These will be serialized as JSON

Parameters:

  • circuit (Circuit)

    The circuit to set options for

  • stored_options (Hash<Symbol, Object>)

    A hash of symbol option names to circuit options. These option values are guranteed to be primive values.

See Also:



111
112
113
114
115
# File 'lib/faulty/storage/redis.rb', line 111

def set_options(circuit, stored_options)
  redis do |r|
    r.set(options_key(circuit.name), JSON.dump(stored_options), ex: options.circuit_ttl)
  end
end

#status(circuit) ⇒ Status

Get the status of a circuit

Parameters:

  • circuit (Circuit)

    The circuit to get status for

Returns:

  • (Status)

    The current status

See Also:



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/faulty/storage/redis.rb', line 240

def status(circuit)
  futures = {}
  pipe do |r|
    futures[:state] = r.get(state_key(circuit.name))
    futures[:lock] = r.get(lock_key(circuit.name))
    futures[:opened_at] = r.get(opened_at_key(circuit.name))
    futures[:reserved_at] = r.get(reserved_at_key(circuit.name))
    futures[:entries] = r.lrange(entries_key(circuit.name), 0, -1)
  end

  build_status(circuit, futures)
end

#unlock(circuit) ⇒ void

This method returns an undefined value.

Unlock a circuit

Parameters:

  • circuit (Circuit)

    The circuit to unlock

See Also:



212
213
214
# File 'lib/faulty/storage/redis.rb', line 212

def unlock(circuit)
  redis { |r| r.del(lock_key(circuit.name)) }
end