Class: Stoplight::Infrastructure::Redis::Storage::State

Inherits:
Object
  • Object
show all
Includes:
Domain::_StateStore
Defined in:
lib/stoplight/infrastructure/redis/storage/state.rb,
sig/_private/stoplight/infrastructure/redis/storage/state.rbs

Overview

Note:

Thread safety is guaranteed by Redis's single-threaded execution model and the use of Lua scripts for atomic multistep operations.

Redis-backed state storage for a single circuit breaker.

Manages circuit breaker state transitions using Redis hashes and Lua scripts for atomic operations. Ensures notification deduplication across distributed processes - when multiple processes detect the same circuit condition, only one will receive true from transition methods.

All state is stored in a single Redis hash with fields:

  • locked_state: forced lock (UNLOCKED, LOCKED_GREEN, LOCKED_RED)
  • breached_at: timestamp (float) when circuit opened
  • recovery_scheduled_after: timestamp (float) when recovery probe allowed
  • recovery_started_at: timestamp (float) when recovery probe began

Examples:

Basic usage

state = State.new(
  clock: SystemClock.new,
  redis: Redis.new,
  scripting: Scripting.new(redis:),
  key_space: KeySpace.build(light_name: "payments", system_name: "main"),
  cool_off_time: 60
)

# Multiple processes may call this concurrently
if state.transition_to_color(Color::RED)
  # Only one process reaches here - send notification
  notifier.notify("payments", :opened)
end

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(clock:, redis:, scripting:, key_space:, cool_off_time:) ⇒ State

Returns a new instance of State.



42
43
44
45
46
47
48
49
50
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 42

def initialize(clock:, redis:, scripting:, key_space:, cool_off_time:)
  @redis = redis
  @scripting = scripting
  @key_space = key_space
  @clock = clock
  @cool_off_time = cool_off_time

  @state_key = key_space.key(:state)
end

Instance Attribute Details

#clockDomain::_Clock (readonly)

Returns the value of attribute clock.

Returns:



97
98
99
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 97

def clock
  @clock
end

#cool_off_timetimestamp (readonly)

Returns the value of attribute cool_off_time.

Returns:

  • (timestamp)


98
99
100
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 98

def cool_off_time
  @cool_off_time
end

#key_spaceKeySpace (readonly)

Returns the value of attribute key_space.

Returns:



96
97
98
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 96

def key_space
  @key_space
end

#redisredis (readonly)

Returns the value of attribute redis.

Returns:



94
95
96
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 94

def redis
  @redis
end

#scriptingScripting (readonly)

Returns the value of attribute scripting.

Returns:



95
96
97
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 95

def scripting
  @scripting
end

#state_keyString (readonly)

Returns the value of attribute state_key.

Returns:

  • (String)


99
100
101
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 99

def state_key
  @state_key
end

Instance Method Details

#clearObject



73
74
75
76
77
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 73

def clear
  redis.with do |client|
    client.del(state_key)
  end
end

#set_state(state) ⇒ Object



52
53
54
55
56
57
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 52

def set_state(state)
  redis.with do |client|
    client.hset(state_key, "locked_state", state)
  end
  state
end

#state_snapshotObject



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 59

def state_snapshot
  breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw = redis.with do |client|
    client.hmget(state_key, :breached_at, :locked_state, :recovery_scheduled_after, :recovery_started_at)
  end

  Domain::StateSnapshot.new(
    breached_at: breached_at_raw && clock.at(breached_at_raw.to_f),
    locked_state: locked_state || Stoplight::State::UNLOCKED,
    recovery_scheduled_after: recovery_scheduled_after_raw && clock.at(recovery_scheduled_after_raw.to_f),
    recovery_started_at: recovery_started_at_raw && clock.at(recovery_started_at_raw.to_f),
    time: clock.current_time
  )
end

#transition_to_color(color) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 79

def transition_to_color(color)
  case color
  when Color::GREEN
    transition_to_green
  when Color::YELLOW
    transition_to_yellow
  when Color::RED
    transition_to_red
  else
    raise ArgumentError, "Invalid color: #{color}"
  end
end

#transition_to_greenBoolean

Transitions to GREEN state and ensures only one notification

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 103

def transition_to_green
  became_green = scripting.call(
    :"state/transition_to_green",
    args: [clock.current_time.to_f],
    keys: [state_key]
  )
  became_green == 1
end

#transition_to_redBoolean

Transitions to RED state and ensures only one notification

Returns:

  • (Boolean)


125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 125

def transition_to_red
  current_ts = clock.current_time.to_f
  recovery_scheduled_after_ts = current_ts + cool_off_time

  became_red = scripting.call(
    :"state/transition_to_red",
    args: [current_ts, recovery_scheduled_after_ts],
    keys: [state_key]
  )

  became_red == 1
end

#transition_to_yellowBoolean

Transitions to YELLOW (recovery) state and ensures only one notification

Returns:

  • (Boolean)


114
115
116
117
118
119
120
121
# File 'lib/stoplight/infrastructure/redis/storage/state.rb', line 114

def transition_to_yellow
  became_yellow = scripting.call(
    :"state/transition_to_yellow",
    args: [clock.current_time.to_f],
    keys: [state_key]
  )
  became_yellow == 1
end