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

#close(circuit) ⇒ Boolean

Mark a circuit as closed

Returns:

  • (Boolean)

    True if the circuit transitioned from open to closed

See Also:


164
165
166
167
168
169
170
# File 'lib/faulty/storage/redis.rb', line 164

def close(circuit)
  redis do |r|
    closed = compare_and_set(r, state_key(circuit), ['open'], 'closed', ex: options.circuit_ttl)
    r.del(entries_key(circuit)) if closed
    closed
  end
end

#entry(circuit, time, success) ⇒ Array<Array>

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

Returns:

  • (Array<Array>)

    An array of the new history tuples after adding the new entry, see #history

See Also:


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

def entry(circuit, time, success)
  key = entries_key(circuit)
  result = pipe do |r|
    r.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)
  end
  map_entries(result.last)
end

#fault_tolerant?true

Redis storage is not fault-tolerant

Returns:

  • (true)

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

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


237
238
239
240
# File 'lib/faulty/storage/redis.rb', line 237

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

#listArray<String>

List all unexpired circuits

Returns:

  • (Array<String>)

245
246
247
# File 'lib/faulty/storage/redis.rb', line 245

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:


179
180
181
# File 'lib/faulty/storage/redis.rb', line 179

def lock(circuit, state)
  redis { |r| r.set(lock_key(circuit), 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:


140
141
142
143
144
145
146
# File 'lib/faulty/storage/redis.rb', line 140

def open(circuit, opened_at)
  redis do |r|
    opened = compare_and_set(r, state_key(circuit), ['closed', nil], 'open', ex: options.circuit_ttl)
    r.set(opened_at_key(circuit), opened_at, ex: options.circuit_ttl) if opened
    opened
  end
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:


153
154
155
156
157
# File 'lib/faulty/storage/redis.rb', line 153

def reopen(circuit, opened_at, previous_opened_at)
  redis do |r|
    compare_and_set(r, opened_at_key(circuit), [previous_opened_at.to_s], opened_at, ex: options.circuit_ttl)
  end
end

#reset(circuit) ⇒ void

This method returns an undefined value.

Reset a circuit

Parameters:

  • circuit (Circuit)

    The circuit to unlock

See Also:


197
198
199
200
201
202
203
204
205
206
207
# File 'lib/faulty/storage/redis.rb', line 197

def reset(circuit)
  pipe do |r|
    r.del(
      entries_key(circuit),
      opened_at_key(circuit),
      lock_key(circuit),
      options_key(circuit)
    )
    r.set(state_key(circuit), '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

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


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/faulty/storage/redis.rb', line 214

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

  Faulty::Status.from_entries(
    map_entries(futures[:entries].value),
    state: futures[:state].value&.to_sym || :closed,
    lock: futures[:lock].value&.to_sym,
    opened_at: futures[:opened_at].value ? futures[:opened_at].value.to_i : nil,
    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:


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

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