flipper 1.0.0 → 1.3.6
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/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +50 -7
- data/.github/workflows/examples.yml +50 -8
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -584
- data/Gemfile +15 -8
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +6 -3
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- 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 +0 -4
- data/lib/flipper/adapters/http/client.rb +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +19 -14
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- 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 +131 -54
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -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 +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +42 -3
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- 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 +9 -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 +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +4 -1
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +9 -8
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
- data/lib/flipper/test/shared_adapter_test.rb +41 -22
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +41 -2
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -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/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +135 -74
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- 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 +166 -0
- data/spec/flipper/cloud/configuration_spec.rb +39 -57
- data/spec/flipper/cloud/dsl_spec.rb +6 -6
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- 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 +31 -25
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -3
- data/spec/flipper/engine_spec.rb +226 -42
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- 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_spec.rb +380 -10
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -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 +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +91 -3
- data/spec/spec_helper.rb +17 -5
- 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/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +145 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- data/spec/support/climate_control.rb +0 -7
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'concurrent/map'
|
|
2
|
+
require 'concurrent/atomic/atomic_fixnum'
|
|
3
|
+
|
|
4
|
+
module Flipper
|
|
5
|
+
module Cloud
|
|
6
|
+
class Telemetry
|
|
7
|
+
class MetricStorage
|
|
8
|
+
def initialize
|
|
9
|
+
@storage = Concurrent::Map.new { |h, k| h[k] = Concurrent::AtomicFixnum.new(0) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def increment(metric)
|
|
13
|
+
@storage[metric].increment
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def drain
|
|
17
|
+
metrics = {}
|
|
18
|
+
@storage.keys.each do |metric|
|
|
19
|
+
metrics[metric] = @storage.delete(metric).value
|
|
20
|
+
end
|
|
21
|
+
metrics.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def empty?
|
|
25
|
+
@storage.empty?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
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(5) { 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.instrument "telemetry_error.#{Flipper::InstrumentationNamespace}", exception: exception, request_id: request_id
|
|
55
|
+
@cloud_configuration.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def retry_with_backoff(attempts, &block)
|
|
59
|
+
result, caught_exception = nil
|
|
60
|
+
should_retry = false
|
|
61
|
+
attempts_remaining = attempts - 1
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
result, should_retry = yield
|
|
65
|
+
return [result, nil] unless should_retry
|
|
66
|
+
rescue => error
|
|
67
|
+
@cloud_configuration.instrument "telemetry_retry.#{Flipper::InstrumentationNamespace}", attempts_remaining: attempts_remaining, exception: error
|
|
68
|
+
@cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
|
|
69
|
+
should_retry = true
|
|
70
|
+
caught_exception = error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if should_retry && attempts_remaining > 0
|
|
74
|
+
sleep @backoff_policy.next_interval.to_f / 1000
|
|
75
|
+
retry_with_backoff attempts_remaining, &block
|
|
76
|
+
else
|
|
77
|
+
[result, caught_exception]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def submit(body)
|
|
82
|
+
client = @cloud_configuration.http_client
|
|
83
|
+
client.add_header "schema-version", SCHEMA_VERSION
|
|
84
|
+
client.add_header "content-encoding", GZIP_ENCODING
|
|
85
|
+
|
|
86
|
+
response = client.post PATH, body
|
|
87
|
+
code = response.code.to_i
|
|
88
|
+
|
|
89
|
+
# Raise error and retry for retriable status codes.
|
|
90
|
+
# FIXME: what about redirects?
|
|
91
|
+
if code < 200 || code == 429 || code >= 500
|
|
92
|
+
raise Error.new(request_id, response)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
response
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
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(1, {
|
|
79
|
+
max_queue: 20, # ~ 20 minutes of data at 1 minute intervals
|
|
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
|
data/lib/flipper/cloud.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Flipper
|
|
|
24
24
|
env_key = options.fetch(:env_key, 'flipper')
|
|
25
25
|
memoizer_options = options.fetch(:memoizer_options, {})
|
|
26
26
|
|
|
27
|
-
app = ->(_) { [404, {
|
|
27
|
+
app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
|
|
28
28
|
builder = Rack::Builder.new
|
|
29
29
|
yield builder if block_given?
|
|
30
30
|
builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
|
|
@@ -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
|
@@ -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.
|
|
@@ -219,6 +256,15 @@ module Flipper
|
|
|
219
256
|
Flipper.group(name)
|
|
220
257
|
end
|
|
221
258
|
|
|
259
|
+
# Public: Gets the expression for the feature.
|
|
260
|
+
#
|
|
261
|
+
# name - The String or Symbol name of the feature.
|
|
262
|
+
#
|
|
263
|
+
# Returns an instance of Flipper::Expression.
|
|
264
|
+
def expression(name)
|
|
265
|
+
feature(name).expression
|
|
266
|
+
end
|
|
267
|
+
|
|
222
268
|
# Public: Returns a Set of the known features for this adapter.
|
|
223
269
|
#
|
|
224
270
|
# Returns Set of Flipper::Feature instances.
|
|
@@ -226,6 +272,11 @@ module Flipper
|
|
|
226
272
|
adapter.features.map { |name| feature(name) }.to_set
|
|
227
273
|
end
|
|
228
274
|
|
|
275
|
+
# Public: Does this adapter support writes or not.
|
|
276
|
+
def read_only?
|
|
277
|
+
adapter.read_only?
|
|
278
|
+
end
|
|
279
|
+
|
|
229
280
|
# Cloud DSL method that does nothing for open source version.
|
|
230
281
|
def sync
|
|
231
282
|
end
|
data/lib/flipper/engine.rb
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
module Flipper
|
|
2
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
|
+
|
|
3
17
|
paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
|
|
4
18
|
|
|
5
19
|
config.before_configuration do
|
|
@@ -9,17 +23,25 @@ module Flipper
|
|
|
9
23
|
preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
|
|
10
24
|
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
|
11
25
|
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
|
|
12
|
-
cloud_path: "_flipper"
|
|
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?),
|
|
13
30
|
)
|
|
14
31
|
end
|
|
15
32
|
|
|
16
|
-
initializer "flipper.
|
|
33
|
+
initializer "flipper.properties" do
|
|
17
34
|
ActiveSupport.on_load(:active_record) do
|
|
18
|
-
|
|
35
|
+
require "flipper/model/active_record"
|
|
36
|
+
ActiveRecord::Base.include Flipper::Model::ActiveRecord
|
|
19
37
|
end
|
|
20
38
|
end
|
|
21
39
|
|
|
22
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
|
+
|
|
23
45
|
require 'flipper/cloud' if cloud?
|
|
24
46
|
|
|
25
47
|
Flipper.configure do |config|
|
|
@@ -44,6 +66,15 @@ module Flipper
|
|
|
44
66
|
end
|
|
45
67
|
end
|
|
46
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
|
+
|
|
47
78
|
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
|
48
79
|
flipper = app.config.flipper
|
|
49
80
|
|
|
@@ -56,8 +87,16 @@ module Flipper
|
|
|
56
87
|
end
|
|
57
88
|
end
|
|
58
89
|
|
|
90
|
+
initializer "flipper.test" do |app|
|
|
91
|
+
require "flipper/test_help" if app.config.flipper.test_help
|
|
92
|
+
end
|
|
93
|
+
|
|
59
94
|
def cloud?
|
|
60
95
|
!!ENV["FLIPPER_CLOUD_TOKEN"]
|
|
61
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
|
|
62
101
|
end
|
|
63
102
|
end
|
data/lib/flipper/export.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Flipper
|
|
|
18
18
|
# Public: The features hash identical to calling get_all on adapter.
|
|
19
19
|
def features
|
|
20
20
|
@features ||= begin
|
|
21
|
-
features =
|
|
21
|
+
features = Typecast.from_json(contents).fetch("features")
|
|
22
22
|
Typecast.features_hash(features)
|
|
23
23
|
rescue JSON::ParserError
|
|
24
24
|
raise JsonError
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
class Expression
|
|
3
|
+
module Builder
|
|
4
|
+
def build(object)
|
|
5
|
+
Expression.build(object)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add(*expressions)
|
|
9
|
+
group? ? build(name => args + expressions.flatten) : any.add(*expressions)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove(*expressions)
|
|
13
|
+
group? ? build(name => args - expressions.flatten) : any.remove(*expressions)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def any
|
|
17
|
+
any? ? self : build({ Any: [self] })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def all
|
|
21
|
+
all? ? self : build({ All: [self] })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def equal(object)
|
|
25
|
+
build({ Equal: [self, object] })
|
|
26
|
+
end
|
|
27
|
+
alias eq equal
|
|
28
|
+
|
|
29
|
+
def not_equal(object)
|
|
30
|
+
build({ NotEqual: [self, object] })
|
|
31
|
+
end
|
|
32
|
+
alias neq not_equal
|
|
33
|
+
|
|
34
|
+
def greater_than(object)
|
|
35
|
+
build({ GreaterThan: [self, object] })
|
|
36
|
+
end
|
|
37
|
+
alias gt greater_than
|
|
38
|
+
|
|
39
|
+
def greater_than_or_equal_to(object)
|
|
40
|
+
build({ GreaterThanOrEqualTo: [self, object] })
|
|
41
|
+
end
|
|
42
|
+
alias gte greater_than_or_equal_to
|
|
43
|
+
alias greater_than_or_equal greater_than_or_equal_to
|
|
44
|
+
|
|
45
|
+
def less_than(object)
|
|
46
|
+
build({ LessThan: [self, object] })
|
|
47
|
+
end
|
|
48
|
+
alias lt less_than
|
|
49
|
+
|
|
50
|
+
def less_than_or_equal_to(object)
|
|
51
|
+
build({ LessThanOrEqualTo: [self, object] })
|
|
52
|
+
end
|
|
53
|
+
alias lte less_than_or_equal_to
|
|
54
|
+
alias less_than_or_equal less_than_or_equal_to
|
|
55
|
+
|
|
56
|
+
def percentage_of_actors(object)
|
|
57
|
+
build({ PercentageOfActors: [self, build(object)] })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def any?
|
|
61
|
+
is_a?(Expression) && function == Expressions::Any
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def all?
|
|
65
|
+
is_a?(Expression) && function == Expressions::All
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def group?
|
|
69
|
+
any? || all?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
class Expression
|
|
3
|
+
# Public: A constant value like a "string", Number (1, 3.5), Boolean (true, false).
|
|
4
|
+
#
|
|
5
|
+
# Implements the same interface as Expression
|
|
6
|
+
class Constant
|
|
7
|
+
include Expression::Builder
|
|
8
|
+
|
|
9
|
+
attr_reader :value
|
|
10
|
+
|
|
11
|
+
def initialize(value)
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def evaluate(_ = nil)
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def eql?(other)
|
|
20
|
+
other.is_a?(self.class) && other.value == value
|
|
21
|
+
end
|
|
22
|
+
alias_method :==, :eql?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|