flipper 0.22.0 → 0.28.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/.codeclimate.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +26 -20
- data/.github/workflows/examples.yml +62 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Changelog.md +152 -3
- data/Dockerfile +1 -1
- data/Gemfile +9 -6
- data/README.md +15 -67
- data/Rakefile +4 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/docker-compose.yml +37 -34
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/dsl.rb +3 -3
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/flipper.gemspec +2 -2
- data/lib/flipper/actor.rb +4 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/dual_write.rb +10 -16
- data/lib/flipper/adapters/failover.rb +83 -0
- data/lib/flipper/adapters/failsafe.rb +76 -0
- data/lib/flipper/adapters/http/client.rb +18 -12
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +14 -4
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +27 -18
- 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 -0
- data/lib/flipper/adapters/poll.rb +39 -0
- data/lib/flipper/adapters/pstore.rb +2 -5
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/adapters/sync.rb +9 -15
- data/lib/flipper/dsl.rb +9 -11
- data/lib/flipper/errors.rb +3 -20
- 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 +32 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +2 -12
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/railtie.rb +23 -22
- 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/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +6 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/actor_spec.rb +10 -2
- data/spec/flipper/adapter_spec.rb +29 -4
- data/spec/flipper/adapters/dual_write_spec.rb +0 -2
- data/spec/flipper/adapters/failover_spec.rb +129 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http_spec.rb +64 -6
- data/spec/flipper/adapters/instrumented_spec.rb +28 -12
- data/spec/flipper/adapters/memoizable_spec.rb +30 -12
- data/spec/flipper/adapters/memory_spec.rb +3 -4
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +0 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync_spec.rb +0 -2
- data/spec/flipper/configuration_spec.rb +0 -1
- data/spec/flipper/dsl_spec.rb +38 -12
- 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 +17 -19
- data/spec/flipper/feature_spec.rb +76 -33
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +0 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +0 -23
- data/spec/flipper/middleware/setup_env_spec.rb +0 -2
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/railtie_spec.rb +73 -33
- data/spec/flipper/registry_spec.rb +0 -1
- data/spec/flipper/typecast_spec.rb +82 -4
- data/spec/flipper/types/actor_spec.rb +45 -46
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +2 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/flipper_integration_spec.rb +62 -51
- data/spec/flipper_spec.rb +7 -2
- data/spec/{helper.rb → spec_helper.rb} +4 -2
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +2 -6
- metadata +63 -19
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -137
- data/docs/api/README.md +0 -884
- data/docs/http/README.md +0 -36
- data/docs/read-only/README.md +0 -24
|
@@ -4,7 +4,7 @@ module Flipper
|
|
|
4
4
|
module Adapters
|
|
5
5
|
# Internal: Adapter that wraps another adapter and instruments all adapter
|
|
6
6
|
# operations.
|
|
7
|
-
class Instrumented
|
|
7
|
+
class Instrumented
|
|
8
8
|
include ::Flipper::Adapter
|
|
9
9
|
|
|
10
10
|
# Private: The name of instrumentation events.
|
|
@@ -24,7 +24,6 @@ module Flipper
|
|
|
24
24
|
# :instrumenter - What to use to instrument all the things.
|
|
25
25
|
#
|
|
26
26
|
def initialize(adapter, options = {})
|
|
27
|
-
super(adapter)
|
|
28
27
|
@adapter = adapter
|
|
29
28
|
@name = :instrumented
|
|
30
29
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
@@ -146,6 +145,30 @@ module Flipper
|
|
|
146
145
|
payload[:result] = @adapter.disable(feature, gate, thing)
|
|
147
146
|
end
|
|
148
147
|
end
|
|
148
|
+
|
|
149
|
+
def import(source)
|
|
150
|
+
default_payload = {
|
|
151
|
+
operation: :import,
|
|
152
|
+
adapter_name: @adapter.name,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
|
156
|
+
payload[:result] = @adapter.import(source)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def export(format: :json, version: 1)
|
|
161
|
+
default_payload = {
|
|
162
|
+
operation: :export,
|
|
163
|
+
adapter_name: @adapter.name,
|
|
164
|
+
format: format,
|
|
165
|
+
version: version,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
|
169
|
+
payload[:result] = @adapter.export(format: format, version: version)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
149
172
|
end
|
|
150
173
|
end
|
|
151
174
|
end
|
|
@@ -5,7 +5,7 @@ module Flipper
|
|
|
5
5
|
# Internal: Adapter that wraps another adapter with the ability to memoize
|
|
6
6
|
# adapter calls in memory. Used by flipper dsl and the memoizer middleware
|
|
7
7
|
# to make it possible to memoize adapter calls for the duration of a request.
|
|
8
|
-
class Memoizable
|
|
8
|
+
class Memoizable
|
|
9
9
|
include ::Flipper::Adapter
|
|
10
10
|
|
|
11
11
|
FeaturesKey = :flipper_features
|
|
@@ -27,7 +27,6 @@ module Flipper
|
|
|
27
27
|
|
|
28
28
|
# Public
|
|
29
29
|
def initialize(adapter, cache = nil)
|
|
30
|
-
super(adapter)
|
|
31
30
|
@adapter = adapter
|
|
32
31
|
@name = :memoizable
|
|
33
32
|
@cache = cache || {}
|
|
@@ -45,24 +44,20 @@ module Flipper
|
|
|
45
44
|
|
|
46
45
|
# Public
|
|
47
46
|
def add(feature)
|
|
48
|
-
|
|
49
|
-
expire_features_set
|
|
50
|
-
result
|
|
47
|
+
@adapter.add(feature).tap { expire_features_set }
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
# Public
|
|
54
51
|
def remove(feature)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
@adapter.remove(feature).tap do
|
|
53
|
+
expire_features_set
|
|
54
|
+
expire_feature(feature)
|
|
55
|
+
end
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
# Public
|
|
62
59
|
def clear(feature)
|
|
63
|
-
|
|
64
|
-
expire_feature(feature)
|
|
65
|
-
result
|
|
60
|
+
@adapter.clear(feature).tap { expire_feature(feature) }
|
|
66
61
|
end
|
|
67
62
|
|
|
68
63
|
# Public
|
|
@@ -124,16 +119,20 @@ module Flipper
|
|
|
124
119
|
|
|
125
120
|
# Public
|
|
126
121
|
def enable(feature, gate, thing)
|
|
127
|
-
|
|
128
|
-
expire_feature(feature)
|
|
129
|
-
result
|
|
122
|
+
@adapter.enable(feature, gate, thing).tap { expire_feature(feature) }
|
|
130
123
|
end
|
|
131
124
|
|
|
132
125
|
# Public
|
|
133
126
|
def disable(feature, gate, thing)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def import(source)
|
|
131
|
+
@adapter.import(source).tap { cache.clear if memoizing? }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def export(format: :json, version: 1)
|
|
135
|
+
@adapter.export(format: format, version: version)
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
# Internal: Turns local caching on/off.
|
|
@@ -149,6 +148,16 @@ module Flipper
|
|
|
149
148
|
!!@memoize
|
|
150
149
|
end
|
|
151
150
|
|
|
151
|
+
if RUBY_VERSION >= '3.0'
|
|
152
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
153
|
+
@adapter.send name, *args, **kwargs, &block
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
def method_missing(name, *args, &block)
|
|
157
|
+
@adapter.send name, *args, &block
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
152
161
|
private
|
|
153
162
|
|
|
154
163
|
def key_for(key)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "flipper/adapter"
|
|
2
|
+
require "flipper/typecast"
|
|
3
|
+
require 'concurrent/atomic/read_write_lock'
|
|
2
4
|
|
|
3
5
|
module Flipper
|
|
4
6
|
module Adapters
|
|
@@ -14,86 +16,93 @@ module Flipper
|
|
|
14
16
|
|
|
15
17
|
# Public
|
|
16
18
|
def initialize(source = nil)
|
|
17
|
-
@source = source
|
|
19
|
+
@source = Typecast.features_hash(source)
|
|
18
20
|
@name = :memory
|
|
21
|
+
@lock = Concurrent::ReadWriteLock.new
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# Public: The set of known features.
|
|
22
25
|
def features
|
|
23
|
-
@source.keys.to_set
|
|
26
|
+
@lock.with_read_lock { @source.keys }.to_set
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Public: Adds a feature to the set of known features.
|
|
27
30
|
def add(feature)
|
|
28
|
-
@source[feature.key] ||= default_config
|
|
31
|
+
@lock.with_write_lock { @source[feature.key] ||= default_config }
|
|
29
32
|
true
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
# Public: Removes a feature from the set of known features and clears
|
|
33
36
|
# all the values for the feature.
|
|
34
37
|
def remove(feature)
|
|
35
|
-
@source.delete(feature.key)
|
|
38
|
+
@lock.with_write_lock { @source.delete(feature.key) }
|
|
36
39
|
true
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
# Public: Clears all the gate values for a feature.
|
|
40
43
|
def clear(feature)
|
|
41
|
-
@source[feature.key] = default_config
|
|
44
|
+
@lock.with_write_lock { @source[feature.key] = default_config }
|
|
42
45
|
true
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
# Public
|
|
46
49
|
def get(feature)
|
|
47
|
-
@source[feature.key] || default_config
|
|
50
|
+
@lock.with_read_lock { @source[feature.key] } || default_config
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def get_multi(features)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
@lock.with_read_lock do
|
|
55
|
+
result = {}
|
|
56
|
+
features.each do |feature|
|
|
57
|
+
result[feature.key] = @source[feature.key] || default_config
|
|
58
|
+
end
|
|
59
|
+
result
|
|
54
60
|
end
|
|
55
|
-
result
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
def get_all
|
|
59
|
-
@source
|
|
64
|
+
@lock.with_read_lock { Typecast.features_hash(@source) }
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
# Public
|
|
63
68
|
def enable(feature, gate, thing)
|
|
64
|
-
@
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
@lock.with_write_lock do
|
|
70
|
+
@source[feature.key] ||= default_config
|
|
71
|
+
|
|
72
|
+
case gate.data_type
|
|
73
|
+
when :boolean
|
|
74
|
+
@source[feature.key] = default_config
|
|
75
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
|
76
|
+
when :integer
|
|
77
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
|
78
|
+
when :set
|
|
79
|
+
@source[feature.key][gate.key] << thing.value.to_s
|
|
80
|
+
else
|
|
81
|
+
raise "#{gate} is not supported by this adapter yet"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
true
|
|
76
85
|
end
|
|
77
|
-
|
|
78
|
-
true
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
# Public
|
|
82
89
|
def disable(feature, gate, thing)
|
|
83
|
-
@
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
@lock.with_write_lock do
|
|
91
|
+
@source[feature.key] ||= default_config
|
|
92
|
+
|
|
93
|
+
case gate.data_type
|
|
94
|
+
when :boolean
|
|
95
|
+
@source[feature.key] = default_config
|
|
96
|
+
when :integer
|
|
97
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
|
98
|
+
when :set
|
|
99
|
+
@source[feature.key][gate.key].delete thing.value.to_s
|
|
100
|
+
else
|
|
101
|
+
raise "#{gate} is not supported by this adapter yet"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
true
|
|
94
105
|
end
|
|
95
|
-
|
|
96
|
-
true
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
# Public
|
|
@@ -104,6 +113,14 @@ module Flipper
|
|
|
104
113
|
]
|
|
105
114
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
|
106
115
|
end
|
|
116
|
+
|
|
117
|
+
# Public: a more efficient implementation of import for this adapter
|
|
118
|
+
def import(source)
|
|
119
|
+
adapter = self.class.from(source)
|
|
120
|
+
get_all = Typecast.features_hash(adapter.get_all)
|
|
121
|
+
@lock.with_write_lock { @source.replace(get_all) }
|
|
122
|
+
true
|
|
123
|
+
end
|
|
107
124
|
end
|
|
108
125
|
end
|
|
109
126
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'flipper/adapters/sync/synchronizer'
|
|
2
|
+
require 'flipper/poller'
|
|
3
|
+
|
|
4
|
+
module Flipper
|
|
5
|
+
module Adapters
|
|
6
|
+
class Poll
|
|
7
|
+
extend Forwardable
|
|
8
|
+
include ::Flipper::Adapter
|
|
9
|
+
|
|
10
|
+
# Deprecated
|
|
11
|
+
Poller = ::Flipper::Poller
|
|
12
|
+
|
|
13
|
+
# Public: The name of the adapter.
|
|
14
|
+
attr_reader :name, :adapter, :poller
|
|
15
|
+
|
|
16
|
+
def_delegators :synced_adapter, :features, :get, :get_multi, :get_all, :add, :remove, :clear, :enable, :disable
|
|
17
|
+
|
|
18
|
+
def initialize(poller, adapter)
|
|
19
|
+
@name = :poll
|
|
20
|
+
@adapter = adapter
|
|
21
|
+
@poller = poller
|
|
22
|
+
@last_synced_at = 0
|
|
23
|
+
@poller.start
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def synced_adapter
|
|
29
|
+
@poller.start
|
|
30
|
+
poller_last_synced_at = @poller.last_synced_at.value
|
|
31
|
+
if poller_last_synced_at > @last_synced_at
|
|
32
|
+
Flipper::Adapters::Sync::Synchronizer.new(@adapter, @poller.adapter).call
|
|
33
|
+
@last_synced_at = poller_last_synced_at
|
|
34
|
+
end
|
|
35
|
+
@adapter
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -17,14 +17,11 @@ module Flipper
|
|
|
17
17
|
# Public: The path to where the file is stored.
|
|
18
18
|
attr_reader :path
|
|
19
19
|
|
|
20
|
-
# Public: PStore's thread_safe option.
|
|
21
|
-
attr_reader :thread_safe
|
|
22
|
-
|
|
23
20
|
# Public
|
|
24
|
-
def initialize(path = 'flipper.pstore', thread_safe =
|
|
21
|
+
def initialize(path = 'flipper.pstore', thread_safe = true)
|
|
22
|
+
@name = :pstore
|
|
25
23
|
@path = path
|
|
26
24
|
@store = ::PStore.new(path, thread_safe)
|
|
27
|
-
@name = :pstore
|
|
28
25
|
end
|
|
29
26
|
|
|
30
27
|
# Public: The set of known features.
|
|
@@ -7,11 +7,6 @@ module Flipper
|
|
|
7
7
|
# Private: Number of seconds between syncs (default: 10).
|
|
8
8
|
DEFAULT_INTERVAL = 10
|
|
9
9
|
|
|
10
|
-
# Private
|
|
11
|
-
def self.now
|
|
12
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
10
|
# Public: The Float or Integer number of seconds between invocations of
|
|
16
11
|
# the wrapped synchronizer.
|
|
17
12
|
attr_reader :interval
|
|
@@ -46,7 +41,7 @@ module Flipper
|
|
|
46
41
|
end
|
|
47
42
|
|
|
48
43
|
def now
|
|
49
|
-
|
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
50
45
|
end
|
|
51
46
|
end
|
|
52
47
|
end
|
|
@@ -37,7 +37,8 @@ module Flipper
|
|
|
37
37
|
# Sync all the gate values.
|
|
38
38
|
remote_get_all.each do |feature_key, remote_gates_hash|
|
|
39
39
|
feature = Feature.new(feature_key, @local)
|
|
40
|
-
|
|
40
|
+
# Check if feature_key is in hash before accessing to prevent unintended hash modification
|
|
41
|
+
local_gates_hash = local_get_all.key?(feature_key) ? local_get_all[feature_key] : @local.default_config
|
|
41
42
|
local_gate_values = GateValues.new(local_gates_hash)
|
|
42
43
|
remote_gate_values = GateValues.new(remote_gates_hash)
|
|
43
44
|
FeatureSynchronizer.new(feature, local_gate_values, remote_gate_values).call
|
|
@@ -58,33 +58,27 @@ module Flipper
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def add(feature)
|
|
61
|
-
|
|
62
|
-
@local.add(feature)
|
|
63
|
-
result
|
|
61
|
+
@remote.add(feature).tap { @local.add(feature) }
|
|
64
62
|
end
|
|
65
63
|
|
|
66
64
|
def remove(feature)
|
|
67
|
-
|
|
68
|
-
@local.remove(feature)
|
|
69
|
-
result
|
|
65
|
+
@remote.remove(feature).tap { @local.remove(feature) }
|
|
70
66
|
end
|
|
71
67
|
|
|
72
68
|
def clear(feature)
|
|
73
|
-
|
|
74
|
-
@local.clear(feature)
|
|
75
|
-
result
|
|
69
|
+
@remote.clear(feature).tap { @local.clear(feature) }
|
|
76
70
|
end
|
|
77
71
|
|
|
78
72
|
def enable(feature, gate, thing)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
@remote.enable(feature, gate, thing).tap do
|
|
74
|
+
@local.enable(feature, gate, thing)
|
|
75
|
+
end
|
|
82
76
|
end
|
|
83
77
|
|
|
84
78
|
def disable(feature, gate, thing)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
@remote.disable(feature, gate, thing).tap do
|
|
80
|
+
@local.disable(feature, gate, thing)
|
|
81
|
+
end
|
|
88
82
|
end
|
|
89
83
|
|
|
90
84
|
private
|
data/lib/flipper/dsl.rb
CHANGED
|
@@ -10,17 +10,19 @@ 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
|
#
|
|
17
17
|
# adapter - The adapter that this DSL instance should use.
|
|
18
18
|
# options - The Hash of options.
|
|
19
19
|
# :instrumenter - What should be used to instrument all the things.
|
|
20
|
+
# :memoize - Should adapter be wrapped by memoize adapter or not.
|
|
20
21
|
def initialize(adapter, options = {})
|
|
21
22
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
memoize = options.fetch(:memoize, true)
|
|
24
|
+
adapter = Adapters::Memoizable.new(adapter) if memoize
|
|
25
|
+
@adapter = adapter
|
|
24
26
|
@memoized_features = {}
|
|
25
27
|
end
|
|
26
28
|
|
|
@@ -235,12 +237,12 @@ module Flipper
|
|
|
235
237
|
|
|
236
238
|
# Public: Wraps an object as a flipper actor.
|
|
237
239
|
#
|
|
238
|
-
#
|
|
240
|
+
# actor - The object that you would like to wrap.
|
|
239
241
|
#
|
|
240
242
|
# Returns an instance of Flipper::Types::Actor.
|
|
241
|
-
# Raises ArgumentError if
|
|
242
|
-
def actor(
|
|
243
|
-
Types::Actor.new(
|
|
243
|
+
# Raises ArgumentError if actor does not respond to `flipper_id`.
|
|
244
|
+
def actor(actor)
|
|
245
|
+
Types::Actor.new(actor)
|
|
244
246
|
end
|
|
245
247
|
|
|
246
248
|
# Public: Shortcut for getting a percentage of time instance.
|
|
@@ -270,10 +272,6 @@ module Flipper
|
|
|
270
272
|
adapter.features.map { |name| feature(name) }.to_set
|
|
271
273
|
end
|
|
272
274
|
|
|
273
|
-
def import(flipper)
|
|
274
|
-
adapter.import(flipper.adapter)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
275
|
# Cloud DSL method that does nothing for open source version.
|
|
278
276
|
def sync
|
|
279
277
|
end
|
data/lib/flipper/errors.rb
CHANGED
|
@@ -2,25 +2,16 @@ module Flipper
|
|
|
2
2
|
# Top level error that all other errors inherit from.
|
|
3
3
|
class Error < StandardError; end
|
|
4
4
|
|
|
5
|
-
# Raised when gate can not be found for
|
|
5
|
+
# Raised when gate can not be found for an actor.
|
|
6
6
|
class GateNotFound < Error
|
|
7
|
-
def initialize(
|
|
8
|
-
super "Could not find gate for #{
|
|
7
|
+
def initialize(actor)
|
|
8
|
+
super "Could not find gate for #{actor.inspect}"
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Raised when attempting to declare a group name that has already been used.
|
|
13
13
|
class DuplicateGroup < Error; end
|
|
14
14
|
|
|
15
|
-
# Raised when default instance not configured but there is an attempt to
|
|
16
|
-
# use it.
|
|
17
|
-
class DefaultNotSet < Flipper::Error
|
|
18
|
-
def initialize(message = nil)
|
|
19
|
-
warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
|
|
20
|
-
super
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
15
|
# Raised when an invalid value is set to a configuration property
|
|
25
16
|
class InvalidConfigurationValue < Flipper::Error
|
|
26
17
|
def initialize(message = nil)
|
|
@@ -28,12 +19,4 @@ module Flipper
|
|
|
28
19
|
super(message || default)
|
|
29
20
|
end
|
|
30
21
|
end
|
|
31
|
-
|
|
32
|
-
# Raised when accessing a configuration property that has been deprecated
|
|
33
|
-
class ConfigurationDeprecated < Flipper::Error
|
|
34
|
-
def initialize(message = nil)
|
|
35
|
-
default = "The configuration property has been deprecated"
|
|
36
|
-
super(message || default)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
22
|
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
|