flipper 0.26.0 → 0.27.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/.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
|