flipper 0.24.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +45 -14
- data/.github/workflows/examples.yml +39 -16
- data/Changelog.md +2 -443
- data/Gemfile +19 -11
- data/README.md +31 -27
- data/Rakefile +6 -4
- 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 +27 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +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/cloud/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +1 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +10 -6
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +72 -0
- data/lib/flipper/adapters/http/client.rb +44 -20
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +31 -16
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +17 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +37 -0
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +263 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +102 -0
- 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/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +87 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- 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 +34 -6
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +7 -1
- data/lib/flipper/middleware/memoizer.rb +28 -22
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +118 -0
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
- data/lib/flipper/test/shared_adapter_test.rb +101 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +50 -11
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +137 -55
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +181 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +373 -0
- 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/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +436 -33
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +23 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +74 -24
- data/spec/flipper/model/active_record_spec.rb +61 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +93 -29
- data/spec/spec_helper.rb +8 -14
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +23 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +19 -2
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +223 -19
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -73
@@ -0,0 +1,98 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "flipper/typecast"
|
3
|
+
require "flipper/cloud/telemetry/backoff_policy"
|
4
|
+
|
5
|
+
module Flipper
|
6
|
+
module Cloud
|
7
|
+
class Telemetry
|
8
|
+
class Submitter
|
9
|
+
PATH = "/telemetry".freeze
|
10
|
+
SCHEMA_VERSION = "V1".freeze
|
11
|
+
GZIP_ENCODING = "gzip".freeze
|
12
|
+
|
13
|
+
Error = Class.new(StandardError) do
|
14
|
+
attr_reader :request_id, :response
|
15
|
+
|
16
|
+
def initialize(request_id, response)
|
17
|
+
@request_id = request_id
|
18
|
+
@response = response
|
19
|
+
super "Unexpected response code=#{response.code} request_id=#{request_id}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :cloud_configuration, :request_id, :backoff_policy
|
24
|
+
|
25
|
+
def initialize(cloud_configuration, backoff_policy: nil)
|
26
|
+
@cloud_configuration = cloud_configuration
|
27
|
+
@backoff_policy = backoff_policy || BackoffPolicy.new
|
28
|
+
@request_id = SecureRandom.uuid
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns Array of [response, error]. response and error could be nil
|
32
|
+
# but usually one or the other will be present.
|
33
|
+
def call(drained)
|
34
|
+
return if drained.empty?
|
35
|
+
body = to_body(drained)
|
36
|
+
return if body.nil? || body.empty?
|
37
|
+
retry_with_backoff(10) { submit(body) }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def to_body(drained)
|
43
|
+
enabled_metrics = drained.map { |metric, value|
|
44
|
+
metric.as_json(with: {"value" => value})
|
45
|
+
}
|
46
|
+
|
47
|
+
json = Typecast.to_json({
|
48
|
+
request_id: request_id,
|
49
|
+
enabled_metrics: enabled_metrics,
|
50
|
+
})
|
51
|
+
|
52
|
+
Typecast.to_gzip(json)
|
53
|
+
rescue => exception
|
54
|
+
@cloud_configuration.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
|
55
|
+
end
|
56
|
+
|
57
|
+
def retry_with_backoff(attempts, &block)
|
58
|
+
result, caught_exception = nil
|
59
|
+
should_retry = false
|
60
|
+
attempts_remaining = attempts - 1
|
61
|
+
|
62
|
+
begin
|
63
|
+
result, should_retry = yield
|
64
|
+
return [result, nil] unless should_retry
|
65
|
+
rescue => error
|
66
|
+
@cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
|
67
|
+
should_retry = true
|
68
|
+
caught_exception = error
|
69
|
+
end
|
70
|
+
|
71
|
+
if should_retry && attempts_remaining > 0
|
72
|
+
sleep @backoff_policy.next_interval.to_f / 1000
|
73
|
+
retry_with_backoff attempts_remaining, &block
|
74
|
+
else
|
75
|
+
[result, caught_exception]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def submit(body)
|
80
|
+
client = @cloud_configuration.http_client
|
81
|
+
client.add_header "schema-version", SCHEMA_VERSION
|
82
|
+
client.add_header "content-encoding", GZIP_ENCODING
|
83
|
+
|
84
|
+
response = client.post PATH, body
|
85
|
+
code = response.code.to_i
|
86
|
+
|
87
|
+
# Raise error and retry for retriable status codes.
|
88
|
+
# FIXME: what about redirects?
|
89
|
+
if code < 200 || code == 429 || code >= 500
|
90
|
+
raise Error.new(request_id, response)
|
91
|
+
end
|
92
|
+
|
93
|
+
response
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require "concurrent/map"
|
2
|
+
require "concurrent/timer_task"
|
3
|
+
require "concurrent/executor/fixed_thread_pool"
|
4
|
+
require "flipper/typecast"
|
5
|
+
require "flipper/cloud/telemetry/metric"
|
6
|
+
require "flipper/cloud/telemetry/metric_storage"
|
7
|
+
require "flipper/cloud/telemetry/submitter"
|
8
|
+
|
9
|
+
module Flipper
|
10
|
+
module Cloud
|
11
|
+
class Telemetry
|
12
|
+
# Internal: Map of instances of telemetry.
|
13
|
+
def self.instances
|
14
|
+
@instances ||= Concurrent::Map.new
|
15
|
+
end
|
16
|
+
private_class_method :instances
|
17
|
+
|
18
|
+
def self.reset
|
19
|
+
instances.each { |_, instance| instance.stop }.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
# Internal: Fetch an instance of telemetry once per process per url +
|
23
|
+
# token (aka cloud endpoint). Should only ever be one instance unless you
|
24
|
+
# are doing some funky stuff.
|
25
|
+
def self.instance_for(cloud_configuration)
|
26
|
+
instances.compute_if_absent(cloud_configuration.url + cloud_configuration.token) do
|
27
|
+
new(cloud_configuration)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: The cloud configuration to use for this telemetry instance.
|
32
|
+
attr_reader :cloud_configuration
|
33
|
+
|
34
|
+
# Internal: Where the metrics are stored between cloud submissions.
|
35
|
+
attr_reader :metric_storage
|
36
|
+
|
37
|
+
# Internal: The pool of background threads that submits metrics to cloud.
|
38
|
+
attr_reader :pool
|
39
|
+
|
40
|
+
# Internal: The timer that triggers draining the metrics to the pool.
|
41
|
+
attr_reader :timer
|
42
|
+
|
43
|
+
# Internal: The interval in seconds for how often telemetry should be sent to cloud.
|
44
|
+
attr_reader :interval
|
45
|
+
|
46
|
+
# Internal: The timeout in seconds for how long to wait for the pool to shutdown.
|
47
|
+
attr_reader :shutdown_timeout
|
48
|
+
|
49
|
+
# Internal: The proc that is called to submit metrics to cloud.
|
50
|
+
attr_accessor :submitter
|
51
|
+
|
52
|
+
def initialize(cloud_configuration)
|
53
|
+
@pid = $$
|
54
|
+
@cloud_configuration = cloud_configuration
|
55
|
+
self.interval = ENV.fetch("FLIPPER_TELEMETRY_INTERVAL", 60).to_f
|
56
|
+
self.shutdown_timeout = ENV.fetch("FLIPPER_TELEMETRY_SHUTDOWN_TIMEOUT", 5).to_f
|
57
|
+
self.submitter = ->(drained) { Submitter.new(@cloud_configuration).call(drained) }
|
58
|
+
start
|
59
|
+
at_exit { stop }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Records telemetry events based on active support notifications.
|
63
|
+
def record(name, payload)
|
64
|
+
return unless name == Flipper::Feature::InstrumentationName
|
65
|
+
return unless payload[:operation] == :enabled?
|
66
|
+
detect_forking
|
67
|
+
|
68
|
+
metric = Metric.new(payload[:feature_name].to_s.freeze, payload[:result])
|
69
|
+
@metric_storage.increment metric
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Start all the tasks and setup new metric storage.
|
73
|
+
def start
|
74
|
+
info "action=start"
|
75
|
+
|
76
|
+
@metric_storage = MetricStorage.new
|
77
|
+
|
78
|
+
@pool = Concurrent::FixedThreadPool.new(2, {
|
79
|
+
max_queue: 5,
|
80
|
+
fallback_policy: :discard,
|
81
|
+
name: "flipper-telemetry-post-to-cloud-pool".freeze,
|
82
|
+
})
|
83
|
+
|
84
|
+
@timer = Concurrent::TimerTask.execute({
|
85
|
+
execution_interval: interval,
|
86
|
+
name: "flipper-telemetry-post-to-pool-timer".freeze,
|
87
|
+
}) { post_to_pool }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Shuts down all the tasks and tries to flush any remaining info to Cloud.
|
91
|
+
def stop
|
92
|
+
info "action=stop"
|
93
|
+
|
94
|
+
if @timer
|
95
|
+
debug "action=timer_shutdown_start"
|
96
|
+
@timer.shutdown
|
97
|
+
# no need to wait long for timer, all it does is drain in memory metric
|
98
|
+
# storage and post to the pool of background workers
|
99
|
+
timer_termination_result = @timer.wait_for_termination(1)
|
100
|
+
@timer.kill unless timer_termination_result
|
101
|
+
debug "action=timer_shutdown_end result=#{timer_termination_result}"
|
102
|
+
end
|
103
|
+
|
104
|
+
if @pool
|
105
|
+
post_to_pool # one last drain
|
106
|
+
debug "action=pool_shutdown_start"
|
107
|
+
@pool.shutdown
|
108
|
+
pool_termination_result = @pool.wait_for_termination(@shutdown_timeout)
|
109
|
+
@pool.kill unless pool_termination_result
|
110
|
+
debug "action=pool_shutdown_end result=#{pool_termination_result}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Restart all the tasks and reset the storage.
|
115
|
+
def restart
|
116
|
+
stop
|
117
|
+
start
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Sets the interval in seconds for how often telemetry should be sent to cloud.
|
121
|
+
def interval=(value)
|
122
|
+
new_interval = [Typecast.to_float(value), 10].max
|
123
|
+
@timer&.execution_interval = new_interval
|
124
|
+
@interval = new_interval
|
125
|
+
end
|
126
|
+
|
127
|
+
# Internal: Sets the timeout in seconds for how long to wait for the pool to shutdown.
|
128
|
+
def shutdown_timeout=(value)
|
129
|
+
new_shutdown_timeout = [Typecast.to_float(value), 0.1].max
|
130
|
+
@shutdown_timeout = new_shutdown_timeout
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def detect_forking
|
136
|
+
if @pid != $$
|
137
|
+
info "action=fork_detected pid_was#{@pid} pid_is=#{$$}"
|
138
|
+
restart
|
139
|
+
@pid = $$
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Drains the metric storage and enqueues the metrics to be posted to cloud.
|
144
|
+
def post_to_pool
|
145
|
+
drained = @metric_storage.drain
|
146
|
+
return if drained.empty?
|
147
|
+
debug "action=post_to_pool metrics=#{drained.size}"
|
148
|
+
@pool.post { post_to_cloud(drained) }
|
149
|
+
rescue => error
|
150
|
+
error "action=post_to_pool error=#{error.inspect}"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Posts the drained metrics to cloud.
|
154
|
+
def post_to_cloud(drained)
|
155
|
+
debug "action=post_to_cloud metrics=#{drained.size}"
|
156
|
+
response, error = submitter.call(drained)
|
157
|
+
debug "action=post_to_cloud response=#{response.inspect} body=#{response&.body.inspect} error=#{error.inspect}"
|
158
|
+
|
159
|
+
# Some of the errors are response code errors which have a response and
|
160
|
+
# thus may have a telemetry-interval header for us to respect.
|
161
|
+
response ||= error.response if error && error.respond_to?(:response)
|
162
|
+
|
163
|
+
if response
|
164
|
+
if Flipper::Typecast.to_boolean(response["telemetry-shutdown"])
|
165
|
+
debug "action=telemetry_shutdown message=The server has requested that telemetry be shut down."
|
166
|
+
stop
|
167
|
+
return
|
168
|
+
end
|
169
|
+
|
170
|
+
if interval = response["telemetry-interval"]
|
171
|
+
self.interval = interval.to_f
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue => error
|
175
|
+
error "action=post_to_cloud error=#{error.inspect}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def error(message)
|
179
|
+
@cloud_configuration.log message, level: :error
|
180
|
+
end
|
181
|
+
|
182
|
+
def info(message)
|
183
|
+
@cloud_configuration.log message, level: :info
|
184
|
+
end
|
185
|
+
|
186
|
+
def debug(message)
|
187
|
+
@cloud_configuration.log message
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "flipper"
|
2
|
+
require "flipper/middleware/setup_env"
|
3
|
+
require "flipper/middleware/memoizer"
|
4
|
+
require "flipper/cloud/configuration"
|
5
|
+
require "flipper/cloud/dsl"
|
6
|
+
require "flipper/cloud/middleware"
|
7
|
+
|
8
|
+
module Flipper
|
9
|
+
module Cloud
|
10
|
+
# Public: Returns a new Flipper instance with an http adapter correctly
|
11
|
+
# configured for flipper cloud.
|
12
|
+
#
|
13
|
+
# token - The String token for the environment from the website.
|
14
|
+
# options - The Hash of options. See Flipper::Cloud::Configuration.
|
15
|
+
# block - The block that configuration will be yielded to allowing you to
|
16
|
+
# customize this cloud instance and its adapter.
|
17
|
+
def self.new(options = {})
|
18
|
+
configuration = Configuration.new(options)
|
19
|
+
yield configuration if block_given?
|
20
|
+
DSL.new(configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.app(flipper = nil, options = {})
|
24
|
+
env_key = options.fetch(:env_key, 'flipper')
|
25
|
+
memoizer_options = options.fetch(:memoizer_options, {})
|
26
|
+
|
27
|
+
app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
|
28
|
+
builder = Rack::Builder.new
|
29
|
+
yield builder if block_given?
|
30
|
+
builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
|
31
|
+
builder.use Flipper::Middleware::Memoizer, memoizer_options.merge(env_key: env_key)
|
32
|
+
builder.use Flipper::Cloud::Middleware, env_key: env_key
|
33
|
+
builder.run app
|
34
|
+
klass = self
|
35
|
+
app = builder.to_app
|
36
|
+
app.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
|
37
|
+
app
|
38
|
+
end
|
39
|
+
|
40
|
+
# Private: Configure Flipper to use Cloud by default
|
41
|
+
def self.set_default
|
42
|
+
if ENV["FLIPPER_CLOUD_TOKEN"]
|
43
|
+
Flipper.configure do |config|
|
44
|
+
config.default do
|
45
|
+
self.new(local_adapter: config.adapter)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Flipper::Cloud.set_default
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Configuration
|
3
3
|
def initialize(options = {})
|
4
|
-
@
|
5
|
-
@
|
4
|
+
@builder = AdapterBuilder.new { store Flipper::Adapters::Memory }
|
5
|
+
@default = -> { Flipper.new(@builder.to_adapter) }
|
6
6
|
end
|
7
7
|
|
8
8
|
# The default adapter to use.
|
@@ -24,9 +24,20 @@ module Flipper
|
|
24
24
|
#
|
25
25
|
def adapter(&block)
|
26
26
|
if block_given?
|
27
|
-
@
|
27
|
+
@builder.store(block)
|
28
28
|
else
|
29
|
-
@
|
29
|
+
@builder.to_adapter
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# An adapter to use to augment the primary storage adapter. See `AdapterBuilder#use`
|
34
|
+
if RUBY_VERSION >= '3.0'
|
35
|
+
def use(klass, *args, **kwargs, &block)
|
36
|
+
@builder.use(klass, *args, **kwargs, &block)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
def use(klass, *args, &block)
|
40
|
+
@builder.use(klass, *args, &block)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
@@ -54,5 +65,15 @@ module Flipper
|
|
54
65
|
@default.call
|
55
66
|
end
|
56
67
|
end
|
68
|
+
|
69
|
+
def statsd
|
70
|
+
require 'flipper/instrumentation/statsd_subscriber'
|
71
|
+
Flipper::Instrumentation::StatsdSubscriber.client
|
72
|
+
end
|
73
|
+
|
74
|
+
def statsd=(client)
|
75
|
+
require "flipper/instrumentation/statsd"
|
76
|
+
Flipper::Instrumentation::StatsdSubscriber.client = client
|
77
|
+
end
|
57
78
|
end
|
58
79
|
end
|
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
|
#
|
@@ -46,6 +46,25 @@ module Flipper
|
|
46
46
|
feature(name).enable(*args)
|
47
47
|
end
|
48
48
|
|
49
|
+
# Public: Enable a feature for an expression.
|
50
|
+
#
|
51
|
+
# name - The String or Symbol name of the feature.
|
52
|
+
# expression - a Flipper::Expression instance or a Hash.
|
53
|
+
#
|
54
|
+
# Returns result of Feature#enable.
|
55
|
+
def enable_expression(name, expression)
|
56
|
+
feature(name).enable_expression(expression)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: Add an expression to a feature.
|
60
|
+
#
|
61
|
+
# expression - an expression or Hash that can be converted to an expression.
|
62
|
+
#
|
63
|
+
# Returns result of enable.
|
64
|
+
def add_expression(name, expression)
|
65
|
+
feature(name).add_expression(expression)
|
66
|
+
end
|
67
|
+
|
49
68
|
# Public: Enable a feature for an actor.
|
50
69
|
#
|
51
70
|
# name - The String or Symbol name of the feature.
|
@@ -100,6 +119,24 @@ module Flipper
|
|
100
119
|
feature(name).disable(*args)
|
101
120
|
end
|
102
121
|
|
122
|
+
# Public: Disable expression for feature.
|
123
|
+
#
|
124
|
+
# name - The String or Symbol name of the feature.
|
125
|
+
#
|
126
|
+
# Returns result of Feature#disable.
|
127
|
+
def disable_expression(name)
|
128
|
+
feature(name).disable_expression
|
129
|
+
end
|
130
|
+
|
131
|
+
# Public: Remove an expression from a feature.
|
132
|
+
#
|
133
|
+
# expression - an Expression or Hash that can be converted to an expression.
|
134
|
+
#
|
135
|
+
# Returns result of enable.
|
136
|
+
def remove_expression(name, expression)
|
137
|
+
feature(name).remove_expression(expression)
|
138
|
+
end
|
139
|
+
|
103
140
|
# Public: Disable a feature for an actor.
|
104
141
|
#
|
105
142
|
# name - The String or Symbol name of the feature.
|
@@ -210,22 +247,6 @@ module Flipper
|
|
210
247
|
# Returns an instance of Flipper::Feature.
|
211
248
|
alias_method :[], :feature
|
212
249
|
|
213
|
-
# Public: Shortcut for getting a boolean type instance.
|
214
|
-
#
|
215
|
-
# value - The true or false value for the boolean.
|
216
|
-
#
|
217
|
-
# Returns a Flipper::Types::Boolean instance.
|
218
|
-
def boolean(value = true)
|
219
|
-
Types::Boolean.new(value)
|
220
|
-
end
|
221
|
-
|
222
|
-
# Public: Even shorter shortcut for getting a boolean type instance.
|
223
|
-
#
|
224
|
-
# value - The true or false value for the boolean.
|
225
|
-
#
|
226
|
-
# Returns a Flipper::Types::Boolean instance.
|
227
|
-
alias_method :bool, :boolean
|
228
|
-
|
229
250
|
# Public: Access a flipper group by name.
|
230
251
|
#
|
231
252
|
# name - The String or Symbol name of the feature.
|
@@ -235,35 +256,14 @@ module Flipper
|
|
235
256
|
Flipper.group(name)
|
236
257
|
end
|
237
258
|
|
238
|
-
# Public:
|
239
|
-
#
|
240
|
-
# thing - The object that you would like to wrap.
|
259
|
+
# Public: Gets the expression for the feature.
|
241
260
|
#
|
242
|
-
#
|
243
|
-
# Raises ArgumentError if thing does not respond to `flipper_id`.
|
244
|
-
def actor(thing)
|
245
|
-
Types::Actor.new(thing)
|
246
|
-
end
|
247
|
-
|
248
|
-
# Public: Shortcut for getting a percentage of time instance.
|
249
|
-
#
|
250
|
-
# number - The percentage of time that should be enabled.
|
251
|
-
#
|
252
|
-
# Returns Flipper::Types::PercentageOfTime.
|
253
|
-
def time(number)
|
254
|
-
Types::PercentageOfTime.new(number)
|
255
|
-
end
|
256
|
-
alias_method :percentage_of_time, :time
|
257
|
-
|
258
|
-
# Public: Shortcut for getting a percentage of actors instance.
|
259
|
-
#
|
260
|
-
# number - The percentage of actors that should be enabled.
|
261
|
+
# name - The String or Symbol name of the feature.
|
261
262
|
#
|
262
|
-
# Returns Flipper::
|
263
|
-
def
|
264
|
-
|
263
|
+
# Returns an instance of Flipper::Expression.
|
264
|
+
def expression(name)
|
265
|
+
feature(name).expression
|
265
266
|
end
|
266
|
-
alias_method :percentage_of_actors, :actors
|
267
267
|
|
268
268
|
# Public: Returns a Set of the known features for this adapter.
|
269
269
|
#
|
@@ -272,8 +272,9 @@ module Flipper
|
|
272
272
|
adapter.features.map { |name| feature(name) }.to_set
|
273
273
|
end
|
274
274
|
|
275
|
-
|
276
|
-
|
275
|
+
# Public: Does this adapter support writes or not.
|
276
|
+
def read_only?
|
277
|
+
adapter.read_only?
|
277
278
|
end
|
278
279
|
|
279
280
|
# Cloud DSL method that does nothing for open source version.
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Engine < Rails::Engine
|
3
|
+
def self.default_strict_value
|
4
|
+
value = ENV["FLIPPER_STRICT"]
|
5
|
+
if value.in?(["warn", "raise", "noop"])
|
6
|
+
value.to_sym
|
7
|
+
elsif value
|
8
|
+
Typecast.to_boolean(value) ? :raise : false
|
9
|
+
elsif Rails.env.production?
|
10
|
+
false
|
11
|
+
else
|
12
|
+
# Warn in development for now. Future versions may default to :raise in development and test
|
13
|
+
Rails.env.development? && :warn
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
|
18
|
+
|
19
|
+
config.before_configuration do
|
20
|
+
config.flipper = ActiveSupport::OrderedOptions.new.update(
|
21
|
+
env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
|
22
|
+
memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
|
23
|
+
preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
|
24
|
+
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
25
|
+
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
|
26
|
+
cloud_path: "_flipper",
|
27
|
+
strict: default_strict_value,
|
28
|
+
actor_limit: ENV["FLIPPER_ACTOR_LIMIT"]&.to_i || 100,
|
29
|
+
test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
initializer "flipper.properties" do
|
34
|
+
ActiveSupport.on_load(:active_record) do
|
35
|
+
require "flipper/model/active_record"
|
36
|
+
ActiveRecord::Base.include Flipper::Model::ActiveRecord
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
initializer "flipper.default", before: :load_config_initializers do |app|
|
41
|
+
# Load cloud secrets from Rails credentials
|
42
|
+
ENV["FLIPPER_CLOUD_TOKEN"] ||= app.credentials.dig(:flipper, :cloud_token)
|
43
|
+
ENV["FLIPPER_CLOUD_SYNC_SECRET"] ||= app.credentials.dig(:flipper, :cloud_sync_secret)
|
44
|
+
|
45
|
+
require 'flipper/cloud' if cloud?
|
46
|
+
|
47
|
+
Flipper.configure do |config|
|
48
|
+
config.default do
|
49
|
+
if cloud?
|
50
|
+
Flipper::Cloud.new(
|
51
|
+
local_adapter: config.adapter,
|
52
|
+
instrumenter: app.config.flipper.instrumenter
|
53
|
+
)
|
54
|
+
else
|
55
|
+
Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
initializer "flipper.log", after: :load_config_initializers do |app|
|
62
|
+
flipper = app.config.flipper
|
63
|
+
|
64
|
+
if flipper.log && flipper.instrumenter == ActiveSupport::Notifications
|
65
|
+
require "flipper/instrumentation/log_subscriber"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
initializer "flipper.adapters", after: :load_config_initializers do |app|
|
70
|
+
flipper = app.config.flipper
|
71
|
+
|
72
|
+
Flipper.configure do |config|
|
73
|
+
config.use Flipper::Adapters::Strict, flipper.strict if flipper.strict
|
74
|
+
config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
79
|
+
flipper = app.config.flipper
|
80
|
+
|
81
|
+
if flipper.memoize
|
82
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
83
|
+
env_key: flipper.env_key,
|
84
|
+
preload: flipper.preload,
|
85
|
+
if: flipper.memoize.respond_to?(:call) ? flipper.memoize : nil
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
initializer "flipper.test" do |app|
|
91
|
+
require "flipper/test_help" if app.config.flipper.test_help
|
92
|
+
end
|
93
|
+
|
94
|
+
def cloud?
|
95
|
+
!!ENV["FLIPPER_CLOUD_TOKEN"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.deprecated_rails_version?
|
99
|
+
Gem::Version.new(Rails.version) < Gem::Version.new(Flipper::NEXT_REQUIRED_RAILS_VERSION)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
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
|