launchdarkly-server-sdk 6.3.0 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- 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 +145 -0
- 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/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +7 -6
- 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 +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- 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 +255 -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 +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -2,7 +2,7 @@ require 'json'
|
|
2
2
|
|
3
3
|
module LaunchDarkly
|
4
4
|
#
|
5
|
-
# A snapshot of the state of all feature flags with regard to a specific
|
5
|
+
# A snapshot of the state of all feature flags with regard to a specific context, generated by
|
6
6
|
# calling the {LDClient#all_flags_state}. Serializing this object to JSON using
|
7
7
|
# `JSON.generate` (or the `to_json` method) will produce the appropriate data structure for
|
8
8
|
# bootstrapping the LaunchDarkly JavaScript client.
|
@@ -16,26 +16,37 @@ module LaunchDarkly
|
|
16
16
|
|
17
17
|
# Used internally to build the state map.
|
18
18
|
# @private
|
19
|
-
def add_flag(
|
20
|
-
key =
|
21
|
-
@flag_values[key] = value
|
19
|
+
def add_flag(flag_state, with_reasons, details_only_if_tracked)
|
20
|
+
key = flag_state[:key]
|
21
|
+
@flag_values[key] = flag_state[:value]
|
22
22
|
meta = {}
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
|
24
|
+
omit_details = false
|
25
|
+
if details_only_if_tracked
|
26
|
+
if !flag_state[:trackEvents] && !flag_state[:trackReason] && !(flag_state[:debugEventsUntilDate] && flag_state[:debugEventsUntilDate] > Impl::Util::current_time_millis)
|
27
|
+
omit_details = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
reason = (!with_reasons and !flag_state[:trackReason]) ? nil : flag_state[:reason]
|
32
|
+
|
33
|
+
if !reason.nil? && !omit_details
|
34
|
+
meta[:reason] = reason
|
26
35
|
end
|
27
|
-
|
28
|
-
|
29
|
-
meta[:
|
36
|
+
|
37
|
+
unless omit_details
|
38
|
+
meta[:version] = flag_state[:version]
|
30
39
|
end
|
31
|
-
|
32
|
-
meta[:
|
33
|
-
meta[:
|
40
|
+
|
41
|
+
meta[:variation] = flag_state[:variation] unless flag_state[:variation].nil?
|
42
|
+
meta[:trackEvents] = true if flag_state[:trackEvents]
|
43
|
+
meta[:trackReason] = true if flag_state[:trackReason]
|
44
|
+
meta[:debugEventsUntilDate] = flag_state[:debugEventsUntilDate] if flag_state[:debugEventsUntilDate]
|
34
45
|
@flag_metadata[key] = meta
|
35
46
|
end
|
36
47
|
|
37
48
|
# Returns true if this object contains a valid snapshot of feature flag state, or false if the
|
38
|
-
# state could not be computed (for instance, because the client was offline or there was no
|
49
|
+
# state could not be computed (for instance, because the client was offline or there was no context).
|
39
50
|
def valid?
|
40
51
|
@valid
|
41
52
|
end
|
@@ -70,7 +81,7 @@ module LaunchDarkly
|
|
70
81
|
|
71
82
|
# Same as as_json, but converts the JSON structure into a string.
|
72
83
|
def to_json(*a)
|
73
|
-
as_json.to_json(a)
|
84
|
+
as_json.to_json(*a)
|
74
85
|
end
|
75
86
|
end
|
76
87
|
end
|
@@ -22,8 +22,8 @@ module LaunchDarkly
|
|
22
22
|
@logger = logger
|
23
23
|
@last_status = nil
|
24
24
|
|
25
|
-
|
26
|
-
@cache = ExpiringCache.new(big_segments_config.
|
25
|
+
unless @store.nil?
|
26
|
+
@cache = ExpiringCache.new(big_segments_config.context_cache_size, big_segments_config.context_cache_time)
|
27
27
|
@poll_worker = RepeatingTask.new(big_segments_config.status_poll_interval, 0, -> { poll_store_and_update_status }, logger)
|
28
28
|
@poll_worker.start
|
29
29
|
end
|
@@ -32,25 +32,25 @@ module LaunchDarkly
|
|
32
32
|
attr_reader :status_provider
|
33
33
|
|
34
34
|
def stop
|
35
|
-
@poll_worker.stop
|
36
|
-
@store.stop
|
35
|
+
@poll_worker.stop unless @poll_worker.nil?
|
36
|
+
@store.stop unless @store.nil?
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
return nil
|
41
|
-
membership = @cache[
|
42
|
-
|
39
|
+
def get_context_membership(context_key)
|
40
|
+
return nil unless @store
|
41
|
+
membership = @cache[context_key]
|
42
|
+
unless membership
|
43
43
|
begin
|
44
|
-
membership = @store.get_membership(BigSegmentStoreManager.
|
44
|
+
membership = @store.get_membership(BigSegmentStoreManager.hash_for_context_key(context_key))
|
45
45
|
membership = EMPTY_MEMBERSHIP if membership.nil?
|
46
|
-
@cache[
|
46
|
+
@cache[context_key] = membership
|
47
47
|
rescue => e
|
48
48
|
LaunchDarkly::Util.log_exception(@logger, "Big Segment store membership query returned error", e)
|
49
49
|
return BigSegmentMembershipResult.new(nil, BigSegmentsStatus::STORE_ERROR)
|
50
50
|
end
|
51
51
|
end
|
52
|
-
poll_store_and_update_status
|
53
|
-
|
52
|
+
poll_store_and_update_status unless @last_status
|
53
|
+
unless @last_status.available
|
54
54
|
return BigSegmentMembershipResult.new(membership, BigSegmentsStatus::STORE_ERROR)
|
55
55
|
end
|
56
56
|
BigSegmentMembershipResult.new(membership, @last_status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
|
@@ -62,26 +62,26 @@ module LaunchDarkly
|
|
62
62
|
|
63
63
|
def poll_store_and_update_status
|
64
64
|
new_status = Interfaces::BigSegmentStoreStatus.new(false, false) # default to "unavailable" if we don't get a new status below
|
65
|
-
|
65
|
+
unless @store.nil?
|
66
66
|
begin
|
67
67
|
metadata = @store.get_metadata
|
68
|
-
new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata ||
|
68
|
+
new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || stale?(metadata.last_up_to_date))
|
69
69
|
rescue => e
|
70
70
|
LaunchDarkly::Util.log_exception(@logger, "Big Segment store status query returned error", e)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
@last_status = new_status
|
74
74
|
@status_provider.update_status(new_status)
|
75
|
-
|
75
|
+
|
76
76
|
new_status
|
77
77
|
end
|
78
78
|
|
79
|
-
def
|
79
|
+
def stale?(timestamp)
|
80
80
|
!timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
|
81
81
|
end
|
82
82
|
|
83
|
-
def self.
|
84
|
-
Digest::SHA256.base64digest(
|
83
|
+
def self.hash_for_context_key(context_key)
|
84
|
+
Digest::SHA256.base64digest(context_key)
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
|
6
|
+
#
|
7
|
+
# A generic mechanism for registering event listeners and broadcasting
|
8
|
+
# events to them.
|
9
|
+
#
|
10
|
+
# The SDK maintains an instance of this for each available type of listener
|
11
|
+
# (flag change, data store status, etc.). They are all intended to share a
|
12
|
+
# single executor service; notifications are submitted individually to this
|
13
|
+
# service for each listener.
|
14
|
+
#
|
15
|
+
class Broadcaster
|
16
|
+
def initialize(executor, logger)
|
17
|
+
@listeners = Concurrent::Set.new
|
18
|
+
@executor = executor
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Register a listener to this broadcaster.
|
24
|
+
#
|
25
|
+
# @param listener [#update]
|
26
|
+
#
|
27
|
+
def add_listener(listener)
|
28
|
+
unless listener.respond_to? :update
|
29
|
+
logger.warn("listener (#{listener.class}) does not respond to :update method. ignoring as registered listener")
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
listeners.add(listener)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Removes a registered listener from this broadcaster.
|
38
|
+
#
|
39
|
+
def remove_listener(listener)
|
40
|
+
listeners.delete(listener)
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_listeners?
|
44
|
+
!listeners.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Broadcast the provided event to all registered listeners.
|
49
|
+
#
|
50
|
+
# Each listener will be notified using the broadcasters executor. This
|
51
|
+
# method is non-blocking.
|
52
|
+
#
|
53
|
+
def broadcast(event)
|
54
|
+
listeners.each do |listener|
|
55
|
+
executor.post do
|
56
|
+
begin
|
57
|
+
listener.update(event)
|
58
|
+
rescue StandardError => e
|
59
|
+
logger.error("listener (#{listener.class}) raised exception (#{e}) processing event (#{event.class})")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# @return [Concurrent::ThreadPoolExecutor]
|
69
|
+
attr_reader :executor
|
70
|
+
|
71
|
+
# @return [Logger]
|
72
|
+
attr_reader :logger
|
73
|
+
|
74
|
+
# @return [Concurrent::Set]
|
75
|
+
attr_reader :listeners
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Context
|
6
|
+
ERR_KIND_NON_STRING = 'context kind must be a string'
|
7
|
+
ERR_KIND_CANNOT_BE_KIND = '"kind" is not a valid context kind'
|
8
|
+
ERR_KIND_CANNOT_BE_MULTI = '"multi" is not a valid context kind'
|
9
|
+
ERR_KIND_INVALID_CHARS = 'context kind contains disallowed characters'
|
10
|
+
|
11
|
+
ERR_KEY_NON_STRING = 'context key must be a string'
|
12
|
+
ERR_KEY_EMPTY = 'context key must not be empty'
|
13
|
+
|
14
|
+
ERR_NAME_NON_STRING = 'context name must be a string'
|
15
|
+
|
16
|
+
ERR_ANONYMOUS_NON_BOOLEAN = 'context anonymous must be a boolean'
|
17
|
+
|
18
|
+
#
|
19
|
+
# We allow consumers of this SDK to provide us with either a Hash or an
|
20
|
+
# instance of an LDContext. This is convenient for them but not as much
|
21
|
+
# for us. To make the conversion slightly more convenient for us, we have
|
22
|
+
# created this method.
|
23
|
+
#
|
24
|
+
# @param context [Hash, LDContext]
|
25
|
+
# @return [LDContext]
|
26
|
+
#
|
27
|
+
def self.make_context(context)
|
28
|
+
return context if context.is_a?(LDContext)
|
29
|
+
|
30
|
+
LDContext.create(context)
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Returns an error message if the kind is invalid; nil otherwise.
|
35
|
+
#
|
36
|
+
# @param kind [any]
|
37
|
+
# @return [String, nil]
|
38
|
+
#
|
39
|
+
def self.validate_kind(kind)
|
40
|
+
return ERR_KIND_NON_STRING unless kind.is_a?(String)
|
41
|
+
return ERR_KIND_CANNOT_BE_KIND if kind == "kind"
|
42
|
+
return ERR_KIND_CANNOT_BE_MULTI if kind == "multi"
|
43
|
+
ERR_KIND_INVALID_CHARS unless kind.match?(/^[\w.-]+$/)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Returns an error message if the key is invalid; nil otherwise.
|
48
|
+
#
|
49
|
+
# @param key [any]
|
50
|
+
# @return [String, nil]
|
51
|
+
#
|
52
|
+
def self.validate_key(key)
|
53
|
+
return ERR_KEY_NON_STRING unless key.is_a?(String)
|
54
|
+
ERR_KEY_EMPTY if key == ""
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Returns an error message if the name is invalid; nil otherwise.
|
59
|
+
#
|
60
|
+
# @param name [any]
|
61
|
+
# @return [String, nil]
|
62
|
+
#
|
63
|
+
def self.validate_name(name)
|
64
|
+
ERR_NAME_NON_STRING unless name.nil? || name.is_a?(String)
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Returns an error message if anonymous is invalid; nil otherwise.
|
69
|
+
#
|
70
|
+
# @param anonymous [any]
|
71
|
+
# @param allow_nil [Boolean]
|
72
|
+
# @return [String, nil]
|
73
|
+
#
|
74
|
+
def self.validate_anonymous(anonymous, allow_nil)
|
75
|
+
return nil if anonymous.nil? && allow_nil
|
76
|
+
return nil if [true, false].include? anonymous
|
77
|
+
|
78
|
+
ERR_ANONYMOUS_NON_BOOLEAN
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# @param kind [String]
|
83
|
+
# @param key [String]
|
84
|
+
# @return [String]
|
85
|
+
#
|
86
|
+
def self.canonicalize_key_for_kind(kind, key)
|
87
|
+
# When building a FullyQualifiedKey, ':' and '%' are percent-escaped;
|
88
|
+
# we do not use a full URL-encoding function because implementations of
|
89
|
+
# this are inconsistent across platforms.
|
90
|
+
encoded = key.gsub("%", "%25").gsub(":", "%3A")
|
91
|
+
|
92
|
+
"#{kind}:#{encoded}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
class ContextFilter
|
4
|
+
#
|
5
|
+
# @param all_attributes_private [Boolean]
|
6
|
+
# @param private_attributes [Array<String>]
|
7
|
+
#
|
8
|
+
def initialize(all_attributes_private, private_attributes)
|
9
|
+
@all_attributes_private = all_attributes_private
|
10
|
+
|
11
|
+
@private_attributes = []
|
12
|
+
private_attributes.each do |attribute|
|
13
|
+
reference = LaunchDarkly::Reference.create(attribute)
|
14
|
+
@private_attributes << reference if reference.error.nil?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Return a hash representation of the provided context with attribute
|
20
|
+
# redaction applied.
|
21
|
+
#
|
22
|
+
# @param context [LaunchDarkly::LDContext]
|
23
|
+
# @return [Hash]
|
24
|
+
#
|
25
|
+
def filter(context)
|
26
|
+
return filter_single_context(context, true) unless context.multi_kind?
|
27
|
+
|
28
|
+
filtered = {kind: 'multi'}
|
29
|
+
(0...context.individual_context_count).each do |i|
|
30
|
+
c = context.individual_context(i)
|
31
|
+
next if c.nil?
|
32
|
+
|
33
|
+
filtered[c.kind] = filter_single_context(c, false)
|
34
|
+
end
|
35
|
+
|
36
|
+
filtered
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Apply redaction rules for a single context.
|
41
|
+
#
|
42
|
+
# @param context [LaunchDarkly::LDContext]
|
43
|
+
# @param include_kind [Boolean]
|
44
|
+
# @return [Hash]
|
45
|
+
#
|
46
|
+
private def filter_single_context(context, include_kind)
|
47
|
+
filtered = {key: context.key}
|
48
|
+
|
49
|
+
filtered[:kind] = context.kind if include_kind
|
50
|
+
filtered[:anonymous] = true if context.get_value(:anonymous)
|
51
|
+
|
52
|
+
redacted = []
|
53
|
+
private_attributes = @private_attributes.concat(context.private_attributes)
|
54
|
+
|
55
|
+
name = context.get_value(:name)
|
56
|
+
if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted)
|
57
|
+
filtered[:name] = name
|
58
|
+
end
|
59
|
+
|
60
|
+
context.get_custom_attribute_names.each do |attribute|
|
61
|
+
unless check_whole_attribute_private(attribute, private_attributes, redacted)
|
62
|
+
value = context.get_value(attribute)
|
63
|
+
filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
filtered[:_meta] = {redactedAttributes: redacted} unless redacted.empty?
|
68
|
+
|
69
|
+
filtered
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Check if an entire attribute should be redacted.
|
74
|
+
#
|
75
|
+
# @param attribute [Symbol]
|
76
|
+
# @param private_attributes [Array<Reference>]
|
77
|
+
# @param redacted [Array<Symbol>]
|
78
|
+
# @return [Boolean]
|
79
|
+
#
|
80
|
+
private def check_whole_attribute_private(attribute, private_attributes, redacted)
|
81
|
+
if @all_attributes_private
|
82
|
+
redacted << attribute
|
83
|
+
return true
|
84
|
+
end
|
85
|
+
|
86
|
+
private_attributes.each do |private_attribute|
|
87
|
+
if private_attribute.component(0) == attribute && private_attribute.depth == 1
|
88
|
+
redacted << attribute
|
89
|
+
return true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Apply redaction rules to the provided value.
|
98
|
+
#
|
99
|
+
# @param parent_path [Array<String>, nil]
|
100
|
+
# @param name [String]
|
101
|
+
# @param value [any]
|
102
|
+
# @param private_attributes [Array<Reference>]
|
103
|
+
# @param redacted [Array<Symbol>]
|
104
|
+
# @return [any]
|
105
|
+
#
|
106
|
+
private def redact_json_value(parent_path, name, value, private_attributes, redacted)
|
107
|
+
return value unless value.is_a?(Hash)
|
108
|
+
|
109
|
+
ret = {}
|
110
|
+
current_path = parent_path.clone || []
|
111
|
+
current_path << name
|
112
|
+
|
113
|
+
value.each do |k, v|
|
114
|
+
was_redacted = false
|
115
|
+
private_attributes.each do |private_attribute|
|
116
|
+
next unless private_attribute.depth == (current_path.count + 1)
|
117
|
+
|
118
|
+
component = private_attribute.component(current_path.count)
|
119
|
+
next unless component == k
|
120
|
+
|
121
|
+
match = true
|
122
|
+
(0...current_path.count).each do |i|
|
123
|
+
unless private_attribute.component(i) == current_path[i]
|
124
|
+
match = false
|
125
|
+
break
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if match
|
130
|
+
redacted << private_attribute.raw_path.to_sym
|
131
|
+
was_redacted = true
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
unless was_redacted
|
137
|
+
ret[k] = redact_json_value(current_path, k, v, private_attributes, redacted)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
ret
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "forwardable"
|
3
|
+
require "ldclient-rb/impl/dependency_tracker"
|
4
|
+
require "ldclient-rb/interfaces"
|
5
|
+
require "set"
|
6
|
+
|
7
|
+
module LaunchDarkly
|
8
|
+
module Impl
|
9
|
+
module DataSource
|
10
|
+
class StatusProvider
|
11
|
+
include LaunchDarkly::Interfaces::DataSource::StatusProvider
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
def_delegators :@status_broadcaster, :add_listener, :remove_listener
|
15
|
+
|
16
|
+
def initialize(status_broadcaster, update_sink)
|
17
|
+
# @type [Broadcaster]
|
18
|
+
@status_broadcaster = status_broadcaster
|
19
|
+
# @type [UpdateSink]
|
20
|
+
@data_source_update_sink = update_sink
|
21
|
+
end
|
22
|
+
|
23
|
+
def status
|
24
|
+
@data_source_update_sink.current_status
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class UpdateSink
|
29
|
+
include LaunchDarkly::Interfaces::DataSource::UpdateSink
|
30
|
+
|
31
|
+
# @return [LaunchDarkly::Interfaces::DataSource::Status]
|
32
|
+
attr_reader :current_status
|
33
|
+
|
34
|
+
def initialize(data_store, status_broadcaster, flag_change_broadcaster)
|
35
|
+
# @type [LaunchDarkly::Interfaces::FeatureStore]
|
36
|
+
@data_store = data_store
|
37
|
+
# @type [Broadcaster]
|
38
|
+
@status_broadcaster = status_broadcaster
|
39
|
+
# @type [Broadcaster]
|
40
|
+
@flag_change_broadcaster = flag_change_broadcaster
|
41
|
+
@dependency_tracker = LaunchDarkly::Impl::DependencyTracker.new
|
42
|
+
|
43
|
+
@mutex = Mutex.new
|
44
|
+
@current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
|
45
|
+
LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
|
46
|
+
Time.now,
|
47
|
+
nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
def init(all_data)
|
51
|
+
old_data = nil
|
52
|
+
monitor_store_update do
|
53
|
+
if @flag_change_broadcaster.has_listeners?
|
54
|
+
old_data = {}
|
55
|
+
LaunchDarkly::ALL_KINDS.each do |kind|
|
56
|
+
old_data[kind] = @data_store.all(kind)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@data_store.init(all_data)
|
61
|
+
end
|
62
|
+
|
63
|
+
update_full_dependency_tracker(all_data)
|
64
|
+
|
65
|
+
return if old_data.nil?
|
66
|
+
|
67
|
+
send_change_events(
|
68
|
+
compute_changed_items_for_full_data_set(old_data, all_data)
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def upsert(kind, item)
|
73
|
+
monitor_store_update { @data_store.upsert(kind, item) }
|
74
|
+
|
75
|
+
# TODO(sc-197908): We only want to do this if the store successfully
|
76
|
+
# updates the record.
|
77
|
+
@dependency_tracker.update_dependencies_from(kind, item[:key], item)
|
78
|
+
if @flag_change_broadcaster.has_listeners?
|
79
|
+
affected_items = Set.new
|
80
|
+
@dependency_tracker.add_affected_items(affected_items, {kind: kind, key: item[:key]})
|
81
|
+
send_change_events(affected_items)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete(kind, key, version)
|
86
|
+
monitor_store_update { @data_store.delete(kind, key, version) }
|
87
|
+
|
88
|
+
@dependency_tracker.update_dependencies_from(kind, key, nil)
|
89
|
+
if @flag_change_broadcaster.has_listeners?
|
90
|
+
affected_items = Set.new
|
91
|
+
@dependency_tracker.add_affected_items(affected_items, {kind: kind, key: key})
|
92
|
+
send_change_events(affected_items)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def update_status(new_state, new_error)
|
97
|
+
return if new_state.nil?
|
98
|
+
|
99
|
+
status_to_broadcast = nil
|
100
|
+
|
101
|
+
@mutex.synchronize do
|
102
|
+
old_status = @current_status
|
103
|
+
|
104
|
+
if new_state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED && old_status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
|
105
|
+
# See {LaunchDarkly::Interfaces::DataSource::UpdateSink#update_status} for more information
|
106
|
+
new_state = LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
|
107
|
+
end
|
108
|
+
|
109
|
+
unless new_state == old_status.state && new_error.nil?
|
110
|
+
@current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
|
111
|
+
new_state,
|
112
|
+
new_state == current_status.state ? current_status.state_since : Time.now,
|
113
|
+
new_error.nil? ? current_status.last_error : new_error
|
114
|
+
)
|
115
|
+
status_to_broadcast = current_status
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
@status_broadcaster.broadcast(status_to_broadcast) unless status_to_broadcast.nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
private def update_full_dependency_tracker(all_data)
|
123
|
+
@dependency_tracker.reset
|
124
|
+
all_data.each do |kind, items|
|
125
|
+
items.each do |key, item|
|
126
|
+
@dependency_tracker.update_dependencies_from(kind, item.key, item)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
#
|
133
|
+
# Method to monitor updates to the store. You provide a block to update
|
134
|
+
# the store. This mthod wraps that block, catching and re-raising all
|
135
|
+
# errors, and notifying all status listeners of the error.
|
136
|
+
#
|
137
|
+
private def monitor_store_update
|
138
|
+
begin
|
139
|
+
yield
|
140
|
+
rescue => e
|
141
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR, 0, e.to_s, Time.now)
|
142
|
+
update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
143
|
+
raise
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
#
|
149
|
+
# @param [Hash] old_data
|
150
|
+
# @param [Hash] new_data
|
151
|
+
# @return [Set]
|
152
|
+
#
|
153
|
+
private def compute_changed_items_for_full_data_set(old_data, new_data)
|
154
|
+
affected_items = Set.new
|
155
|
+
|
156
|
+
LaunchDarkly::ALL_KINDS.each do |kind|
|
157
|
+
old_items = old_data[kind] || {}
|
158
|
+
new_items = new_data[kind] || {}
|
159
|
+
|
160
|
+
old_items.keys.concat(new_items.keys).each do |key|
|
161
|
+
old_item = old_items[key]
|
162
|
+
new_item = new_items[key]
|
163
|
+
|
164
|
+
next if old_item.nil? && new_item.nil?
|
165
|
+
|
166
|
+
if old_item.nil? || new_item.nil? || old_item[:version] < new_item[:version]
|
167
|
+
@dependency_tracker.add_affected_items(affected_items, {kind: kind, key: key.to_s})
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
affected_items
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# @param affected_items [Set]
|
177
|
+
#
|
178
|
+
private def send_change_events(affected_items)
|
179
|
+
affected_items.each do |item|
|
180
|
+
if item[:kind] == LaunchDarkly::FEATURES
|
181
|
+
@flag_change_broadcaster.broadcast(LaunchDarkly::Interfaces::FlagChange.new(item[:key]))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|