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, &block)
  @options = Options.new(options, &block)

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



275
276
277
# File 'lib/faulty/storage/redis.rb', line 275

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
# 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))
  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 (Integer)

    The unix timestamp for the run

  • 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)


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

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:



252
253
254
255
# File 'lib/faulty/storage/redis.rb', line 252

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>)


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

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:



189
190
191
# File 'lib/faulty/storage/redis.rb', line 189

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 (Integer)

    The timestmp the circuit was opened at

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 (Integer)

    The timestmp the circuit was opened at

  • previous_opened_at (Integer)

    The last known value of opened_at. Can be used to comare-and-set.

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

#reset(circuit) ⇒ void

This method returns an undefined value.

Reset a circuit

Parameters:

  • circuit (Circuit)

    The circuit to unlock

See Also:



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/faulty/storage/redis.rb', line 207

def reset(circuit)
  name = circuit.is_a?(Circuit) ? circuit.name : circuit
  pipe do |r|
    r.del(
      entries_key(name),
      opened_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:



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/faulty/storage/redis.rb', line 225

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[:entries] = r.lrange(entries_key(circuit.name), 0, -1)
  end

  state = futures[:state].value&.to_sym || :closed
  opened_at = futures[:opened_at].value ? Float(futures[:opened_at].value) : nil
  opened_at = Faulty.current_time - options.circuit_ttl if state == :open && opened_at.nil?

  Faulty::Status.from_entries(
    map_entries(futures[:entries].value),
    state: state,
    lock: futures[:lock].value&.to_sym,
    opened_at: opened_at,
    options: circuit.options
  )
end

#unlock(circuit) ⇒ void

This method returns an undefined value.

Unlock a circuit

Parameters:

  • circuit (Circuit)

    The circuit to unlock

See Also:



198
199
200
# File 'lib/faulty/storage/redis.rb', line 198

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