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.
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