launchdarkly-server-sdk 8.8.3-java
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 +7 -0
- data/LICENSE.txt +13 -0
- data/README.md +61 -0
- data/lib/launchdarkly-server-sdk.rb +1 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +658 -0
- data/lib/ldclient-rb/context.rb +565 -0
- data/lib/ldclient-rb/evaluation_detail.rb +387 -0
- data/lib/ldclient-rb/events.rb +642 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/flags_state.rb +88 -0
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +166 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +109 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +129 -0
- data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
- data/lib/ldclient-rb/impl/evaluator.rb +539 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +86 -0
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +131 -0
- data/lib/ldclient-rb/impl/event_sender.rb +100 -0
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +170 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +300 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +229 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +306 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- 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/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +254 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +72 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +46 -0
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +141 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/impl/util.rb +95 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations/consul.rb +45 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +92 -0
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +98 -0
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +663 -0
- data/lib/ldclient-rb/integrations/test_data.rb +213 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +246 -0
- data/lib/ldclient-rb/integrations.rb +6 -0
- data/lib/ldclient-rb/interfaces.rb +974 -0
- data/lib/ldclient-rb/ldclient.rb +822 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +102 -0
- data/lib/ldclient-rb/reference.rb +295 -0
- data/lib/ldclient-rb/requestor.rb +102 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +196 -0
- data/lib/ldclient-rb/util.rb +132 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/lib/ldclient-rb.rb +27 -0
- metadata +400 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
require "date"
|
2
|
+
require "semantic"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
# Defines the behavior of all operators that can be used in feature flag rules and segment rules.
|
8
|
+
module EvaluatorOperators
|
9
|
+
# Applies an operator to produce a boolean result.
|
10
|
+
#
|
11
|
+
# @param op [Symbol] one of the supported LaunchDarkly operators, as a symbol
|
12
|
+
# @param context_value the value of the context attribute that is referenced in the current clause (left-hand
|
13
|
+
# side of the expression)
|
14
|
+
# @param clause_value the constant value that `context_value` is being compared to (right-hand side of the
|
15
|
+
# expression)
|
16
|
+
# @return [Boolean] true if the expression should be considered a match; false if it is not a match, or
|
17
|
+
# if the values cannot be compared because they are of the wrong types, or if the operator is unknown
|
18
|
+
def self.apply(op, context_value, clause_value)
|
19
|
+
case op
|
20
|
+
when :in
|
21
|
+
context_value == clause_value
|
22
|
+
when :startsWith
|
23
|
+
string_op(context_value, clause_value, lambda { |a, b| a.start_with? b })
|
24
|
+
when :endsWith
|
25
|
+
string_op(context_value, clause_value, lambda { |a, b| a.end_with? b })
|
26
|
+
when :contains
|
27
|
+
string_op(context_value, clause_value, lambda { |a, b| a.include? b })
|
28
|
+
when :matches
|
29
|
+
string_op(context_value, clause_value, lambda { |a, b|
|
30
|
+
begin
|
31
|
+
re = Regexp.new b
|
32
|
+
!re.match(a).nil?
|
33
|
+
rescue
|
34
|
+
false
|
35
|
+
end
|
36
|
+
})
|
37
|
+
when :lessThan
|
38
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a < b })
|
39
|
+
when :lessThanOrEqual
|
40
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a <= b })
|
41
|
+
when :greaterThan
|
42
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a > b })
|
43
|
+
when :greaterThanOrEqual
|
44
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a >= b })
|
45
|
+
when :before
|
46
|
+
date_op(context_value, clause_value, lambda { |a, b| a < b })
|
47
|
+
when :after
|
48
|
+
date_op(context_value, clause_value, lambda { |a, b| a > b })
|
49
|
+
when :semVerEqual
|
50
|
+
semver_op(context_value, clause_value, lambda { |a, b| a == b })
|
51
|
+
when :semVerLessThan
|
52
|
+
semver_op(context_value, clause_value, lambda { |a, b| a < b })
|
53
|
+
when :semVerGreaterThan
|
54
|
+
semver_op(context_value, clause_value, lambda { |a, b| a > b })
|
55
|
+
when :segmentMatch
|
56
|
+
# We should never reach this; it can't be evaluated based on just two parameters, because it requires
|
57
|
+
# looking up the segment from the data store. Instead, we special-case this operator in clause_match_context.
|
58
|
+
false
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
67
|
+
private_constant :NUMERIC_VERSION_COMPONENTS_REGEX
|
68
|
+
|
69
|
+
def self.string_op(context_value, clause_value, fn)
|
70
|
+
(context_value.is_a? String) && (clause_value.is_a? String) && fn.call(context_value, clause_value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.numeric_op(context_value, clause_value, fn)
|
74
|
+
(context_value.is_a? Numeric) && (clause_value.is_a? Numeric) && fn.call(context_value, clause_value)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.date_op(context_value, clause_value, fn)
|
78
|
+
ud = to_date(context_value)
|
79
|
+
if !ud.nil?
|
80
|
+
cd = to_date(clause_value)
|
81
|
+
!cd.nil? && fn.call(ud, cd)
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.semver_op(context_value, clause_value, fn)
|
88
|
+
uv = to_semver(context_value)
|
89
|
+
if !uv.nil?
|
90
|
+
cv = to_semver(clause_value)
|
91
|
+
!cv.nil? && fn.call(uv, cv)
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.to_date(value)
|
98
|
+
if value.is_a? String
|
99
|
+
begin
|
100
|
+
DateTime.rfc3339(value).strftime("%Q").to_i
|
101
|
+
rescue => e
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
elsif value.is_a? Numeric
|
105
|
+
value
|
106
|
+
else
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.to_semver(value)
|
112
|
+
if value.is_a? String
|
113
|
+
for _ in 0..2 do
|
114
|
+
begin
|
115
|
+
return Semantic::Version.new(value)
|
116
|
+
rescue ArgumentError
|
117
|
+
value = add_zero_version_component(value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.add_zero_version_component(v)
|
125
|
+
NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
|
126
|
+
m[0] + ".0" + v[m[0].length..-1]
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "ldclient-rb/impl/unbounded_pool"
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "http"
|
5
|
+
require "stringio"
|
6
|
+
require "zlib"
|
7
|
+
|
8
|
+
module LaunchDarkly
|
9
|
+
module Impl
|
10
|
+
EventSenderResult = Struct.new(:success, :must_shutdown, :time_from_server)
|
11
|
+
|
12
|
+
class EventSender
|
13
|
+
CURRENT_SCHEMA_VERSION = 4
|
14
|
+
DEFAULT_RETRY_INTERVAL = 1
|
15
|
+
|
16
|
+
def initialize(sdk_key, config, http_client = nil, retry_interval = DEFAULT_RETRY_INTERVAL)
|
17
|
+
@sdk_key = sdk_key
|
18
|
+
@config = config
|
19
|
+
@events_uri = config.events_uri + "/bulk"
|
20
|
+
@diagnostic_uri = config.events_uri + "/diagnostic"
|
21
|
+
@logger = config.logger
|
22
|
+
@retry_interval = retry_interval
|
23
|
+
@http_client_pool = UnboundedPool.new(
|
24
|
+
lambda { LaunchDarkly::Util.new_http_client(@config.events_uri, @config) },
|
25
|
+
lambda { |client| client.close })
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
@http_client_pool.dispose_all()
|
30
|
+
end
|
31
|
+
|
32
|
+
def send_event_data(event_data, description, is_diagnostic)
|
33
|
+
uri = is_diagnostic ? @diagnostic_uri : @events_uri
|
34
|
+
payload_id = is_diagnostic ? nil : SecureRandom.uuid
|
35
|
+
begin
|
36
|
+
http_client = @http_client_pool.acquire()
|
37
|
+
response = nil
|
38
|
+
2.times do |attempt|
|
39
|
+
if attempt > 0
|
40
|
+
@logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
|
41
|
+
sleep(@retry_interval)
|
42
|
+
end
|
43
|
+
begin
|
44
|
+
@logger.debug { "[LDClient] sending #{description}: #{event_data}" }
|
45
|
+
headers = {}
|
46
|
+
headers["content-type"] = "application/json"
|
47
|
+
headers["content-encoding"] = "gzip" if @config.compress_events
|
48
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
49
|
+
unless is_diagnostic
|
50
|
+
headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
|
51
|
+
headers["X-LaunchDarkly-Payload-ID"] = payload_id
|
52
|
+
end
|
53
|
+
|
54
|
+
body = event_data
|
55
|
+
if @config.compress_events
|
56
|
+
gzip = Zlib::GzipWriter.new(StringIO.new)
|
57
|
+
gzip << event_data
|
58
|
+
|
59
|
+
body = gzip.close.string
|
60
|
+
end
|
61
|
+
|
62
|
+
response = http_client.request("POST", uri, {
|
63
|
+
headers: headers,
|
64
|
+
body: body,
|
65
|
+
})
|
66
|
+
rescue StandardError => exn
|
67
|
+
@logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
|
68
|
+
next
|
69
|
+
end
|
70
|
+
status = response.status.code
|
71
|
+
# must fully read body for persistent connections
|
72
|
+
body = response.to_s
|
73
|
+
if status >= 200 && status < 300
|
74
|
+
res_time = nil
|
75
|
+
unless response.headers["date"].nil?
|
76
|
+
begin
|
77
|
+
res_time = Time.httpdate(response.headers["date"])
|
78
|
+
rescue ArgumentError
|
79
|
+
# Ignored
|
80
|
+
end
|
81
|
+
end
|
82
|
+
return EventSenderResult.new(true, false, res_time)
|
83
|
+
end
|
84
|
+
must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
|
85
|
+
can_retry = !must_shutdown && attempt == 0
|
86
|
+
message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
|
87
|
+
@logger.error { "[LDClient] #{message}" }
|
88
|
+
if must_shutdown
|
89
|
+
return EventSenderResult.new(false, true, nil)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
# used up our retries
|
93
|
+
EventSenderResult.new(false, false, nil)
|
94
|
+
ensure
|
95
|
+
@http_client_pool.release(http_client)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "ldclient-rb/impl/event_types"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
EventSummary = Struct.new(:start_date, :end_date, :counters)
|
7
|
+
|
8
|
+
EventSummaryFlagInfo = Struct.new(:default, :versions, :context_kinds)
|
9
|
+
|
10
|
+
EventSummaryFlagVariationCounter = Struct.new(:value, :count)
|
11
|
+
|
12
|
+
# Manages the state of summarizable information for the EventProcessor, including the
|
13
|
+
# event counters and context deduplication. Note that the methods of this class are
|
14
|
+
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
|
15
|
+
# synchronization across both the summarizer and the event queue.
|
16
|
+
class EventSummarizer
|
17
|
+
class Counter
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
clear
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds this event to our counters, if it is a type of event we need to count.
|
25
|
+
def summarize_event(event)
|
26
|
+
return unless event.is_a?(LaunchDarkly::Impl::EvalEvent)
|
27
|
+
|
28
|
+
counters_for_flag = @counters[event.key]
|
29
|
+
if counters_for_flag.nil?
|
30
|
+
counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new, Set.new)
|
31
|
+
@counters[event.key] = counters_for_flag
|
32
|
+
end
|
33
|
+
|
34
|
+
counters_for_flag_version = counters_for_flag.versions[event.version]
|
35
|
+
if counters_for_flag_version.nil?
|
36
|
+
counters_for_flag_version = Hash.new
|
37
|
+
counters_for_flag.versions[event.version] = counters_for_flag_version
|
38
|
+
end
|
39
|
+
|
40
|
+
counters_for_flag.context_kinds.merge(event.context.kinds)
|
41
|
+
|
42
|
+
variation_counter = counters_for_flag_version[event.variation]
|
43
|
+
if variation_counter.nil?
|
44
|
+
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
|
45
|
+
else
|
46
|
+
variation_counter.count = variation_counter.count + 1
|
47
|
+
end
|
48
|
+
|
49
|
+
time = event.timestamp
|
50
|
+
unless time.nil?
|
51
|
+
@start_date = time if @start_date == 0 || time < @start_date
|
52
|
+
@end_date = time if time > @end_date
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a snapshot of the current summarized event data, and resets this state.
|
57
|
+
def snapshot
|
58
|
+
EventSummary.new(@start_date, @end_date, @counters)
|
59
|
+
end
|
60
|
+
|
61
|
+
def clear
|
62
|
+
@start_date = 0
|
63
|
+
@end_date = 0
|
64
|
+
@counters = {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
class Event
|
6
|
+
# @param timestamp [Integer]
|
7
|
+
# @param context [LaunchDarkly::LDContext]
|
8
|
+
# @param sampling_ratio [Integer, nil]
|
9
|
+
# @param exclude_from_summaries [Boolean]
|
10
|
+
def initialize(timestamp, context, sampling_ratio = nil, exclude_from_summaries = false)
|
11
|
+
@timestamp = timestamp
|
12
|
+
@context = context
|
13
|
+
@sampling_ratio = sampling_ratio
|
14
|
+
@exclude_from_summaries = exclude_from_summaries
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Integer]
|
18
|
+
attr_reader :timestamp
|
19
|
+
# @return [LaunchDarkly::LDContext]
|
20
|
+
attr_reader :context
|
21
|
+
# @return [Integer, nil]
|
22
|
+
attr_reader :sampling_ratio
|
23
|
+
# @return [Boolean]
|
24
|
+
attr_reader :exclude_from_summaries
|
25
|
+
end
|
26
|
+
|
27
|
+
class EvalEvent < Event
|
28
|
+
def initialize(timestamp, context, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
|
29
|
+
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false)
|
30
|
+
super(timestamp, context, sampling_ratio, exclude_from_summaries)
|
31
|
+
@key = key
|
32
|
+
@version = version
|
33
|
+
@variation = variation
|
34
|
+
@value = value
|
35
|
+
@reason = reason
|
36
|
+
@default = default
|
37
|
+
# avoid setting rarely-used attributes if they have no value - this saves a little space per instance
|
38
|
+
@track_events = track_events if track_events
|
39
|
+
@debug_until = debug_until if debug_until
|
40
|
+
@prereq_of = prereq_of if prereq_of
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :key
|
44
|
+
attr_reader :version
|
45
|
+
attr_reader :variation
|
46
|
+
attr_reader :value
|
47
|
+
attr_reader :reason
|
48
|
+
attr_reader :default
|
49
|
+
attr_reader :track_events
|
50
|
+
attr_reader :debug_until
|
51
|
+
attr_reader :prereq_of
|
52
|
+
end
|
53
|
+
|
54
|
+
class MigrationOpEvent < Event
|
55
|
+
#
|
56
|
+
# A migration op event represents the results of a migration-assisted read or write operation.
|
57
|
+
#
|
58
|
+
# The event includes optional measurements reporting on consistency checks, error reporting, and operation latency
|
59
|
+
# values.
|
60
|
+
#
|
61
|
+
# @param timestamp [Integer]
|
62
|
+
# @param context [LaunchDarkly::LDContext]
|
63
|
+
# @param key [string]
|
64
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag, nil]
|
65
|
+
# @param operation [Symbol]
|
66
|
+
# @param default_stage [Symbol]
|
67
|
+
# @param evaluation [LaunchDarkly::EvaluationDetail]
|
68
|
+
# @param invoked [Set]
|
69
|
+
# @param consistency_check [Boolean, nil]
|
70
|
+
# @param consistency_check_ratio [Integer, nil]
|
71
|
+
# @param errors [Set]
|
72
|
+
# @param latencies [Hash<Symbol, Float>]
|
73
|
+
#
|
74
|
+
def initialize(timestamp, context, key, flag, operation, default_stage, evaluation, invoked, consistency_check, consistency_check_ratio, errors, latencies)
|
75
|
+
super(timestamp, context)
|
76
|
+
@operation = operation
|
77
|
+
@key = key
|
78
|
+
@version = flag&.version
|
79
|
+
@sampling_ratio = flag&.sampling_ratio
|
80
|
+
@default = default_stage
|
81
|
+
@evaluation = evaluation
|
82
|
+
@consistency_check = consistency_check
|
83
|
+
@consistency_check_ratio = consistency_check.nil? ? nil : consistency_check_ratio
|
84
|
+
@invoked = invoked
|
85
|
+
@errors = errors
|
86
|
+
@latencies = latencies
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_reader :operation
|
90
|
+
attr_reader :key
|
91
|
+
attr_reader :version
|
92
|
+
attr_reader :sampling_ratio
|
93
|
+
attr_reader :default
|
94
|
+
attr_reader :evaluation
|
95
|
+
attr_reader :consistency_check
|
96
|
+
attr_reader :consistency_check_ratio
|
97
|
+
attr_reader :invoked
|
98
|
+
attr_reader :errors
|
99
|
+
attr_reader :latencies
|
100
|
+
end
|
101
|
+
|
102
|
+
class IdentifyEvent < Event
|
103
|
+
def initialize(timestamp, context)
|
104
|
+
super(timestamp, context)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class CustomEvent < Event
|
109
|
+
def initialize(timestamp, context, key, data = nil, metric_value = nil)
|
110
|
+
super(timestamp, context)
|
111
|
+
@key = key
|
112
|
+
@data = data unless data.nil?
|
113
|
+
@metric_value = metric_value unless metric_value.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :key
|
117
|
+
attr_reader :data
|
118
|
+
attr_reader :metric_value
|
119
|
+
end
|
120
|
+
|
121
|
+
class IndexEvent < Event
|
122
|
+
def initialize(timestamp, context)
|
123
|
+
super(timestamp, context)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class DebugEvent < Event
|
128
|
+
def initialize(eval_event)
|
129
|
+
super(eval_event.timestamp, eval_event.context)
|
130
|
+
@eval_event = eval_event
|
131
|
+
end
|
132
|
+
|
133
|
+
attr_reader :eval_event
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -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
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Integrations
|
6
|
+
module Consul
|
7
|
+
#
|
8
|
+
# Internal implementation of the Consul feature store, intended to be used with CachingStoreWrapper.
|
9
|
+
#
|
10
|
+
class ConsulFeatureStoreCore
|
11
|
+
begin
|
12
|
+
require "diplomat"
|
13
|
+
CONSUL_ENABLED = true
|
14
|
+
rescue ScriptError, StandardError
|
15
|
+
CONSUL_ENABLED = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(opts)
|
19
|
+
unless CONSUL_ENABLED
|
20
|
+
raise RuntimeError.new("can't use Consul feature store without the 'diplomat' gem")
|
21
|
+
end
|
22
|
+
|
23
|
+
@prefix = (opts[:prefix] || LaunchDarkly::Integrations::Consul.default_prefix) + '/'
|
24
|
+
@logger = opts[:logger] || Config.default_logger
|
25
|
+
Diplomat.configuration = opts[:consul_config] unless opts[:consul_config].nil?
|
26
|
+
Diplomat.configuration.url = opts[:url] unless opts[:url].nil?
|
27
|
+
@logger.info("ConsulFeatureStore: using Consul host at #{Diplomat.configuration.url}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def init_internal(all_data)
|
31
|
+
# Start by reading the existing keys; we will later delete any of these that weren't in all_data.
|
32
|
+
unused_old_keys = Set.new
|
33
|
+
keys = Diplomat::Kv.get(@prefix, { keys: true, recurse: true }, :return)
|
34
|
+
unused_old_keys.merge(keys) if keys != ""
|
35
|
+
|
36
|
+
ops = []
|
37
|
+
num_items = 0
|
38
|
+
|
39
|
+
# Insert or update every provided item
|
40
|
+
all_data.each do |kind, items|
|
41
|
+
items.values.each do |item|
|
42
|
+
value = Model.serialize(kind, item)
|
43
|
+
key = item_key(kind, item[:key])
|
44
|
+
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => key, 'Value' => value } })
|
45
|
+
unused_old_keys.delete(key)
|
46
|
+
num_items = num_items + 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Now delete any previously existing items whose keys were not in the current data
|
51
|
+
unused_old_keys.each do |key|
|
52
|
+
ops.push({ 'KV' => { 'Verb' => 'delete', 'Key' => key } })
|
53
|
+
end
|
54
|
+
|
55
|
+
# Now set the special key that we check in initialized_internal?
|
56
|
+
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => inited_key, 'Value' => '' } })
|
57
|
+
|
58
|
+
ConsulUtil.batch_operations(ops)
|
59
|
+
|
60
|
+
@logger.info { "Initialized database with #{num_items} items" }
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_internal(kind, key)
|
64
|
+
value = Diplomat::Kv.get(item_key(kind, key), {}, :return) # :return means "don't throw an error if not found"
|
65
|
+
(value.nil? || value == "") ? nil : Model.deserialize(kind, value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_all_internal(kind)
|
69
|
+
items_out = {}
|
70
|
+
results = Diplomat::Kv.get(kind_key(kind), { recurse: true }, :return)
|
71
|
+
(results == "" ? [] : results).each do |result|
|
72
|
+
value = result[:value]
|
73
|
+
unless value.nil?
|
74
|
+
item = Model.deserialize(kind, value)
|
75
|
+
items_out[item[:key].to_sym] = item
|
76
|
+
end
|
77
|
+
end
|
78
|
+
items_out
|
79
|
+
end
|
80
|
+
|
81
|
+
def upsert_internal(kind, new_item)
|
82
|
+
key = item_key(kind, new_item[:key])
|
83
|
+
json = Model.serialize(kind, new_item)
|
84
|
+
|
85
|
+
# We will potentially keep retrying indefinitely until someone's write succeeds
|
86
|
+
while true
|
87
|
+
old_value = Diplomat::Kv.get(key, { decode_values: true }, :return)
|
88
|
+
if old_value.nil? || old_value == ""
|
89
|
+
mod_index = 0
|
90
|
+
else
|
91
|
+
old_item = Model.deserialize(kind, old_value[0]["Value"])
|
92
|
+
# Check whether the item is stale. If so, don't do the update (and return the existing item to
|
93
|
+
# FeatureStoreWrapper so it can be cached)
|
94
|
+
if old_item[:version] >= new_item[:version]
|
95
|
+
return old_item
|
96
|
+
end
|
97
|
+
mod_index = old_value[0]["ModifyIndex"]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Otherwise, try to write. We will do a compare-and-set operation, so the write will only succeed if
|
101
|
+
# the key's ModifyIndex is still equal to the previous value. If the previous ModifyIndex was zero,
|
102
|
+
# it means the key did not previously exist and the write will only succeed if it still doesn't exist.
|
103
|
+
success = Diplomat::Kv.put(key, json, cas: mod_index)
|
104
|
+
return new_item if success
|
105
|
+
|
106
|
+
# If we failed, retry the whole shebang
|
107
|
+
@logger.debug { "Concurrent modification detected, retrying" }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialized_internal?
|
112
|
+
# Unfortunately we need to use exceptions here, instead of the :return parameter, because with
|
113
|
+
# :return there's no way to distinguish between a missing value and an empty string.
|
114
|
+
begin
|
115
|
+
Diplomat::Kv.get(inited_key, {})
|
116
|
+
true
|
117
|
+
rescue Diplomat::KeyNotFound
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
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
|
+
|
134
|
+
def stop
|
135
|
+
# There's no Consul client instance to dispose of
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def item_key(kind, key)
|
141
|
+
kind_key(kind) + key.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
def kind_key(kind)
|
145
|
+
@prefix + kind[:namespace] + '/'
|
146
|
+
end
|
147
|
+
|
148
|
+
def inited_key
|
149
|
+
@prefix + '$inited'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class ConsulUtil
|
154
|
+
#
|
155
|
+
# Submits as many transactions as necessary to submit all of the given operations.
|
156
|
+
# The ops array is consumed.
|
157
|
+
#
|
158
|
+
def self.batch_operations(ops)
|
159
|
+
batch_size = 64 # Consul can only do this many at a time
|
160
|
+
while true
|
161
|
+
chunk = ops.shift(batch_size)
|
162
|
+
break if chunk.empty?
|
163
|
+
Diplomat::Kv.txn(chunk)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|