breaker_machines 0.21.2 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca6a74b2ade5414f83cd68ce2f160672d867ddd26b1ed8b5c8e033d20005e364
4
- data.tar.gz: 88d044647e231cf66e7b463eb2fbf9fa1e7a9762827a2349716cee29a4e8369c
3
+ metadata.gz: deabe27c242daf0d590e59854866aa82de852bd5bf23a9dbc68ddb744c241b1d
4
+ data.tar.gz: 461633a405ff2e17ff26b604ea92c05a2323e777a0a439149915a3d50a7e2a5c
5
5
  SHA512:
6
- metadata.gz: 6d009cbbe89e2ab5c38d2aaaaeb6148c1cb24199da282479cd436c31be68dd2fe09de70d6ac4208be3e96b7d11c92b77b33960656248a2337d1b4b66cbd4eff7
7
- data.tar.gz: 3072f5bda0574496547d325de2e60b47f675961ef2755c755a4f3d486ec4c112628afad7e360f8fe22d8f87ff14a7d41ac899b29464a2f12d2b68f198ce941d6
6
+ metadata.gz: 8ecf93848f13975efb9740e41fd2d76468ab7da07d9ba6667ae1e1e408eaa6739030c7c178631b5039f29229da4da7a37a1ae94d0f6a69172d1b199bf268c897
7
+ data.tar.gz: 2133bf3bf089fcbaab9974ea9cf5ea00da860c4f1a87b28df1301e022371e34143526d4c9eabafddb16fca822b53827bf154af957be0edef837b7421ce6cd0a5
@@ -221,7 +221,11 @@ module BreakerMachines
221
221
  puts '-' * 80
222
222
 
223
223
  events.each do |event|
224
- timestamp = Time.at(event[:timestamp]).strftime('%Y-%m-%d %H:%M:%S')
224
+ timestamp = if event[:timestamp_epoch]
225
+ Time.at(event[:timestamp_epoch]).strftime('%Y-%m-%d %H:%M:%S')
226
+ else
227
+ format('%.3fs', event[:timestamp])
228
+ end
225
229
  type = colorize_event_type(event[:type])
226
230
  duration = event[:duration_ms] ? "#{event[:duration_ms]}ms" : '-'
227
231
  error = event[:error_class] || '-'
@@ -15,6 +15,9 @@ module BreakerMachines
15
15
  @bucket_seconds = Integer(options[:bucket_seconds] || 1)
16
16
  raise ArgumentError, 'bucket_seconds must be positive' unless @bucket_seconds.positive?
17
17
 
18
+ @max_events = Integer(options[:max_events] || 100)
19
+ raise ArgumentError, 'max_events must be positive' unless @max_events.positive?
20
+
18
21
  @warned_windows = Concurrent::Set.new
19
22
  end
20
23
 
@@ -67,28 +70,23 @@ module BreakerMachines
67
70
  keys = [success_key(circuit_name), failure_key(circuit_name)].flat_map do |key|
68
71
  bucket_keys(key, bucket_count(@expires_in) + 1)
69
72
  end
70
- delete_counters(keys)
73
+ delete_counters(keys + [event_sequence_key(circuit_name)] + event_slot_keys(circuit_name))
71
74
  end
72
75
 
73
76
  def clear_all
74
- # Clear all circuit data by pattern if cache supports it
75
- if @cache.respond_to?(:delete_matched)
76
- @cache.delete_matched("#{@prefix}:*")
77
- else
78
- # Fallback: can't efficiently clear all without pattern support
79
- BreakerMachines.logger&.warn(
80
- "[BreakerMachines] Cache store doesn't support delete_matched. " \
81
- 'Individual circuit data must be cleared manually.'
82
- )
83
- end
77
+ return @cache.delete_matched(prefix_matcher) if @cache.respond_to?(:delete_matched)
78
+
79
+ warn_clear_all_unsupported
80
+ rescue NotImplementedError
81
+ warn_clear_all_unsupported
84
82
  end
85
83
 
86
84
  def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
87
- events = @cache.fetch(events_key(circuit_name)) { [] }
88
-
85
+ now = monotonic_time
89
86
  event = {
90
87
  type: type,
91
- timestamp: monotonic_time,
88
+ timestamp: now,
89
+ timestamp_epoch: BreakerMachines.monotonic_to_epoch(now),
92
90
  duration_ms: (duration * 1000).round(2)
93
91
  }
94
92
 
@@ -96,15 +94,24 @@ module BreakerMachines
96
94
  event[:error_message] = error.message if error
97
95
  event[:new_state] = new_state if new_state
98
96
 
99
- events << event
100
- events.shift while events.size > (@max_events || 100)
97
+ return append_event(circuit_name, event) unless @cache.respond_to?(:increment)
101
98
 
