Class: Stoplight::Infrastructure::Redis::Storage::UnboundedMetrics

Inherits:
Metrics
  • Object
show all
Defined in:
lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb,
sig/_private/stoplight/infrastructure/redis/storage/unbounded_metrics.rbs

Overview

Note:

The errors and successes fields in the returned Stoplight::Domain::Metrics are always nil since this class doesn't track windowed totals.

Note:

The metrics hash TTL is refreshed on every write operation. Circuits with no activity will have their metrics expire, which is generally desirable for ephemeral or decommissioned lights.

Distributed metrics storage for consecutive-error light strategies.

This class provides a lightweight alternative to WindowMetrics for circuit breakers that don't need time-windowed rate calculations. It tracks only:

  • Consecutive success/failure counters (reset on opposite outcome)
  • Most recent error with timestamp and serialized details
  • Most recent success timestamp

Storage Structure

All data is stored in a single Redis hash:

stoplight:{version}:{system}:{light}:metrics

Hash fields:

  • last_success_at: Unix timestamp (float) of most recent success
  • last_error_at: Unix timestamp (float) of most recent failure
  • last_error_json: Serialized Domain::Failure for error details
  • consecutive_successes: Integer counter, reset to 0 on failure
  • consecutive_errors: Integer counter, reset to 0 on success

Performance Characteristics

This implementation is optimized for minimal Redis overhead:

  • Single hash key per circuit (vs. multiple ZSETs for WindowMetrics)
  • O(1) reads and writes
  • No time-range queries or bucket management
  • Low memory footprint

Atomicity

Record operations use Lua scripts to ensure atomic read-modify-write:

  • Consecutive counters are incremented and reset in one round-trip
  • "Last" timestamps only update if the new event is more recent, preventing out-of-order writes from corrupting state

When to Use

Choose UnboundedMetrics when your circuit breaker strategy is based on consecutive failures (e.g., "open after 5 failures in a row"). Choose WindowMetrics when you need error rate calculations (e.g., "open when error rate exceeds 50% over 5 minutes").

Direct Known Subclasses

RecoveryMetrics

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Metrics

#deserialize_failure, #serialize_exception

Constructor Details

#initialize(redis:, scripting:, key_space:, clock:) ⇒ UnboundedMetrics

Returns a new instance of UnboundedMetrics.



57
58
59
60
61
62
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 57

def initialize(redis:, scripting:, key_space:, clock:)
  @clock = clock
  @scripting = scripting
  @redis = redis
  @metrics_key = key_space.key(:metrics)
end

Instance Attribute Details

#clockDomain::_Clock (readonly)

Returns the value of attribute clock.

Returns:



118
119
120
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 118

def clock
  @clock
end

#metrics_keyString (readonly)

Returns the value of attribute metrics_key.

Returns:

  • (String)


117
118
119
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 117

def metrics_key
  @metrics_key
end

#redisredis (readonly)

Returns the value of attribute redis.

Returns:



115
116
117
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 115

def redis
  @redis
end

#scriptingScripting (readonly)

Returns the value of attribute scripting.

Returns:



116
117
118
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 116

def scripting
  @scripting
end

Instance Method Details

#clearObject



107
108
109
110
111
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 107

def clear
  redis.with do |client|
    client.hdel(metrics_key, "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes")
  end
end

#metrics_snapshotObject

Get metrics for the current light



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 66

def metrics_snapshot
  last_success_at, last_error_json, consecutive_errors, consecutive_successes = redis.with do |client|
    client.hmget(
      metrics_key,
      "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
    )
  end

  Domain::MetricsSnapshot.new(
    successes: nil, errors: nil,
    consecutive_errors: consecutive_errors.to_i,
    consecutive_successes: consecutive_successes.to_i,
    last_error: deserialize_failure(last_error_json),
    last_success_at: (clock.at(last_success_at.to_f) if last_success_at)
  )
end

#record_failure(exception) ⇒ Object

Records failed circuit breaker execution



97
98
99
100
101
102
103
104
105
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 97

def record_failure(exception)
  timestamp = clock.current_time.to_f

  scripting.call(
    :"unbounded_metrics/record_failure",
    args: [timestamp, serialize_exception(exception, timestamp:), metrics_ttl],
    keys: [metrics_key]
  )
end

#record_successObject

Records successful circuit breaker execution



85
86
87
88
89
90
91
92
93
# File 'lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb', line 85

def record_success
  timestamp = clock.current_time.to_f

  scripting.call(
    :"unbounded_metrics/record_success",
    args: [timestamp, metrics_ttl],
    keys: [metrics_key]
  )
end