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 +4 -4
- data/lib/rollout/adapters/redis.rb +96 -1
- data/lib/rollout/redis/codec.rb +5 -1
- data/lib/rollout/redis/feature_export.rb +30 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4c44183a42a826a4a8d96d018caa891dcd43519700ff309dbb83c8aab1df4ec
|
|
4
|
+
data.tar.gz: e6bedd04a653fe34fc060d5a0eeb57a195fb68c87442ca419795ef4998b02ab0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
data/lib/rollout/redis/codec.rb
CHANGED
|
@@ -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:
|
|
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.
|
|
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-
|
|
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
|