flipper-active_record 0.26.0 → 0.26.2
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/benchmark/active_record_adapter_ips.rb +15 -0
- data/benchmark/active_record_ips.rb +17 -0
- data/benchmark/active_record_setup.rb +31 -0
- data/lib/flipper/adapters/active_record.rb +87 -56
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/active_record_spec.rb +122 -0
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2bf01c33aad4266b0237775c779d6b3b19d01d43adff566c82c1ddb26fb76f5
|
4
|
+
data.tar.gz: 34ba1e6bded36a3352031c82b9f32d34abbb5f8781eb28b15f7aa450cbd02cbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 303520e21a9e3f71284d72bb817032df8849b5bee213e4a42924ab5e90af1f38e82734830fba40d11c0b6e91962b134838e80d181f4c7482f306f64651222bf6
|
7
|
+
data.tar.gz: f9b371acc123a5a7cc8e901d40db474a83f706d886496512b9f24c298d0a8e621c2e394b3b38f8bddc5b92966d7ab594f38c1b83912026eac967133dccd1eb2d
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require_relative './active_record_setup'
|
3
|
+
require 'flipper'
|
4
|
+
require 'flipper/adapters/active_record'
|
5
|
+
require 'benchmark/ips'
|
6
|
+
|
7
|
+
flipper = Flipper.new(Flipper::Adapters::ActiveRecord.new)
|
8
|
+
|
9
|
+
2000.times do |i|
|
10
|
+
flipper.enable_actor :foo, Flipper::Actor.new("User;#{i}")
|
11
|
+
end
|
12
|
+
|
13
|
+
Benchmark.ips do |x|
|
14
|
+
x.report("get_all") { flipper.preload_all }
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require_relative './active_record_setup'
|
3
|
+
require 'flipper'
|
4
|
+
require 'flipper/adapters/active_record'
|
5
|
+
require 'benchmark/ips'
|
6
|
+
|
7
|
+
flipper = Flipper.new(Flipper::Adapters::ActiveRecord.new)
|
8
|
+
|
9
|
+
2000.times do |i|
|
10
|
+
flipper.enable_actor :foo, Flipper::Actor.new("User;#{i}")
|
11
|
+
end
|
12
|
+
|
13
|
+
Benchmark.ips do |x|
|
14
|
+
x.report("all") { Flipper::Adapters::ActiveRecord::Gate.where(feature_key: "foo".freeze).load }
|
15
|
+
x.report("pluck") { Flipper::Adapters::ActiveRecord::Gate.where(feature_key: "foo".freeze).pluck(:key, :value) }
|
16
|
+
x.compare!
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection({
|
5
|
+
adapter: 'sqlite3',
|
6
|
+
database: ':memory:',
|
7
|
+
})
|
8
|
+
|
9
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
10
|
+
CREATE TABLE flipper_features (
|
11
|
+
id integer PRIMARY KEY,
|
12
|
+
key text NOT NULL UNIQUE,
|
13
|
+
created_at datetime NOT NULL,
|
14
|
+
updated_at datetime NOT NULL
|
15
|
+
)
|
16
|
+
SQL
|
17
|
+
|
18
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
19
|
+
CREATE TABLE flipper_gates (
|
20
|
+
id integer PRIMARY KEY,
|
21
|
+
feature_key text NOT NULL,
|
22
|
+
key text NOT NULL,
|
23
|
+
value text DEFAULT NULL,
|
24
|
+
created_at datetime NOT NULL,
|
25
|
+
updated_at datetime NOT NULL
|
26
|
+
)
|
27
|
+
SQL
|
28
|
+
|
29
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
30
|
+
CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
|
31
|
+
SQL
|
@@ -53,18 +53,20 @@ module Flipper
|
|
53
53
|
|
54
54
|
# Public: The set of known features.
|
55
55
|
def features
|
56
|
-
@feature_class.all.map(&:key).to_set
|
56
|
+
with_connection(@feature_class) { @feature_class.all.map(&:key).to_set }
|
57
57
|
end
|
58
58
|
|
59
59
|
# Public: Adds a feature to the set of known features.
|
60
60
|
def add(feature)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
61
|
+
with_connection(@feature_class) do
|
62
|
+
# race condition, but add is only used by enable/disable which happen
|
63
|
+
# super rarely, so it shouldn't matter in practice
|
64
|
+
@feature_class.transaction do
|
65
|
+
unless @feature_class.where(key: feature.key).first
|
66
|
+
begin
|
67
|
+
@feature_class.create! { |f| f.key = feature.key }
|
68
|
+
rescue ::ActiveRecord::RecordNotUnique
|
69
|
+
end
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -74,16 +76,18 @@ module Flipper
|
|
74
76
|
|
75
77
|
# Public: Removes a feature from the set of known features.
|
76
78
|
def remove(feature)
|
77
|
-
@feature_class
|
78
|
-
@feature_class.
|
79
|
-
|
79
|
+
with_connection(@feature_class) do
|
80
|
+
@feature_class.transaction do
|
81
|
+
@feature_class.where(key: feature.key).destroy_all
|
82
|
+
clear(feature)
|
83
|
+
end
|
80
84
|
end
|
81
85
|
true
|
82
86
|
end
|
83
87
|
|
84
88
|
# Public: Clears the gate values for a feature.
|
85
89
|
def clear(feature)
|
86
|
-
@gate_class.where(feature_key: feature.key).destroy_all
|
90
|
+
with_connection(@gate_class) { @gate_class.where(feature_key: feature.key).destroy_all }
|
87
91
|
true
|
88
92
|
end
|
89
93
|
|
@@ -91,35 +95,52 @@ module Flipper
|
|
91
95
|
#
|
92
96
|
# Returns a Hash of Flipper::Gate#key => value.
|
93
97
|
def get(feature)
|
94
|
-
|
95
|
-
|
98
|
+
gates = with_connection(@gate_class) { @gate_class.where(feature_key: feature.key).pluck(:key, :value) }
|
99
|
+
result_for_gates(feature, gates)
|
96
100
|
end
|
97
101
|
|
98
102
|
def get_multi(features)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
103
|
+
with_connection(@gate_class) do
|
104
|
+
gates = @gate_class.where(feature_key: features.map(&:key)).pluck(:feature_key, :key, :value)
|
105
|
+
grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
|
106
|
+
hash[feature_key] ||= []
|
107
|
+
hash[feature_key] << [key, value]
|
108
|
+
hash
|
109
|
+
end
|
110
|
+
|
111
|
+
result = {}
|
112
|
+
features.each do |feature|
|
113
|
+
result[feature.key] = result_for_gates(feature, grouped_gates[feature.key])
|
114
|
+
end
|
115
|
+
result
|
104
116
|
end
|
105
|
-
result
|
106
117
|
end
|
107
118
|
|
108
119
|
def get_all
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
.
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
120
|
+
with_connection(@feature_class) do
|
121
|
+
# query the gates from the db in a single query
|
122
|
+
features = ::Arel::Table.new(@feature_class.table_name.to_sym)
|
123
|
+
gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
|
124
|
+
rows_query = features.join(gates, ::Arel::Nodes::OuterJoin)
|
125
|
+
.on(features[:key].eq(gates[:feature_key]))
|
126
|
+
.project(features[:key].as('feature_key'), gates[:key], gates[:value])
|
127
|
+
gates = @feature_class.connection.select_rows(rows_query)
|
128
|
+
|
129
|
+
# group the gates by feature key
|
130
|
+
grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
|
131
|
+
hash[feature_key] ||= []
|
132
|
+
hash[feature_key] << [key, value]
|
133
|
+
hash
|
134
|
+
end
|
135
|
+
|
136
|
+
# build up the result hash
|
137
|
+
result = Hash.new { |hash, key| hash[key] = default_config }
|
138
|
+
features = grouped_gates.keys.map { |key| Flipper::Feature.new(key, self) }
|
139
|
+
features.each do |feature|
|
140
|
+
result[feature.key] = result_for_gates(feature, grouped_gates[feature.key])
|
141
|
+
end
|
142
|
+
result
|
121
143
|
end
|
122
|
-
result
|
123
144
|
end
|
124
145
|
|
125
146
|
# Public: Enables a gate for a given thing.
|
@@ -158,7 +179,9 @@ module Flipper
|
|
158
179
|
when :integer
|
159
180
|
set(feature, gate, thing)
|
160
181
|
when :set
|
161
|
-
@gate_class
|
182
|
+
with_connection(@gate_class) do
|
183
|
+
@gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all
|
184
|
+
end
|
162
185
|
else
|
163
186
|
unsupported_data_type gate.data_type
|
164
187
|
end
|
@@ -175,18 +198,20 @@ module Flipper
|
|
175
198
|
|
176
199
|
def set(feature, gate, thing, options = {})
|
177
200
|
clear_feature = options.fetch(:clear, false)
|
178
|
-
@gate_class
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
201
|
+
with_connection(@gate_class) do
|
202
|
+
@gate_class.transaction do
|
203
|
+
clear(feature) if clear_feature
|
204
|
+
@gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
|
205
|
+
begin
|
206
|
+
@gate_class.create! do |g|
|
207
|
+
g.feature_key = feature.key
|
208
|
+
g.key = gate.key
|
209
|
+
g.value = thing.value.to_s
|
210
|
+
end
|
211
|
+
rescue ::ActiveRecord::RecordNotUnique
|
212
|
+
# assume this happened concurrently with the same thing and its fine
|
213
|
+
# see https://github.com/jnunemaker/flipper/issues/544
|
186
214
|
end
|
187
|
-
rescue ::ActiveRecord::RecordNotUnique
|
188
|
-
# assume this happened concurrently with the same thing and its fine
|
189
|
-
# see https://github.com/jnunemaker/flipper/issues/544
|
190
215
|
end
|
191
216
|
end
|
192
217
|
|
@@ -194,10 +219,12 @@ module Flipper
|
|
194
219
|
end
|
195
220
|
|
196
221
|
def enable_multi(feature, gate, thing)
|
197
|
-
@gate_class
|
198
|
-
|
199
|
-
|
200
|
-
|
222
|
+
with_connection(@gate_class) do
|
223
|
+
@gate_class.create! do |g|
|
224
|
+
g.feature_key = feature.key
|
225
|
+
g.key = gate.key
|
226
|
+
g.value = thing.value.to_s
|
227
|
+
end
|
201
228
|
end
|
202
229
|
|
203
230
|
nil
|
@@ -205,28 +232,32 @@ module Flipper
|
|
205
232
|
# already added so no need move on with life
|
206
233
|
end
|
207
234
|
|
208
|
-
def
|
209
|
-
db_gates ||= []
|
235
|
+
def result_for_gates(feature, gates)
|
210
236
|
result = {}
|
237
|
+
gates ||= []
|
211
238
|
feature.gates.each do |gate|
|
212
239
|
result[gate.key] =
|
213
240
|
case gate.data_type
|
214
241
|
when :boolean
|
215
|
-
if
|
216
|
-
|
242
|
+
if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
|
243
|
+
row.last
|
217
244
|
end
|
218
245
|
when :integer
|
219
|
-
if
|
220
|
-
|
246
|
+
if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
|
247
|
+
row.last
|
221
248
|
end
|
222
249
|
when :set
|
223
|
-
|
250
|
+
gates.select { |key, value| !key.nil? && key.to_sym == gate.key }.map(&:last).to_set
|
224
251
|
else
|
225
252
|
unsupported_data_type gate.data_type
|
226
253
|
end
|
227
254
|
end
|
228
255
|
result
|
229
256
|
end
|
257
|
+
|
258
|
+
def with_connection(model = @feature_class, &block)
|
259
|
+
model.connection_pool.with_connection(&block)
|
260
|
+
end
|
230
261
|
end
|
231
262
|
end
|
232
263
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -57,4 +57,126 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
|
|
57
57
|
expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::ActiveRecord)
|
58
58
|
end
|
59
59
|
end
|
60
|
+
|
61
|
+
context "ActiveRecord connection_pool" do
|
62
|
+
before do
|
63
|
+
ActiveRecord::Base.clear_active_connections!
|
64
|
+
end
|
65
|
+
|
66
|
+
context "#features" do
|
67
|
+
it "does not hold onto connections" do
|
68
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
69
|
+
subject.features
|
70
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "does not release previously held connection" do
|
74
|
+
ActiveRecord::Base.connection # establish a new connection
|
75
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
76
|
+
subject.features
|
77
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "#get_all" do
|
82
|
+
it "does not hold onto connections" do
|
83
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
84
|
+
subject.get_all
|
85
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "does not release previously held connection" do
|
89
|
+
ActiveRecord::Base.connection # establish a new connection
|
90
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
91
|
+
subject.get_all
|
92
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "#add / #remove / #clear" do
|
97
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
98
|
+
|
99
|
+
it "does not hold onto connections" do
|
100
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
101
|
+
subject.add(feature)
|
102
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
103
|
+
subject.remove(feature)
|
104
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
105
|
+
subject.clear(feature)
|
106
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "does not release previously held connection" do
|
110
|
+
ActiveRecord::Base.connection # establish a new connection
|
111
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
112
|
+
subject.add(feature)
|
113
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
114
|
+
subject.remove(feature)
|
115
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
116
|
+
subject.clear(feature)
|
117
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "#get_multi" do
|
122
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
123
|
+
|
124
|
+
it "does not hold onto connections" do
|
125
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
126
|
+
subject.get_multi([feature])
|
127
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "does not release previously held connection" do
|
131
|
+
ActiveRecord::Base.connection # establish a new connection
|
132
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
133
|
+
subject.get_multi([feature])
|
134
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "#enable/#disable boolean" do
|
139
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
140
|
+
let(:gate) { feature.gate(:boolean)}
|
141
|
+
|
142
|
+
it "does not hold onto connections" do
|
143
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
144
|
+
subject.enable(feature, gate, gate.wrap(true))
|
145
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
146
|
+
subject.disable(feature, gate, gate.wrap(false))
|
147
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "does not release previously held connection" do
|
151
|
+
ActiveRecord::Base.connection # establish a new connection
|
152
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
153
|
+
subject.enable(feature, gate, gate.wrap(true))
|
154
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
155
|
+
subject.disable(feature, gate, gate.wrap(false))
|
156
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "#enable/#disable set" do
|
161
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
162
|
+
let(:gate) { feature.gate(:group) }
|
163
|
+
|
164
|
+
it "does not hold onto connections" do
|
165
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
166
|
+
subject.enable(feature, gate, gate.wrap(:admin))
|
167
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
168
|
+
subject.disable(feature, gate, gate.wrap(:admin))
|
169
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "does not release previously held connection" do
|
173
|
+
ActiveRecord::Base.connection # establish a new connection
|
174
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
175
|
+
subject.enable(feature, gate, gate.wrap(:admin))
|
176
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
177
|
+
subject.disable(feature, gate, gate.wrap(:admin))
|
178
|
+
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
60
182
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipper-active_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.26.
|
4
|
+
version: 0.26.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: flipper
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.26.
|
19
|
+
version: 0.26.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.26.
|
26
|
+
version: 0.26.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,6 +51,9 @@ executables: []
|
|
51
51
|
extensions: []
|
52
52
|
extra_rdoc_files: []
|
53
53
|
files:
|
54
|
+
- benchmark/active_record_adapter_ips.rb
|
55
|
+
- benchmark/active_record_ips.rb
|
56
|
+
- benchmark/active_record_setup.rb
|
54
57
|
- examples/active_record/ar_setup.rb
|
55
58
|
- examples/active_record/basic.rb
|
56
59
|
- examples/active_record/internals.rb
|