102
- @cache.write(events_key(circuit_name), events, expires_in: @expires_in)
99
+ sequence = @cache.increment(event_sequence_key(circuit_name), 1, expires_in: @expires_in)
100
+ return false unless sequence
101
+
102
+ @cache.write(event_slot_key(circuit_name, sequence % @max_events),
103
+ event.merge(sequence: sequence), expires_in: @expires_in)
103
104
  end
104
105
 
105
106
  def event_log(circuit_name, limit)
106
- events = @cache.read(events_key(circuit_name)) || []
107
- events.last(limit)
107
+ events = if @cache.respond_to?(:increment)
108
+ read_values(event_slot_keys(circuit_name)).sort_by do |event|
109
+ [event[:timestamp_epoch], event[:sequence]]
110
+ end
111
+ else
112
+ @cache.read(events_key(circuit_name)) || []
113
+ end
114
+ events.last(limit).map { |event| local_event(event) }
108
115
  end
109
116
 
110
117
  def with_timeout(_timeout_ms)
@@ -165,9 +172,39 @@ module BreakerMachines
165
172
 
166
173
  # read_multi is a single round trip; stores without it read each bucket.
167
174
  def read_counters(keys)
168
- return @cache.read_multi(*keys, raw: true).values if @cache.respond_to?(:read_multi)
175
+ read_values(keys, raw: true)
176
+ end
177
+
178
+ def read_values(keys, **options)
179
+ return @cache.read_multi(*keys, **options).values if @cache.respond_to?(:read_multi)
180
+
181
+ keys.filter_map { |key| @cache.read(key, options) }
182
+ end
169
183
 
170
- keys.filter_map { |key| @cache.read(key) }
184
+ def append_event(circuit_name, event)
185
+ events = @cache.fetch(events_key(circuit_name)) { [] }
186
+ events << event
187
+ events.shift while events.size > @max_events
188
+ @cache.write(events_key(circuit_name), events, expires_in: @expires_in)
189
+ end
190
+
191
+ def local_event(event)
192
+ epoch = event[:timestamp_epoch]
193
+ local = event.except(:sequence)
194
+ epoch ? local.merge(timestamp: BreakerMachines.epoch_to_monotonic(epoch)) : local
195
+ end
196
+
197
+ def prefix_matcher
198
+ return "#{@prefix.gsub(/[?*\[\]\\]/) { |char| "\\#{char}" }}:*" if @cache.respond_to?(:redis)
199
+
200
+ /^#{Regexp.escape(@prefix)}:/
201
+ end
202
+
203
+ def warn_clear_all_unsupported
204
+ BreakerMachines.logger&.warn(
205
+ "[BreakerMachines] #{@cache.class} doesn't support delete_matched. " \
206
+ 'Individual circuit data must be cleared manually.'
207
+ )
171
208
  end
172
209
 
173
210
  def delete_counters(keys)
@@ -210,6 +247,18 @@ module BreakerMachines
210
247
  "#{@prefix}:#{circuit_name}:events"
211
248
  end
212
249
 
250
+ def event_sequence_key(circuit_name)
251
+ "#{@prefix}:#{circuit_name}:event_sequence"
252
+ end
253
+
254
+ def event_slot_key(circuit_name, slot)
255
+ "#{@prefix}:#{circuit_name}:event:#{slot}"
256
+ end
257
+
258
+ def event_slot_keys(circuit_name)
259
+ Array.new(@max_events) { |slot| event_slot_key(circuit_name, slot) }
260
+ end
261
+
213
262
  def monotonic_time
214
263
  BreakerMachines.monotonic_time
215
264
  end
@@ -46,9 +46,11 @@ module BreakerMachines
46
46
  def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
47
47
  events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
48
48
 
49
+ now = monotonic_time
49
50
  event = {
50
51
  type: type,
51
- timestamp: monotonic_time,
52
+ timestamp: now,
53
+ timestamp_epoch: BreakerMachines.monotonic_to_epoch(@start_time + now),
52
54
  duration_ms: (duration * 1000).round(2)
53
55
  }
54
56
 
@@ -24,6 +24,7 @@ module BreakerMachines
24
24
  super
25
25
  # Native extension is guaranteed to be available when this class is loaded
26
26
  @backend = BreakerMachinesNative::Storage.new
27
+ @epoch_origin = BreakerMachines.monotonic_to_epoch(BreakerMachines.monotonic_time)
27
28
  end
28
29
 
29
30
  # Check if using native backend
@@ -81,7 +82,9 @@ module BreakerMachines
81
82
  end
82
83
 
83
84
  def event_log(circuit_name, limit)
84
- @backend.event_log(circuit_name.to_s, limit)
85
+ @backend.event_log(circuit_name.to_s, limit).map do |event|
86
+ event.merge(timestamp_epoch: @epoch_origin + event[:timestamp])
87
+ end
85
88
  end
86
89
 
87
90
  def with_timeout(_timeout_ms)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.21.2'
4
+ VERSION = '0.22.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: breaker_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.2
4
+ version: 0.22.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih