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 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