launchdarkly-server-sdk 7.0.2 → 8.4.2
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/README.md +9 -4
- data/lib/ldclient-rb/config.rb +50 -70
- data/lib/ldclient-rb/context.rb +65 -50
- data/lib/ldclient-rb/evaluation_detail.rb +5 -1
- data/lib/ldclient-rb/events.rb +81 -8
- data/lib/ldclient-rb/impl/big_segments.rb +1 -1
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +3 -3
- data/lib/ldclient-rb/impl/context_filter.rb +30 -9
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
- data/lib/ldclient-rb/impl/event_sender.rb +1 -0
- data/lib/ldclient-rb/impl/event_types.rb +61 -3
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +12 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +8 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +16 -3
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +19 -2
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +25 -3
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -3
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/in_memory_store.rb +7 -0
- data/lib/ldclient-rb/integrations/file_data.rb +1 -1
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +84 -15
- data/lib/ldclient-rb/integrations/test_data.rb +3 -3
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +11 -0
- data/lib/ldclient-rb/interfaces.rb +671 -0
- data/lib/ldclient-rb/ldclient.rb +313 -22
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/polling.rb +51 -5
- data/lib/ldclient-rb/reference.rb +11 -0
- data/lib/ldclient-rb/requestor.rb +5 -5
- data/lib/ldclient-rb/stream.rb +91 -29
- data/lib/ldclient-rb/util.rb +89 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +1 -0
- metadata +44 -6
@@ -0,0 +1,58 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "ldclient-rb/interfaces"
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
class FlagTracker
|
8
|
+
include LaunchDarkly::Interfaces::FlagTracker
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@broadcaster, :add_listener, :remove_listener
|
12
|
+
|
13
|
+
def initialize(broadcaster, eval_fn)
|
14
|
+
@broadcaster = broadcaster
|
15
|
+
@eval_fn = eval_fn
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_flag_value_change_listener(key, context, listener)
|
19
|
+
flag_change_listener = FlagValueChangeAdapter.new(key, context, listener, @eval_fn)
|
20
|
+
add_listener(flag_change_listener)
|
21
|
+
|
22
|
+
flag_change_listener
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# An adapter which turns a normal flag change listener into a flag value change listener.
|
27
|
+
#
|
28
|
+
class FlagValueChangeAdapter
|
29
|
+
# @param [Symbol] flag_key
|
30
|
+
# @param [LaunchDarkly::LDContext] context
|
31
|
+
# @param [#update] listener
|
32
|
+
# @param [#call] eval_fn
|
33
|
+
def initialize(flag_key, context, listener, eval_fn)
|
34
|
+
@flag_key = flag_key
|
35
|
+
@context = context
|
36
|
+
@listener = listener
|
37
|
+
@eval_fn = eval_fn
|
38
|
+
@value = Concurrent::AtomicReference.new(@eval_fn.call(@flag_key, @context))
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# @param [LaunchDarkly::Interfaces::FlagChange] flag_change
|
43
|
+
#
|
44
|
+
def update(flag_change)
|
45
|
+
return unless flag_change.key == @flag_key
|
46
|
+
|
47
|
+
new_eval = @eval_fn.call(@flag_key, @context)
|
48
|
+
old_eval = @value.get_and_set(new_eval)
|
49
|
+
|
50
|
+
return if new_eval == old_eval
|
51
|
+
|
52
|
+
@listener.update(
|
53
|
+
LaunchDarkly::Interfaces::FlagValueChange.new(@flag_key, old_eval, new_eval))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -119,6 +119,18 @@ module LaunchDarkly
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
+
def available?
|
123
|
+
# Most implementations use the initialized_internal? method as a
|
124
|
+
# proxy for this check. However, since `initialized_internal?`
|
125
|
+
# catches a KeyNotFound exception, and that exception can be raised
|
126
|
+
# when the server goes away, we have to modify our behavior
|
127
|
+
# slightly.
|
128
|
+
Diplomat::Kv.get(inited_key, {}, :return, :return)
|
129
|
+
true
|
130
|
+
rescue
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
122
134
|
def stop
|
123
135
|
# There's no Consul client instance to dispose of
|
124
136
|
end
|
@@ -62,6 +62,14 @@ module LaunchDarkly
|
|
62
62
|
"DynamoDBFeatureStore"
|
63
63
|
end
|
64
64
|
|
65
|
+
def available?
|
66
|
+
resp = get_item_by_keys(inited_key, inited_key)
|
67
|
+
!resp.item.nil? && resp.item.length > 0
|
68
|
+
true
|
69
|
+
rescue
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
65
73
|
def init_internal(all_data)
|
66
74
|
# Start by reading the existing keys; we will later delete any of these that weren't in all_data.
|
67
75
|
unused_old_keys = read_existing_keys(all_data.keys)
|
@@ -18,10 +18,18 @@ module LaunchDarkly
|
|
18
18
|
require 'listen'
|
19
19
|
@@have_listen = true
|
20
20
|
rescue LoadError
|
21
|
+
# Ignored
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
#
|
25
|
+
# @param data_store [LaunchDarkly::Interfaces::FeatureStore]
|
26
|
+
# @param data_source_update_sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil] Might be nil for backwards compatibility reasons.
|
27
|
+
# @param logger [Logger]
|
28
|
+
# @param options [Hash]
|
29
|
+
#
|
30
|
+
def initialize(data_store, data_source_update_sink, logger, options={})
|
31
|
+
@data_store = data_source_update_sink || data_store
|
32
|
+
@data_source_update_sink = data_source_update_sink
|
25
33
|
@logger = logger
|
26
34
|
@paths = options[:paths] || []
|
27
35
|
if @paths.is_a? String
|
@@ -80,10 +88,15 @@ module LaunchDarkly
|
|
80
88
|
load_file(path, all_data)
|
81
89
|
rescue => exn
|
82
90
|
LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
|
91
|
+
@data_source_update_sink&.update_status(
|
92
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
93
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA, 0, exn.to_s, Time.now)
|
94
|
+
)
|
83
95
|
return
|
84
96
|
end
|
85
97
|
end
|
86
|
-
@
|
98
|
+
@data_store.init(all_data)
|
99
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
87
100
|
@initialized.make_true
|
88
101
|
end
|
89
102
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "ldclient-rb/interfaces"
|
1
2
|
require "concurrent/atomics"
|
2
3
|
require "json"
|
3
4
|
|
@@ -42,6 +43,14 @@ module LaunchDarkly
|
|
42
43
|
@wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
|
43
44
|
end
|
44
45
|
|
46
|
+
def monitoring_enabled?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def available?
|
51
|
+
@wrapper.available?
|
52
|
+
end
|
53
|
+
|
45
54
|
#
|
46
55
|
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
47
56
|
# running at `localhost` with its default port.
|
@@ -103,13 +112,13 @@ module LaunchDarkly
|
|
103
112
|
@pool = create_redis_pool(opts)
|
104
113
|
|
105
114
|
# shutdown pool on close unless the client passed a custom pool and specified not to shutdown
|
106
|
-
@pool_shutdown_on_close =
|
115
|
+
@pool_shutdown_on_close = !opts[:pool] || opts.fetch(:pool_shutdown_on_close, true)
|
107
116
|
|
108
117
|
@prefix = opts[:prefix] || LaunchDarkly::Integrations::Redis::default_prefix
|
109
118
|
@logger = opts[:logger] || Config.default_logger
|
110
119
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
111
120
|
|
112
|
-
@stopped = Concurrent::AtomicBoolean.new
|
121
|
+
@stopped = Concurrent::AtomicBoolean.new
|
113
122
|
|
114
123
|
with_connection do |redis|
|
115
124
|
@logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
|
@@ -154,6 +163,14 @@ module LaunchDarkly
|
|
154
163
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
155
164
|
end
|
156
165
|
|
166
|
+
def available?
|
167
|
+
# We don't care what the status is, only that we can connect
|
168
|
+
initialized_internal?
|
169
|
+
true
|
170
|
+
rescue
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
157
174
|
def description
|
158
175
|
"RedisFeatureStore"
|
159
176
|
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Migrations
|
6
|
+
|
7
|
+
#
|
8
|
+
# A migration config stores references to callable methods which execute customer defined read or write
|
9
|
+
# operations on old or new origins of information. For read operations, an optional comparison function also be
|
10
|
+
# defined.
|
11
|
+
#
|
12
|
+
class MigrationConfig
|
13
|
+
#
|
14
|
+
# @param old [#call] Refer to {#old}
|
15
|
+
# @param new [#call] Refer to {#new}
|
16
|
+
# @param comparison [#call, nil] Refer to {#comparison}
|
17
|
+
#
|
18
|
+
def initialize(old, new, comparison)
|
19
|
+
@old = old
|
20
|
+
@new = new
|
21
|
+
@comparison = comparison
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Callable which receives a nullable payload parameter and returns an {LaunchDarkly::Result}.
|
26
|
+
#
|
27
|
+
# This function call should affect the old migration origin when called.
|
28
|
+
#
|
29
|
+
# @return [#call]
|
30
|
+
#
|
31
|
+
attr_reader :old
|
32
|
+
|
33
|
+
#
|
34
|
+
# Callable which receives a nullable payload parameter and returns an {LaunchDarkly::Result}.
|
35
|
+
#
|
36
|
+
# This function call should affect the new migration origin when called.
|
37
|
+
#
|
38
|
+
# @return [#call]
|
39
|
+
#
|
40
|
+
attr_reader :new
|
41
|
+
|
42
|
+
#
|
43
|
+
# Optional callable which receives two objects of any kind and returns a boolean representing equality.
|
44
|
+
#
|
45
|
+
# The result of this comparison can be sent upstream to LaunchDarkly to enhance migration observability.
|
46
|
+
#
|
47
|
+
# @return [#call, nil]
|
48
|
+
#
|
49
|
+
attr_reader :comparison
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# An implementation of the [LaunchDarkly::Interfaces::Migrations::Migrator] interface, capable of supporting
|
54
|
+
# feature-flag backed technology migrations.
|
55
|
+
#
|
56
|
+
class Migrator
|
57
|
+
include LaunchDarkly::Interfaces::Migrations::Migrator
|
58
|
+
|
59
|
+
#
|
60
|
+
# @param client [LaunchDarkly::LDClient]
|
61
|
+
# @param read_execution_order [Symbol]
|
62
|
+
# @param read_config [MigrationConfig]
|
63
|
+
# @param write_config [MigrationConfig]
|
64
|
+
# @param measure_latency [Boolean]
|
65
|
+
# @param measure_errors [Boolean]
|
66
|
+
#
|
67
|
+
def initialize(client, read_execution_order, read_config, write_config, measure_latency, measure_errors)
|
68
|
+
@client = client
|
69
|
+
@read_execution_order = read_execution_order
|
70
|
+
@read_config = read_config
|
71
|
+
@write_config = write_config
|
72
|
+
@measure_latency = measure_latency
|
73
|
+
@measure_errors = measure_errors
|
74
|
+
@sampler = LaunchDarkly::Impl::Sampler.new(Random.new)
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Perform the configured read operations against the appropriate old and/or new origins.
|
79
|
+
#
|
80
|
+
# @param key [String] The migration-based flag key to use for determining migration stages
|
81
|
+
# @param context [LaunchDarkly::LDContext] The context to use for evaluating the migration flag
|
82
|
+
# @param default_stage [Symbol] The stage to fallback to if one could not be determined for the requested flag
|
83
|
+
# @param payload [String] An optional payload to pass through to the configured read operations.
|
84
|
+
#
|
85
|
+
# @return [LaunchDarkly::Migrations::OperationResult]
|
86
|
+
#
|
87
|
+
def read(key, context, default_stage, payload = nil)
|
88
|
+
stage, tracker = @client.migration_variation(key, context, default_stage)
|
89
|
+
tracker.operation(LaunchDarkly::Migrations::OP_READ)
|
90
|
+
|
91
|
+
old = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_OLD, @read_config.old, tracker, @measure_latency, @measure_errors, payload)
|
92
|
+
new = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_NEW, @read_config.new, tracker, @measure_latency, @measure_errors, payload)
|
93
|
+
|
94
|
+
case stage
|
95
|
+
when LaunchDarkly::Migrations::STAGE_OFF
|
96
|
+
result = old.run
|
97
|
+
when LaunchDarkly::Migrations::STAGE_DUALWRITE
|
98
|
+
result = old.run
|
99
|
+
when LaunchDarkly::Migrations::STAGE_SHADOW
|
100
|
+
result = read_both(old, new, @read_config.comparison, @read_execution_order, tracker)
|
101
|
+
when LaunchDarkly::Migrations::STAGE_LIVE
|
102
|
+
result = read_both(new, old, @read_config.comparison, @read_execution_order, tracker)
|
103
|
+
when LaunchDarkly::Migrations::STAGE_RAMPDOWN
|
104
|
+
result = new.run
|
105
|
+
when LaunchDarkly::Migrations::STAGE_COMPLETE
|
106
|
+
result = new.run
|
107
|
+
else
|
108
|
+
result = LaunchDarkly::Migrations::OperationResult.new(
|
109
|
+
LaunchDarkly::Migrations::ORIGIN_OLD,
|
110
|
+
LaunchDarkly::Result.fail("invalid stage #{stage}; cannot execute read")
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
@client.track_migration_op(tracker)
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Perform the configured write operations against the appropriate old and/or new origins.
|
121
|
+
#
|
122
|
+
# @param key [String] The migration-based flag key to use for determining migration stages
|
123
|
+
# @param context [LaunchDarkly::LDContext] The context to use for evaluating the migration flag
|
124
|
+
# @param default_stage [Symbol] The stage to fallback to if one could not be determined for the requested flag
|
125
|
+
# @param payload [String] An optional payload to pass through to the configured write operations.
|
126
|
+
#
|
127
|
+
# @return [LaunchDarkly::Migrations::WriteResult]
|
128
|
+
#
|
129
|
+
def write(key, context, default_stage, payload = nil)
|
130
|
+
stage, tracker = @client.migration_variation(key, context, default_stage)
|
131
|
+
tracker.operation(LaunchDarkly::Migrations::OP_WRITE)
|
132
|
+
|
133
|
+
old = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_OLD, @write_config.old, tracker, @measure_latency, @measure_errors, payload)
|
134
|
+
new = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_NEW, @write_config.new, tracker, @measure_latency, @measure_errors, payload)
|
135
|
+
|
136
|
+
case stage
|
137
|
+
when LaunchDarkly::Migrations::STAGE_OFF
|
138
|
+
result = old.run()
|
139
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(result)
|
140
|
+
when LaunchDarkly::Migrations::STAGE_DUALWRITE
|
141
|
+
authoritative_result, nonauthoritative_result = write_both(old, new, tracker)
|
142
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
|
143
|
+
when LaunchDarkly::Migrations::STAGE_SHADOW
|
144
|
+
authoritative_result, nonauthoritative_result = write_both(old, new, tracker)
|
145
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
|
146
|
+
when LaunchDarkly::Migrations::STAGE_LIVE
|
147
|
+
authoritative_result, nonauthoritative_result = write_both(new, old, tracker)
|
148
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
|
149
|
+
when LaunchDarkly::Migrations::STAGE_RAMPDOWN
|
150
|
+
authoritative_result, nonauthoritative_result = write_both(new, old, tracker)
|
151
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
|
152
|
+
when LaunchDarkly::Migrations::STAGE_COMPLETE
|
153
|
+
result = new.run()
|
154
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(result)
|
155
|
+
else
|
156
|
+
result = LaunchDarkly::Migrations::OperationResult.fail(
|
157
|
+
LaunchDarkly::Migrations::ORIGIN_OLD,
|
158
|
+
LaunchDarkly::Result.fail("invalid stage #{stage}; cannot execute write")
|
159
|
+
)
|
160
|
+
write_result = LaunchDarkly::Migrations::WriteResult.new(result)
|
161
|
+
end
|
162
|
+
|
163
|
+
@client.track_migration_op(tracker)
|
164
|
+
|
165
|
+
write_result
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
# Execute both read methods in accordance with the requested execution order.
|
170
|
+
#
|
171
|
+
# This method always returns the {LaunchDarkly::Migrations::OperationResult} from running the authoritative read operation. The
|
172
|
+
# non-authoritative executor may fail but it will not affect the return value.
|
173
|
+
#
|
174
|
+
# @param authoritative [Executor]
|
175
|
+
# @param nonauthoritative [Executor]
|
176
|
+
# @param comparison [#call]
|
177
|
+
# @param execution_order [Symbol]
|
178
|
+
# @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
|
179
|
+
#
|
180
|
+
# @return [LaunchDarkly::Migrations::OperationResult]
|
181
|
+
#
|
182
|
+
private def read_both(authoritative, nonauthoritative, comparison, execution_order, tracker)
|
183
|
+
authoritative_result = nil
|
184
|
+
nonauthoritative_result = nil
|
185
|
+
|
186
|
+
case execution_order
|
187
|
+
when LaunchDarkly::Migrations::MigratorBuilder::EXECUTION_PARALLEL
|
188
|
+
auth_handler = Thread.new { authoritative_result = authoritative.run }
|
189
|
+
nonauth_handler = Thread.new { nonauthoritative_result = nonauthoritative.run }
|
190
|
+
|
191
|
+
auth_handler.join()
|
192
|
+
nonauth_handler.join()
|
193
|
+
when LaunchDarkly::Migrations::MigratorBuilder::EXECUTION_RANDOM && @sampler.sample(2)
|
194
|
+
nonauthoritative_result = nonauthoritative.run
|
195
|
+
authoritative_result = authoritative.run
|
196
|
+
else
|
197
|
+
authoritative_result = authoritative.run
|
198
|
+
nonauthoritative_result = nonauthoritative.run
|
199
|
+
end
|
200
|
+
|
201
|
+
return authoritative_result if comparison.nil?
|
202
|
+
|
203
|
+
if authoritative_result.success? && nonauthoritative_result.success?
|
204
|
+
tracker.consistent(->{ comparison.call(authoritative_result.value, nonauthoritative_result.value) })
|
205
|
+
end
|
206
|
+
|
207
|
+
authoritative_result
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Execute both operations sequentially.
|
212
|
+
#
|
213
|
+
# If the authoritative executor fails, do not run the non-authoritative one. As a result, this method will
|
214
|
+
# always return an authoritative {LaunchDarkly::Migrations::OperationResult} as the first value, and optionally the non-authoritative
|
215
|
+
# {LaunchDarkly::Migrations::OperationResult} as the second value.
|
216
|
+
#
|
217
|
+
# @param authoritative [Executor]
|
218
|
+
# @param nonauthoritative [Executor]
|
219
|
+
# @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
|
220
|
+
#
|
221
|
+
# @return [Array<LaunchDarkly::Migrations::OperationResult, [LaunchDarkly::Migrations::OperationResult, nil]>]
|
222
|
+
#
|
223
|
+
private def write_both(authoritative, nonauthoritative, tracker)
|
224
|
+
authoritative_result = authoritative.run()
|
225
|
+
tracker.invoked(authoritative.origin)
|
226
|
+
|
227
|
+
return authoritative_result, nil unless authoritative_result.success?
|
228
|
+
|
229
|
+
nonauthoritative_result = nonauthoritative.run()
|
230
|
+
tracker.invoked(nonauthoritative.origin)
|
231
|
+
|
232
|
+
[authoritative_result, nonauthoritative_result]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
#
|
237
|
+
# Utility class for executing migration operations while also tracking our built-in migration measurements.
|
238
|
+
#
|
239
|
+
class Executor
|
240
|
+
#
|
241
|
+
# @return [Symbol]
|
242
|
+
#
|
243
|
+
attr_reader :origin
|
244
|
+
|
245
|
+
#
|
246
|
+
# @param origin [Symbol]
|
247
|
+
# @param fn [#call]
|
248
|
+
# @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
|
249
|
+
# @param measure_latency [Boolean]
|
250
|
+
# @param measure_errors [Boolean]
|
251
|
+
# @param payload [Object, nil]
|
252
|
+
#
|
253
|
+
def initialize(logger, origin, fn, tracker, measure_latency, measure_errors, payload)
|
254
|
+
@logger = logger
|
255
|
+
@origin = origin
|
256
|
+
@fn = fn
|
257
|
+
@tracker = tracker
|
258
|
+
@measure_latency = measure_latency
|
259
|
+
@measure_errors = measure_errors
|
260
|
+
@payload = payload
|
261
|
+
end
|
262
|
+
|
263
|
+
#
|
264
|
+
# Execute the configured operation and track any available measurements.
|
265
|
+
#
|
266
|
+
# @return [LaunchDarkly::Migrations::OperationResult]
|
267
|
+
#
|
268
|
+
def run()
|
269
|
+
start = Time.now
|
270
|
+
|
271
|
+
begin
|
272
|
+
result = @fn.call(@payload)
|
273
|
+
rescue => e
|
274
|
+
LaunchDarkly::Util.log_exception(@logger, "Unexpected error running method for '#{origin}' origin", e)
|
275
|
+
result = LaunchDarkly::Result.fail("'#{origin}' operation raised an exception", e)
|
276
|
+
end
|
277
|
+
|
278
|
+
@tracker.latency(@origin, (Time.now - start) * 1_000) if @measure_latency
|
279
|
+
@tracker.error(@origin) if @measure_errors && !result.success?
|
280
|
+
@tracker.invoked(@origin)
|
281
|
+
|
282
|
+
LaunchDarkly::Migrations::OperationResult.new(@origin, result)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require "set"
|
2
|
+
require "ldclient-rb/impl/sampler"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
module Migrations
|
8
|
+
class OpTracker
|
9
|
+
include LaunchDarkly::Interfaces::Migrations::OpTracker
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param logger [Logger] logger
|
13
|
+
# @param key [string] key
|
14
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag] flag
|
15
|
+
# @param context [LaunchDarkly::LDContext] context
|
16
|
+
# @param detail [LaunchDarkly::EvaluationDetail] detail
|
17
|
+
# @param default_stage [Symbol] default_stage
|
18
|
+
#
|
19
|
+
def initialize(logger, key, flag, context, detail, default_stage)
|
20
|
+
@logger = logger
|
21
|
+
@key = key
|
22
|
+
@flag = flag
|
23
|
+
@context = context
|
24
|
+
@detail = detail
|
25
|
+
@default_stage = default_stage
|
26
|
+
@sampler = LaunchDarkly::Impl::Sampler.new(Random.new)
|
27
|
+
|
28
|
+
@mutex = Mutex.new
|
29
|
+
|
30
|
+
# @type [Symbol, nil]
|
31
|
+
@operation = nil
|
32
|
+
|
33
|
+
# @type [Set<Symbol>]
|
34
|
+
@invoked = Set.new
|
35
|
+
# @type [Boolean, nil]
|
36
|
+
@consistent = nil
|
37
|
+
|
38
|
+
# @type [Int]
|
39
|
+
@consistent_ratio = @flag&.migration_settings&.check_ratio
|
40
|
+
@consistent_ratio = 1 if @consistent_ratio.nil?
|
41
|
+
|
42
|
+
# @type [Set<Symbol>]
|
43
|
+
@errors = Set.new
|
44
|
+
# @type [Hash<Symbol, Float>]
|
45
|
+
@latencies = Hash.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def operation(operation)
|
49
|
+
return unless LaunchDarkly::Migrations::VALID_OPERATIONS.include? operation
|
50
|
+
|
51
|
+
@mutex.synchronize do
|
52
|
+
@operation = operation
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def invoked(origin)
|
57
|
+
return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
|
58
|
+
|
59
|
+
@mutex.synchronize do
|
60
|
+
@invoked.add(origin)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def consistent(is_consistent)
|
65
|
+
@mutex.synchronize do
|
66
|
+
if @sampler.sample(@consistent_ratio)
|
67
|
+
begin
|
68
|
+
@consistent = is_consistent.call
|
69
|
+
rescue => e
|
70
|
+
LaunchDarkly::Util.log_exception(@logger, "Exception raised during consistency check; failed to record measurement", e)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def error(origin)
|
77
|
+
return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
|
78
|
+
|
79
|
+
@mutex.synchronize do
|
80
|
+
@errors.add(origin)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def latency(origin, duration)
|
85
|
+
return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
|
86
|
+
return unless duration.is_a? Numeric
|
87
|
+
return if duration < 0
|
88
|
+
|
89
|
+
@mutex.synchronize do
|
90
|
+
@latencies[origin] = duration
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def build
|
95
|
+
@mutex.synchronize do
|
96
|
+
return "operation cannot contain an empty key" if @key.empty?
|
97
|
+
return "operation not provided" if @operation.nil?
|
98
|
+
return "no origins were invoked" if @invoked.empty?
|
99
|
+
return "provided context was invalid" unless @context.valid?
|
100
|
+
|
101
|
+
result = check_invoked_consistency
|
102
|
+
return result unless result == true
|
103
|
+
|
104
|
+
LaunchDarkly::Impl::MigrationOpEvent.new(
|
105
|
+
LaunchDarkly::Impl::Util.current_time_millis,
|
106
|
+
@context,
|
107
|
+
@key,
|
108
|
+
@flag,
|
109
|
+
@operation,
|
110
|
+
@default_stage,
|
111
|
+
@detail,
|
112
|
+
@invoked,
|
113
|
+
@consistent,
|
114
|
+
@consistent_ratio,
|
115
|
+
@errors,
|
116
|
+
@latencies
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private def check_invoked_consistency
|
122
|
+
LaunchDarkly::Migrations::VALID_ORIGINS.each do |origin|
|
123
|
+
next if @invoked.include? origin
|
124
|
+
|
125
|
+
return "provided latency for origin '#{origin}' without recording invocation" if @latencies.include? origin
|
126
|
+
return "provided error for origin '#{origin}' without recording invocation" if @errors.include? origin
|
127
|
+
end
|
128
|
+
|
129
|
+
return "provided consistency without recording both invocations" if !@consistent.nil? && @invoked.size != 2
|
130
|
+
|
131
|
+
true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -26,6 +26,10 @@ module LaunchDarkly
|
|
26
26
|
@version = data[:version]
|
27
27
|
@deleted = !!data[:deleted]
|
28
28
|
return if @deleted
|
29
|
+
migration_settings = data[:migration] || {}
|
30
|
+
@migration_settings = MigrationSettings.new(migration_settings[:checkRatio])
|
31
|
+
@sampling_ratio = data[:samplingRatio]
|
32
|
+
@exclude_from_summaries = !!data[:excludeFromSummaries]
|
29
33
|
@variations = data[:variations] || []
|
30
34
|
@on = !!data[:on]
|
31
35
|
fallthrough = data[:fallthrough] || {}
|
@@ -33,7 +37,7 @@ module LaunchDarkly
|
|
33
37
|
@off_variation = data[:offVariation]
|
34
38
|
check_variation_range(self, errors, @off_variation, "off variation")
|
35
39
|
@prerequisites = (data[:prerequisites] || []).map do |prereq_data|
|
36
|
-
Prerequisite.new(prereq_data, self
|
40
|
+
Prerequisite.new(prereq_data, self)
|
37
41
|
end
|
38
42
|
@targets = (data[:targets] || []).map do |target_data|
|
39
43
|
Target.new(target_data, self, errors)
|
@@ -63,6 +67,12 @@ module LaunchDarkly
|
|
63
67
|
attr_reader :version
|
64
68
|
# @return [Boolean]
|
65
69
|
attr_reader :deleted
|
70
|
+
# @return [MigrationSettings, nil]
|
71
|
+
attr_reader :migration_settings
|
72
|
+
# @return [Integer, nil]
|
73
|
+
attr_reader :sampling_ratio
|
74
|
+
# @return [Boolean, nil]
|
75
|
+
attr_reader :exclude_from_summaries
|
66
76
|
# @return [Array]
|
67
77
|
attr_reader :variations
|
68
78
|
# @return [Boolean]
|
@@ -108,13 +118,12 @@ module LaunchDarkly
|
|
108
118
|
end
|
109
119
|
|
110
120
|
class Prerequisite
|
111
|
-
def initialize(data, flag
|
121
|
+
def initialize(data, flag)
|
112
122
|
@data = data
|
113
123
|
@key = data[:key]
|
114
124
|
@variation = data[:variation]
|
115
125
|
@failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
|
116
126
|
EvaluationReason::prerequisite_failed(@key))
|
117
|
-
check_variation_range(flag, errors_out, @variation, "prerequisite")
|
118
127
|
end
|
119
128
|
|
120
129
|
# @return [Hash]
|
@@ -173,6 +182,19 @@ module LaunchDarkly
|
|
173
182
|
attr_reader :variation_or_rollout
|
174
183
|
end
|
175
184
|
|
185
|
+
|
186
|
+
class MigrationSettings
|
187
|
+
#
|
188
|
+
# @param check_ratio [Int, nil]
|
189
|
+
#
|
190
|
+
def initialize(check_ratio)
|
191
|
+
@check_ratio = check_ratio
|
192
|
+
end
|
193
|
+
|
194
|
+
# @return [Integer, nil]
|
195
|
+
attr_reader :check_ratio
|
196
|
+
end
|
197
|
+
|
176
198
|
class VariationOrRollout
|
177
199
|
def initialize(variation, rollout_data, flag = nil, errors_out = nil, description = nil)
|
178
200
|
@variation = variation
|