Class: Stoplight::Infrastructure::Redis::Storage::WindowMetrics

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

Overview

Distributed storage for time-windowed light metrics using Redis.

This class implements sliding window metrics using Redis sorted sets (ZSETs) for efficient time-range queries. Events are bucketed by hour to bound memory usage and enable automatic expiration via Redis TTLs.

Storage Structure

Events are stored in hourly buckets as ZSETs:

stoplight:{version}:{system}:{light}:window_metrics:success:1696154400
stoplight:{version}:{system}:{light}:window_metrics:failure:1696154400

Each ZSET member is a unique event ID with its timestamp as the score, enabling O(log N) range queries via ZCOUNT.

Metadata (consecutive counters, last error) is stored in a hash:

stoplight:{version}:{system}:{light}:window_metrics

Bucket Strategy

Fixed 1-hour buckets provide a balance between:

  • Query efficiency: At most ~25 buckets for a 24-hour window
  • Memory efficiency: Natural expiration without manual cleanup
  • Precision: Sub-bucket accuracy via ZSET scores

Atomicity

All operations use Lua scripts to ensure atomicity:

  • record_success: Increments counter and updates metadata in one round-trip
  • record_failure: Same, plus stores serialized error details
  • metrics_snapshot: Aggregates across buckets atomically

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Metrics

#deserialize_failure, #serialize_exception

Constructor Details

#initialize(redis:, scripting:, config:, clock:, key_space:) ⇒ WindowMetrics

Returns a new instance of WindowMetrics.



40
41
42
43
44
45
46
47
48
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 40

def initialize(redis:, scripting:, config:, clock:, key_space:)
  @clock = clock
  @scripting = scripting
  @redis = redis
  @config = config
  @key_space = key_space
  @metrics_key = key_space.key(:window_metrics)
  @window_size = T.must(config.window_size).to_i
end

Instance Attribute Details

#clockDomain::_Clock (readonly)

Returns the value of attribute clock.

Returns:



157
158
159
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 157

def clock
  @clock
end

#configDomain::Config (readonly)

Returns the value of attribute config.

Returns:



6
7
8
# File 'sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs', line 6

def config
  @config
end

#key_spaceKeySpace (readonly)

Returns the value of attribute key_space.

Returns:



158
159
160
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 158

def key_space
  @key_space
end

#metrics_keyString (readonly)

Returns the value of attribute metrics_key.

Returns:

  • (String)


156
157
158
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 156

def metrics_key
  @metrics_key
end

#redisredis (readonly)

Returns the value of attribute redis.

Returns:



154
155
156
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 154

def redis
  @redis
end

#scriptingScripting (readonly)

Returns the value of attribute scripting.

Returns:



155
156
157
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 155

def scripting
  @scripting
end

Instance Method Details

#bucket_key(metric:, time:) ⇒ String

Generates a Redis key for a specific metric and time.

Parameters:

  • metric (Symbol)

    The metric type (e.g., "errors").

  • time (Time, Numeric)

    The time for which to generate the key.

  • metric: (Symbol)
  • time: (_ToI)

Returns:

  • (String)

    The generated Redis key.



127
128
129
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 127

def bucket_key(metric:, time:)
  key_space.key(:window_metrics, metric, (time.to_i / bucket_size) * bucket_size)
end

#bucket_sizeInteger

1 hour

Returns:

  • (Integer)


160
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 160

def bucket_size = 3600 # 1 hour

#bucket_ttlInteger

Returns:

  • (Integer)


161
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 161

def bucket_ttl = @window_size + bucket_size

#buckets_for_window(metric:, window_end:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Retrieves the list of Redis bucket keys required to cover a specific time window.

Parameters:

  • metric

    The metric type (e.g., "errors").

  • window_end

    The end time of the window (can be a Time object or a numeric timestamp).

  • metric: (Symbol)
  • window_end: (_ToI)

Returns:

  • A list of Redis keys for the buckets that cover the time window.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 137

def buckets_for_window(metric:, window_end:)
  window_end_ts = window_end.to_i
  window_start_ts = window_end_ts - @window_size

  # Find bucket timestamps that contain any part of the window
  start_bucket = (window_start_ts / bucket_size) * bucket_size

  # End bucket is the last bucket that contains data within our window
  end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size

  (start_bucket..end_bucket).step(bucket_size).map do |bucket_start|
    bucket_key(metric: metric, time: bucket_start)
  end
end

#clearObject



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

def clear
  window_end_ts = clock.current_time.to_f
  failure_keys = failure_bucket_keys(window_end_ts)
  success_keys = success_bucket_keys(window_end_ts)

  redis.with do |client|
    client.del(metrics_key, *failure_keys, *success_keys)
  end
end

#errors_key(time:) ⇒ String

Parameters:

  • time: (_ToI)

Returns:

  • (String)


165
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 165

def errors_key(time:) = bucket_key(metric: :failure, time:)

#failure_bucket_keys(window_end) ⇒ Array[String]

Parameters:

  • window_end (_ToI)

Returns:

  • (Array[String])


167
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 167

def failure_bucket_keys(window_end) = buckets_for_window(metric: :failure, window_end:)

#metrics_snapshotStoplight::Domain::Metrics

Get metrics for the current light

Returns:

  • (Stoplight::Domain::Metrics)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 53

def metrics_snapshot
  window_end_ts = clock.current_time.to_f
  window_start_ts = window_end_ts - @window_size
  failure_keys = failure_bucket_keys(window_end_ts)
  success_keys = success_bucket_keys(window_end_ts)

  successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = scripting.call(
    :"window_metrics/metrics_snapshot",
    args: [
      failure_keys.count,
      window_start_ts,
      window_end_ts,
      "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes"
    ],
    keys: [
      metrics_key,
      *success_keys,
      *failure_keys
    ]
  )
  Domain::MetricsSnapshot.new(
    successes:, errors:,
    consecutive_errors: [consecutive_errors.to_i, errors].min,
    consecutive_successes: [consecutive_successes.to_i, successes].min,
    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) ⇒ void

This method returns an undefined value.

Records failed circuit breaker execution

Parameters:

  • exception (StandardError)


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

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

  scripting.call(
    :"window_metrics/record_failure",
    args: [timestamp, SecureRandom.hex(12), serialize_exception(exception, timestamp:), bucket_ttl, metrics_ttl],
    keys: [metrics_key, errors_key(time: timestamp)]
  )
end

#record_successvoid

This method returns an undefined value.

Records successful circuit breaker execution



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

def record_success
  timestamp = clock.current_time.to_f

  scripting.call(
    :"window_metrics/record_success",
    args: [timestamp, SecureRandom.hex(12), bucket_ttl, metrics_ttl],
    keys: [
      metrics_key,
      successes_key(time: timestamp)
    ]
  )
end

#success_bucket_keys(window_end) ⇒ Array[String]

Parameters:

  • window_end (_ToI)

Returns:

  • (Array[String])


169
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 169

def success_bucket_keys(window_end) = buckets_for_window(metric: :success, window_end:)

#successes_key(time:) ⇒ String

Parameters:

  • time: (_ToI)

Returns:

  • (String)


163
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 163

def successes_key(time:) = bucket_key(metric: :success, time:)