launchdarkly-server-sdk 8.8.3-java

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