flipper-active_record 0.26.0 → 0.26.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96994023a61be27a5bb30418c7bc367c82689ee4f964bb838996b84ff93e4725
4
- data.tar.gz: 3fbd092338493aaa9da024517e35a2daef1c176526858b94bb93ccafa3c876a9
3
+ metadata.gz: f2bf01c33aad4266b0237775c779d6b3b19d01d43adff566c82c1ddb26fb76f5
4
+ data.tar.gz: 34ba1e6bded36a3352031c82b9f32d34abbb5f8781eb28b15f7aa450cbd02cbd
5
5
  SHA512:
6
- metadata.gz: 2d1d74510fbba3f6b933c277c7747d2e0057e26aefb472889ef7dc463f80316cd7e8d0a9dd2f4d7916888b2bf40266ecb68dd84889f38efab59b058885d54c2c
7
- data.tar.gz: f885cc037bfd91646b895056b54802ec512d56c1a670c98fa93d67d7c8de3b39bd1e5512ce630fc95e71c1db604ca69430a5aca1cf962d6de322d100c00a4ceb
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
- # race condition, but add is only used by enable/disable which happen
62
- # super rarely, so it shouldn't matter in practice
63
- @feature_class.transaction do
64
- unless @feature_class.where(key: feature.key).first
65
- begin
66
- @feature_class.create! { |f| f.key = feature.key }
67
- rescue ::ActiveRecord::RecordNotUnique
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.transaction do
78
- @feature_class.where(key: feature.key).destroy_all
79
- clear(feature)
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
- db_gates = @gate_class.where(feature_key: feature.key)
95
- result_for_feature(feature, db_gates)
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
- db_gates = @gate_class.where(feature_key: features.map(&:key))
100
- grouped_db_gates = db_gates.group_by(&:feature_key)
101
- result = {}
102
- features.each do |feature|
103
- result[feature.key] = result_for_feature(feature, grouped_db_gates[feature.key])
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
- features = ::Arel::Table.new(@feature_class.table_name.to_sym)
110
- gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
111
- rows_query = features.join(gates, Arel::Nodes::OuterJoin)
112
- .on(features[:key].eq(gates[:feature_key]))
113
- .project(features[:key].as('feature_key'), gates[:key], gates[:value])
114
- rows = @feature_class.connection.select_all rows_query
115
- db_gates = rows.map { |row| @gate_class.new(row) }
116
- grouped_db_gates = db_gates.group_by(&:feature_key)
117
- result = Hash.new { |hash, key| hash[key] = default_config }
118
- features = grouped_db_gates.keys.map { |key| Flipper::Feature.new(key, self) }
119
- features.each do |feature|
120
- result[feature.key] = result_for_feature(feature, grouped_db_gates[feature.key])
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.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all
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.transaction do
179
- clear(feature) if clear_feature
180
- @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
181
- begin
182
- @gate_class.create! do |g|
183
- g.feature_key = feature.key
184
- g.key = gate.key
185
- g.value = thing.value.to_s
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.create! do |g|
198
- g.feature_key = feature.key
199
- g.key = gate.key
200
- g.value = thing.value.to_s
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 result_for_feature(feature, db_gates)
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 detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s }
216
- detected_db_gate.value
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 detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s }
220
- detected_db_gate.value
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
- db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set
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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.26.0'.freeze
2
+ VERSION = '0.26.2'.freeze
3
3
  end
@@ -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.0
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: 2022-12-05 00:00:00.000000000 Z
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.0
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.0
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