launchdarkly-server-sdk 6.3.0 → 8.0.0
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 +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
|