rollout-redis-adapter 0.2.0 → 0.3.0

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: cf83c825545efedafc1178bb65a87f6a8efdf94da42818aaad99342c7b58d750
4
- data.tar.gz: 1391230d545262e09d4e02b40419c2f6ece6d45a0475862e53b1bf085d3b3935
3
+ metadata.gz: c4c44183a42a826a4a8d96d018caa891dcd43519700ff309dbb83c8aab1df4ec
4
+ data.tar.gz: e6bedd04a653fe34fc060d5a0eeb57a195fb68c87442ca419795ef4998b02ab0
5
5
  SHA512:
6
- metadata.gz: 4c677217147be1281238fc328cf7063f40cdfe44084dc128746321f127e0d0c0d41290cca725e2dc2177fe642bb2196cf901a341d5a2e70121afe1190f372754
7
- data.tar.gz: 0c170c12b556cacce684a044a27958e8033aa73d406a8c225cfb04cbbf47bbe54b01acb63e4b9cdba80ab4008e71c91e168955ae146bfdd789b970d9a6ece307
6
+ metadata.gz: 7601f9edbd29fb598348964a1b081b0de79899d604fc370b029550635c0020ff9ba411b3cec6dd42b4788e0f6cc20db3cbfc6a231913042cd8d53be8b3112af2
7
+ data.tar.gz: 4e1af966e074998263b6c8d8d27be904b5ebbfc40fa711d10d53f93bf791711065bfd2394a092917f45dfb80c6eeb38c6ff92ae9c4b231e29b0b1d480761289e
@@ -4,6 +4,7 @@ require 'redis'
4
4
  require 'rollout'
5
5
  require 'rollout/logging'
6
6
  require 'rollout/redis/codec'
7
+ require 'rollout/redis/feature_export'
7
8
 
8
9
  class Rollout
9
10
  module Adapters
@@ -54,6 +55,21 @@ class Rollout
54
55
  @client.del(FEATURES_KEY)
55
56
  end
56
57
 
58
+ def export_features(include_history: false)
59
+ registered = feature_names.map(&:to_s).reject { |name| name.empty? }.uniq
60
+ stored = stored_feature_names
61
+ missing_names = (registered - stored).sort
62
+ unregistered_names = (stored - registered).sort
63
+ present = registered - missing_names
64
+
65
+ ::Rollout::Redis::FeatureExport.new(
66
+ states: fetch_features(present),
67
+ missing_names: missing_names,
68
+ unregistered_names: unregistered_names,
69
+ history: include_history ? export_history : [],
70
+ )
71
+ end
72
+
57
73
  def mutate_feature(name)
58
74
  mutation = yield fetch_feature(name)
59
75
  save_feature(mutation.fetch(:state))
@@ -89,7 +105,7 @@ class Rollout
89
105
 
90
106
  def feature_updated_at(name)
91
107
  _, score = @client.zrange(events_key(name), 0, 0, with_scores: true).first
92
- Time.at(-score.to_f / 1_000_000) if score
108
+ ::Rollout::Redis::Codec.time_from_score(score) if score
93
109
  end
94
110
 
95
111
  def delete_feature_events(name)
@@ -98,6 +114,85 @@ class Rollout
98
114
 
99
115
  private
100
116
 
