flipper 0.26.0 → 0.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +13 -11
- data/.github/workflows/examples.yml +5 -10
- data/Changelog.md +16 -0
- data/Gemfile +6 -3
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/flipper.gemspec +0 -2
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/http.rb +11 -3
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +19 -2
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +4 -0
- data/lib/flipper/dsl.rb +1 -5
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/feature.rb +22 -18
- data/lib/flipper/feature_check_context.rb +4 -4
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +2 -12
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +4 -8
- data/lib/flipper/gates/percentage_of_actors.rb +9 -11
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/instrumentation/subscriber.rb +8 -0
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +2 -1
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_spec.rb +29 -2
- data/spec/flipper/adapters/http_spec.rb +25 -3
- data/spec/flipper/adapters/instrumented_spec.rb +28 -10
- data/spec/flipper/adapters/memoizable_spec.rb +30 -10
- data/spec/flipper/adapters/memory_spec.rb +3 -1
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
- data/spec/flipper/dsl_spec.rb +20 -3
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/feature_check_context_spec.rb +12 -12
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/typecast_spec.rb +82 -3
- data/spec/flipper_spec.rb +7 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/skippable.rb +18 -0
- metadata +25 -3
- data/.github/workflows/release.yml +0 -44
@@ -5,8 +5,8 @@ module Flipper
|
|
5
5
|
# Public: Adapter that wraps another adapter and stores the operations.
|
6
6
|
#
|
7
7
|
# Useful in tests to verify calls and such. Never use outside of testing.
|
8
|
-
class OperationLogger
|
9
|
-
include
|
8
|
+
class OperationLogger
|
9
|
+
include Flipper::Adapter
|
10
10
|
|
11
11
|
class Operation
|
12
12
|
attr_reader :type, :args
|
@@ -18,6 +18,8 @@ module Flipper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
OperationTypes = [
|
21
|
+
:import,
|
22
|
+
:export,
|
21
23
|
:features,
|
22
24
|
:add,
|
23
25
|
:remove,
|
@@ -37,7 +39,6 @@ module Flipper
|
|
37
39
|
|
38
40
|
# Public
|
39
41
|
def initialize(adapter, operations = nil)
|
40
|
-
super(adapter)
|
41
42
|
@adapter = adapter
|
42
43
|
@name = :operation_logger
|
43
44
|
@operations = operations || []
|
@@ -98,6 +99,18 @@ module Flipper
|
|
98
99
|
@adapter.disable(feature, gate, thing)
|
99
100
|
end
|
100
101
|
|
102
|
+
# Public
|
103
|
+
def import(source)
|
104
|
+
@operations << Operation.new(:import, [source])
|
105
|
+
@adapter.import(source)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public
|
109
|
+
def export(format: :json, version: 1)
|
110
|
+
@operations << Operation.new(:export, [format, version])
|
111
|
+
@adapter.export(format: format, version: version)
|
112
|
+
end
|
113
|
+
|
101
114
|
# Public: Count the number of times a certain operation happened.
|
102
115
|
def count(type)
|
103
116
|
type(type).size
|
@@ -1,125 +1,2 @@
|
|
1
|
-
|
2
|
-
require '
|
3
|
-
require 'concurrent/utility/monotonic_time'
|
4
|
-
require 'concurrent/map'
|
5
|
-
|
6
|
-
module Flipper
|
7
|
-
module Adapters
|
8
|
-
class Poll
|
9
|
-
class Poller
|
10
|
-
attr_reader :thread, :pid, :mutex, :interval, :last_synced_at
|
11
|
-
|
12
|
-
def self.instances
|
13
|
-
@instances ||= Concurrent::Map.new
|
14
|
-
end
|
15
|
-
private_class_method :instances
|
16
|
-
|
17
|
-
def self.get(key, options = {})
|
18
|
-
instances.compute_if_absent(key) { new(options) }
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.reset
|
22
|
-
instances.each {|_,poller| poller.stop }.clear
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize(options = {})
|
26
|
-
@thread = nil
|
27
|
-
@pid = Process.pid
|
28
|
-
@mutex = Mutex.new
|
29
|
-
@adapter = Memory.new
|
30
|
-
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
31
|
-
@remote_adapter = options.fetch(:remote_adapter)
|
32
|
-
@interval = options.fetch(:interval, 10).to_f
|
33
|
-
@lock = Concurrent::ReadWriteLock.new
|
34
|
-
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
35
|
-
|
36
|
-
if @interval < 1
|
37
|
-
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
38
|
-
@interval = 1
|
39
|
-
end
|
40
|
-
|
41
|
-
@start_automatically = options.fetch(:start_automatically, true)
|
42
|
-
|
43
|
-
if options.fetch(:shutdown_automatically, true)
|
44
|
-
at_exit { stop }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def adapter
|
49
|
-
@lock.with_read_lock { Memory.new(@adapter.get_all.dup) }
|
50
|
-
end
|
51
|
-
|
52
|
-
def start
|
53
|
-
reset if forked?
|
54
|
-
ensure_worker_running
|
55
|
-
end
|
56
|
-
|
57
|
-
def stop
|
58
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
59
|
-
operation: :stop,
|
60
|
-
})
|
61
|
-
@thread&.kill
|
62
|
-
end
|
63
|
-
|
64
|
-
def run
|
65
|
-
loop do
|
66
|
-
sleep jitter
|
67
|
-
start = Concurrent.monotonic_time
|
68
|
-
begin
|
69
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
70
|
-
adapter = Memory.new
|
71
|
-
adapter.import(@remote_adapter)
|
72
|
-
|
73
|
-
@lock.with_write_lock { @adapter.import(adapter) }
|
74
|
-
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
75
|
-
end
|
76
|
-
rescue => exception
|
77
|
-
# you can instrument these using poller.flipper
|
78
|
-
end
|
79
|
-
|
80
|
-
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
81
|
-
sleep sleep_interval if sleep_interval.positive?
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
private
|
86
|
-
|
87
|
-
def jitter
|
88
|
-
rand
|
89
|
-
end
|
90
|
-
|
91
|
-
def forked?
|
92
|
-
pid != Process.pid
|
93
|
-
end
|
94
|
-
|
95
|
-
def ensure_worker_running
|
96
|
-
# Return early if thread is alive and avoid the mutex lock and unlock.
|
97
|
-
return if thread_alive?
|
98
|
-
|
99
|
-
# If another thread is starting worker thread, then return early so this
|
100
|
-
# thread can enqueue and move on with life.
|
101
|
-
return unless mutex.try_lock
|
102
|
-
|
103
|
-
begin
|
104
|
-
return if thread_alive?
|
105
|
-
@thread = Thread.new { run }
|
106
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
107
|
-
operation: :thread_start,
|
108
|
-
})
|
109
|
-
ensure
|
110
|
-
mutex.unlock
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def thread_alive?
|
115
|
-
@thread && @thread.alive?
|
116
|
-
end
|
117
|
-
|
118
|
-
def reset
|
119
|
-
@pid = Process.pid
|
120
|
-
mutex.unlock if mutex.locked?
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
1
|
+
warn "DEPRECATION WARNING: Flipper::Adapters::Poll::Poller is deprecated. Use Flipper::Poller instead."
|
2
|
+
require 'flipper/adapters/poll'
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'flipper/adapters/sync/synchronizer'
|
2
|
+
require 'flipper/poller'
|
2
3
|
|
3
4
|
module Flipper
|
4
5
|
module Adapters
|
@@ -6,6 +7,9 @@ module Flipper
|
|
6
7
|
extend Forwardable
|
7
8
|
include ::Flipper::Adapter
|
8
9
|
|
10
|
+
# Deprecated
|
11
|
+
Poller = ::Flipper::Poller
|
12
|
+
|
9
13
|
# Public: The name of the adapter.
|
10
14
|
attr_reader :name, :adapter, :poller
|
11
15
|
|
data/lib/flipper/dsl.rb
CHANGED
@@ -10,7 +10,7 @@ module Flipper
|
|
10
10
|
# Private: What is being used to instrument all the things.
|
11
11
|
attr_reader :instrumenter
|
12
12
|
|
13
|
-
def_delegators :@adapter, :memoize=, :memoizing
|
13
|
+
def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
|
14
14
|
|
15
15
|
# Public: Returns a new instance of the DSL.
|
16
16
|
#
|
@@ -272,10 +272,6 @@ module Flipper
|
|
272
272
|
adapter.features.map { |name| feature(name) }.to_set
|
273
273
|
end
|
274
274
|
|
275
|
-
def import(flipper)
|
276
|
-
adapter.import(flipper.adapter)
|
277
|
-
end
|
278
|
-
|
279
275
|
# Cloud DSL method that does nothing for open source version.
|
280
276
|
def sync
|
281
277
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "flipper/adapters/memory"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Export
|
5
|
+
attr_reader :contents, :format, :version
|
6
|
+
|
7
|
+
def initialize(contents:, format: :json, version: 1)
|
8
|
+
@contents = contents
|
9
|
+
@format = format
|
10
|
+
@version = version
|
11
|
+
end
|
12
|
+
|
13
|
+
def features
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def adapter
|
18
|
+
@adapter ||= Flipper::Adapters::Memory.new(features)
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
|
23
|
+
end
|
24
|
+
alias_method :==, :eql?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "flipper/exporters/json/v1"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Exporter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
FORMATTERS = {
|
8
|
+
json: {
|
9
|
+
1 => Flipper::Exporters::Json::V1,
|
10
|
+
}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def build(format: :json, version: 1)
|
14
|
+
FORMATTERS.fetch(format).fetch(version).new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "flipper/export"
|
2
|
+
require "flipper/typecast"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
# Raised when the contents of the export are not valid.
|
8
|
+
class InvalidError < StandardError; end
|
9
|
+
class JsonError < InvalidError; end
|
10
|
+
|
11
|
+
# Internal: JSON export class that knows how to build features hash
|
12
|
+
# from data.
|
13
|
+
class Export < ::Flipper::Export
|
14
|
+
def initialize(contents:, version: 1)
|
15
|
+
super contents: contents, version: version, format: :json
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: The features hash identical to calling get_all on adapter.
|
19
|
+
def features
|
20
|
+
@features ||= begin
|
21
|
+
features = JSON.parse(contents).fetch("features")
|
22
|
+
Typecast.features_hash(features)
|
23
|
+
rescue JSON::ParserError
|
24
|
+
raise JsonError
|
25
|
+
rescue
|
26
|
+
raise InvalidError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "json"
|
2
|
+
require "flipper/exporters/json/export"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
class V1
|
8
|
+
VERSION = 1
|
9
|
+
|
10
|
+
def call(adapter)
|
11
|
+
features = adapter.get_all
|
12
|
+
|
13
|
+
# Convert sets to arrays for json
|
14
|
+
features.each do |feature_key, gates|
|
15
|
+
gates.each do |key, value|
|
16
|
+
case value
|
17
|
+
when Set
|
18
|
+
features[feature_key][key] = value.to_a
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
json = JSON.dump({
|
24
|
+
version: VERSION,
|
25
|
+
features: features,
|
26
|
+
})
|
27
|
+
|
28
|
+
Json::Export.new(contents: json, version: VERSION)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/flipper/feature.rb
CHANGED
@@ -100,13 +100,12 @@ module Flipper
|
|
100
100
|
#
|
101
101
|
# Returns true if enabled, false if not.
|
102
102
|
def enabled?(thing = nil)
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
payload[:thing] = thing
|
103
|
+
thing = Types::Actor.wrap(thing) unless thing.nil?
|
104
|
+
|
105
|
+
instrument(:enabled?, thing: thing) do |payload|
|
107
106
|
context = FeatureCheckContext.new(
|
108
107
|
feature_name: @name,
|
109
|
-
values:
|
108
|
+
values: gate_values,
|
110
109
|
thing: thing
|
111
110
|
)
|
112
111
|
|
@@ -207,7 +206,7 @@ module Flipper
|
|
207
206
|
|
208
207
|
if values.boolean || values.percentage_of_time == 100
|
209
208
|
:on
|
210
|
-
elsif non_boolean_gates.detect { |gate| gate.enabled?(values
|
209
|
+
elsif non_boolean_gates.detect { |gate| gate.enabled?(values.send(gate.key)) }
|
211
210
|
:conditional
|
212
211
|
else
|
213
212
|
:off
|
@@ -232,7 +231,8 @@ module Flipper
|
|
232
231
|
|
233
232
|
# Public: Returns the raw gate values stored by the adapter.
|
234
233
|
def gate_values
|
235
|
-
|
234
|
+
adapter_values = adapter.get(self)
|
235
|
+
GateValues.new(adapter_values)
|
236
236
|
end
|
237
237
|
|
238
238
|
# Public: Get groups enabled for this feature.
|
@@ -290,7 +290,7 @@ module Flipper
|
|
290
290
|
# Returns an Array of Flipper::Gate instances.
|
291
291
|
def enabled_gates
|
292
292
|
values = gate_values
|
293
|
-
gates.select { |gate| gate.enabled?(values
|
293
|
+
gates.select { |gate| gate.enabled?(values.send(gate.key)) }
|
294
294
|
end
|
295
295
|
|
296
296
|
# Public: Get the names of the enabled gates.
|
@@ -339,20 +339,24 @@ module Flipper
|
|
339
339
|
#
|
340
340
|
# Returns an array of gates
|
341
341
|
def gates
|
342
|
-
@gates ||=
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
Gates::
|
348
|
-
|
342
|
+
@gates ||= gates_hash.values.freeze
|
343
|
+
end
|
344
|
+
|
345
|
+
def gates_hash
|
346
|
+
@gates_hash ||= {
|
347
|
+
boolean: Gates::Boolean.new,
|
348
|
+
actor: Gates::Actor.new,
|
349
|
+
percentage_of_actors: Gates::PercentageOfActors.new,
|
350
|
+
percentage_of_time: Gates::PercentageOfTime.new,
|
351
|
+
group: Gates::Group.new,
|
352
|
+
}.freeze
|
349
353
|
end
|
350
354
|
|
351
355
|
# Public: Find a gate by name.
|
352
356
|
#
|
353
357
|
# Returns a Flipper::Gate if found, nil if not.
|
354
358
|
def gate(name)
|
355
|
-
|
359
|
+
gates_hash[name.to_sym]
|
356
360
|
end
|
357
361
|
|
358
362
|
# Public: Find the gate that protects a thing.
|
@@ -368,8 +372,8 @@ module Flipper
|
|
368
372
|
private
|
369
373
|
|
370
374
|
# Private: Instrument a feature operation.
|
371
|
-
def instrument(operation)
|
372
|
-
@instrumenter.instrument(InstrumentationName) do |payload|
|
375
|
+
def instrument(operation, initial_payload = {})
|
376
|
+
@instrumenter.instrument(InstrumentationName, initial_payload) do |payload|
|
373
377
|
payload[:feature_name] = name
|
374
378
|
payload[:operation] = operation
|
375
379
|
payload[:result] = yield(payload) if block_given?
|
@@ -10,10 +10,10 @@ module Flipper
|
|
10
10
|
# Public: The thing we want to know if a feature is enabled for.
|
11
11
|
attr_reader :thing
|
12
12
|
|
13
|
-
def initialize(
|
14
|
-
@feature_name =
|
15
|
-
@values =
|
16
|
-
@thing =
|
13
|
+
def initialize(feature_name:, values:, thing:)
|
14
|
+
@feature_name = feature_name
|
15
|
+
@values = values
|
16
|
+
@thing = thing
|
17
17
|
end
|
18
18
|
|
19
19
|
# Public: Convenience method for groups value like Feature has.
|
data/lib/flipper/gate_values.rb
CHANGED
@@ -3,16 +3,6 @@ require 'flipper/typecast'
|
|
3
3
|
|
4
4
|
module Flipper
|
5
5
|
class GateValues
|
6
|
-
# Private: Array of instance variables that are readable through the []
|
7
|
-
# instance method.
|
8
|
-
LegitIvars = {
|
9
|
-
'boolean' => '@boolean',
|
10
|
-
'actors' => '@actors',
|
11
|
-
'groups' => '@groups',
|
12
|
-
'percentage_of_time' => '@percentage_of_time',
|
13
|
-
'percentage_of_actors' => '@percentage_of_actors',
|
14
|
-
}.freeze
|
15
|
-
|
16
6
|
attr_reader :boolean
|
17
7
|
attr_reader :actors
|
18
8
|
attr_reader :groups
|
@@ -27,12 +17,6 @@ module Flipper
|
|
27
17
|
@percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
|
28
18
|
end
|
29
19
|
|
30
|
-
def [](key)
|
31
|
-
if ivar = LegitIvars[key.to_s]
|
32
|
-
instance_variable_get(ivar)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
20
|
def eql?(other)
|
37
21
|
self.class.eql?(other.class) &&
|
38
22
|
boolean == other.boolean &&
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -23,18 +23,8 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
25
|
def open?(context)
|
26
|
-
|
27
|
-
|
28
|
-
false
|
29
|
-
else
|
30
|
-
if protects?(context.thing)
|
31
|
-
actor = wrap(context.thing)
|
32
|
-
enabled_actor_ids = value
|
33
|
-
enabled_actor_ids.include?(actor.value)
|
34
|
-
else
|
35
|
-
false
|
36
|
-
end
|
37
|
-
end
|
26
|
+
return false if context.thing.nil?
|
27
|
+
context.values.actors.include?(context.thing.value)
|
38
28
|
end
|
39
29
|
|
40
30
|
def wrap(thing)
|
data/lib/flipper/gates/group.rb
CHANGED
@@ -23,14 +23,10 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
25
|
def open?(context)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
value.any? do |name|
|
31
|
-
group = Flipper.group(name)
|
32
|
-
group.match?(context.thing, context)
|
33
|
-
end
|
26
|
+
return false if context.thing.nil?
|
27
|
+
|
28
|
+
context.values.groups.any? do |name|
|
29
|
+
Flipper.group(name).match?(context.thing, context)
|
34
30
|
end
|
35
31
|
end
|
36
32
|
|
@@ -21,21 +21,19 @@ module Flipper
|
|
21
21
|
value > 0
|
22
22
|
end
|
23
23
|
|
24
|
+
# Private: this constant is used to support up to 3 decimal places
|
25
|
+
# in percentages.
|
26
|
+
SCALING_FACTOR = 1_000
|
27
|
+
private_constant :SCALING_FACTOR
|
28
|
+
|
24
29
|
# Internal: Checks if the gate is open for a thing.
|
25
30
|
#
|
26
31
|
# Returns true if gate open for thing, false if not.
|
27
32
|
def open?(context)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
id = "#{context.feature_name}#{actor.value}"
|
33
|
-
# this is to support up to 3 decimal places in percentages
|
34
|
-
scaling_factor = 1_000
|
35
|
-
Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
|
36
|
-
else
|
37
|
-
false
|
38
|
-
end
|
33
|
+
return false if context.thing.nil?
|
34
|
+
|
35
|
+
id = "#{context.feature_name}#{context.thing.value}"
|
36
|
+
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
|
39
37
|
end
|
40
38
|
|
41
39
|
def protects?(thing)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'concurrent/utility/monotonic_time'
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
5
|
+
|
6
|
+
module Flipper
|
7
|
+
class Poller
|
8
|
+
attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
|
9
|
+
|
10
|
+
def self.instances
|
11
|
+
@instances ||= Concurrent::Map.new
|
12
|
+
end
|
13
|
+
private_class_method :instances
|
14
|
+
|
15
|
+
def self.get(key, options = {})
|
16
|
+
instances.compute_if_absent(key) { new(options) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.reset
|
20
|
+
instances.each {|_,poller| poller.stop }.clear
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@thread = nil
|
25
|
+
@pid = Process.pid
|
26
|
+
@mutex = Mutex.new
|
27
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
28
|
+
@remote_adapter = options.fetch(:remote_adapter)
|
29
|
+
@interval = options.fetch(:interval, 10).to_f
|
30
|
+
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
31
|
+
@adapter = Adapters::Memory.new
|
32
|
+
|
33
|
+
if @interval < 1
|
34
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
35
|
+
@interval = 1
|
36
|
+
end
|
37
|
+
|
38
|
+
@start_automatically = options.fetch(:start_automatically, true)
|
39
|
+
|
40
|
+
if options.fetch(:shutdown_automatically, true)
|
41
|
+
at_exit { stop }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
reset if forked?
|
47
|
+
ensure_worker_running
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
52
|
+
operation: :stop,
|
53
|
+
})
|
54
|
+
@thread&.kill
|
55
|
+
end
|
56
|
+
|
57
|
+
def run
|
58
|
+
loop do
|
59
|
+
sleep jitter
|
60
|
+
start = Concurrent.monotonic_time
|
61
|
+
begin
|
62
|
+
sync
|
63
|
+
rescue => exception
|
64
|
+
# you can instrument these using poller.flipper
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
68
|
+
sleep sleep_interval if sleep_interval.positive?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def sync
|
73
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
74
|
+
@adapter.import @remote_adapter
|
75
|
+
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def jitter
|
82
|
+
rand
|
83
|
+
end
|
84
|
+
|
85
|
+
def forked?
|
86
|
+
pid != Process.pid
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_worker_running
|
90
|
+
# Return early if thread is alive and avoid the mutex lock and unlock.
|
91
|
+
return if thread_alive?
|
92
|
+
|
93
|
+
# If another thread is starting worker thread, then return early so this
|
94
|
+
# thread can enqueue and move on with life.
|
95
|
+
return unless mutex.try_lock
|
96
|
+
|
97
|
+
begin
|
98
|
+
return if thread_alive?
|
99
|
+
@thread = Thread.new { run }
|
100
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
101
|
+
operation: :thread_start,
|
102
|
+
})
|
103
|
+
ensure
|
104
|
+
mutex.unlock
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def thread_alive?
|
109
|
+
@thread && @thread.alive?
|
110
|
+
end
|
111
|
+
|
112
|
+
def reset
|
113
|
+
@pid = Process.pid
|
114
|
+
mutex.unlock if mutex.locked?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|