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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. 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 user, generated by
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(flag, value, variation, reason = nil, details_only_if_tracked = false)
20
- key = flag[: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
- with_details = !details_only_if_tracked || flag[:trackEvents]
24
- if !with_details && flag[:debugEventsUntilDate]
25
- with_details = flag[:debugEventsUntilDate] > Impl::Util::current_time_millis
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
- if with_details
28
- meta[:version] = flag[:version]
29
- meta[:reason] = reason if !reason.nil?
36
+
37
+ unless omit_details
38
+ meta[:version] = flag_state[:version]
30
39
  end
31
- meta[:variation] = variation if !variation.nil?
32
- meta[:trackEvents] = true if flag[:trackEvents]
33
- meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
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 user).
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
- if !@store.nil?
26
- @cache = ExpiringCache.new(big_segments_config.user_cache_size, big_segments_config.user_cache_time)
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 if !@poll_worker.nil?
36
- @store.stop if !@store.nil?
35
+ @poll_worker.stop unless @poll_worker.nil?
36
+ @store.stop unless @store.nil?
37
37
  end
38
38
 
39
- def get_user_membership(user_key)
40
- return nil if !@store
41
- membership = @cache[user_key]
42
- if !membership
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.hash_for_user_key(user_key))
44
+ membership = @store.get_membership(BigSegmentStoreManager.hash_for_context_key(context_key))
45
45
  membership = EMPTY_MEMBERSHIP if membership.nil?
46
- @cache[user_key] = membership
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 if !@last_status
53
- if !@last_status.available
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
- if !@store.nil?
65
+ unless @store.nil?
66
66
  begin
67
67
  metadata = @store.get_metadata
68
- new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || is_stale(metadata.last_up_to_date))
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 is_stale(timestamp)
79
+ def stale?(timestamp)
80
80
  !timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
81
81
  end
82
82
 
83
- def self.hash_for_user_key(user_key)
84
- Digest::SHA256.base64digest(user_key)
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