117
+ def stored_feature_names
118
+ names = []
119
+ @client.scan_each(match: 'feature:*') do |raw_key|
120
+ key = raw_key.to_s
121
+ next if key == FEATURES_KEY
122
+ next if key.end_with?(':logging:events')
123
+
124
+ names << key.sub(/\Afeature:/, '')
125
+ end
126
+ names.uniq
127
+ end
128
+
129
+ def export_history
130
+ feature_members = []
131
+ global_members = {}
132
+ seen_keys = {}
133
+
134
+ @client.scan_each(match: 'feature:*:logging:events') do |raw_key|
135
+ key = raw_key.to_s
136
+ next if seen_keys[key]
137
+
138
+ seen_keys[key] = true
139
+ if key == global_events_key
140
+ zrange_pairs(key).each do |member, score|
141
+ global_members[member] = score
142
+ end
143
+ else
144
+ name = history_feature_name(key)
145
+ next if name.nil? || name == '_global_'
146
+
147
+ zrange_pairs(key).each do |member, score|
148
+ feature_members << [member, score]
149
+ end
150
+ end
151
+ end
152
+
153
+ seen = {}
154
+ rows = []
155
+ feature_members.each do |member, score|
156
+ seen[member] = true
157
+ rows << [member, score, true, !global_members[member].nil?]
158
+ end
159
+ global_members.each do |member, score|
160
+ next if seen[member]
161
+
162
+ rows << [member, score, false, true]
163
+ end
164
+
165
+ sort_history_rows(rows).map do |member, score, feature_visible, global_visible|
166
+ history_entry(member, score, feature_visible, global_visible)
167
+ end
168
+ end
169
+
170
+ def history_feature_name(key)
171
+ return nil unless key.start_with?('feature:') && key.end_with?(':logging:events')
172
+
173
+ key.sub(/\Afeature:/, '').sub(/:logging:events\z/, '')
174
+ end
175
+
176
+ def zrange_pairs(key)
177
+ pairs = @client.zrange(key, 0, -1, with_scores: true)
178
+ return [] if pairs.nil? || pairs.empty?
179
+ return pairs if pairs.first.is_a?(Array)
180
+
181
+ pairs.each_slice(2).to_a
182
+ end
183
+
184
+ def history_entry(member, score, feature_visible, global_visible)
185
+ ::Rollout::Redis::FeatureExport::HistoryEntry.new(
186
+ event: ::Rollout::Redis::Codec.decode_event(member, score),
187
+ feature_visible: feature_visible,
188
+ global_visible: global_visible,
189
+ )
190
+ end
191
+
192
+ def sort_history_rows(rows)
193
+ rows.sort_by { |member, score, _feature_visible, _global_visible| [score, member] }.reverse
194
+ end
195
+
101
196
  def key(name)
102
197
  "feature:#{name}"
103
198
  end
@@ -37,7 +37,11 @@ class Rollout
37
37
  def self.decode_event(value, score)
38
38
  hash = JSON.parse(value, symbolize_names: true)
39
39
 
40
- Logging::Event.new(**hash.merge(created_at: Time.at(-score.to_f / 1_000_000)))
40
+ Logging::Event.new(**hash.merge(created_at: time_from_score(score)))
41
+ end
42
+
43
+ def self.time_from_score(score)
44
+ Time.at(Rational(-score.to_i, 1_000_000))
41
45
  end
42
46
  end
43
47
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rollout
4
+ module Redis
5
+ class FeatureExport
6
+ class HistoryEntry
7
+ attr_reader :event, :feature_visible, :global_visible
8
+
9
+ def initialize(event:, feature_visible:, global_visible:)
10
+ @event = event
11
+ @feature_visible = feature_visible
12
+ @global_visible = global_visible
13
+ end
14
+ end
15
+
16
+ attr_reader :states, :missing_names, :unregistered_names, :history
17
+
18
+ def initialize(states:, missing_names:, unregistered_names:, history: [])
19
+ @states = states
20
+ @missing_names = missing_names
21
+ @unregistered_names = unregistered_names
22
+ @history = history
23
+ end
24
+
25
+ def valid?
26
+ missing_names.empty? && unregistered_names.empty?
27
+ end
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout-redis-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FetLife
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -61,6 +61,7 @@ files:
61
61
  - lib/rollout-redis-adapter.rb
62
62
  - lib/rollout/adapters/redis.rb
63
63
  - lib/rollout/redis/codec.rb
64
+ - lib/rollout/redis/feature_export.rb
64
65
  homepage: https://github.com/FetLife/rollout
65
66
  licenses:
66
67
  - MIT