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,213 @@
1
+ require 'ldclient-rb/impl/integrations/test_data/test_data_source'
2
+ require 'ldclient-rb/impl/model/feature_flag'
3
+ require 'ldclient-rb/impl/model/segment'
4
+ require 'ldclient-rb/integrations/test_data/flag_builder'
5
+
6
+ require 'concurrent/atomics'
7
+
8
+ module LaunchDarkly
9
+ module Integrations
10
+ #
11
+ # A mechanism for providing dynamically updatable feature flag state in a simplified form to an SDK
12
+ # client in test scenarios.
13
+ #
14
+ # Unlike {LaunchDarkly::Integrations::FileData}, this mechanism does not use any external resources. It
15
+ # provides only the data that the application has put into it using the {#update} method.
16
+ #
17
+ # @example
18
+ # td = LaunchDarkly::Integrations::TestData.data_source
19
+ # td.update(td.flag("flag-key-1").variation_for_all(true))
20
+ # config = LaunchDarkly::Config.new(data_source: td)
21
+ # client = LaunchDarkly::LDClient.new('sdkKey', config)
22
+ # # flags can be updated at any time:
23
+ # td.update(td.flag("flag-key-2")
24
+ # .variation_for_key("user", some-user-key", true)
25
+ # .fallthrough_variation(false))
26
+ #
27
+ # The above example uses a simple boolean flag, but more complex configurations are possible using
28
+ # the methods of the {FlagBuilder} that is returned by {#flag}. {FlagBuilder}
29
+ # supports many of the ways a flag can be configured on the LaunchDarkly dashboard, but does not
30
+ # currently support 1. rule operators other than "in" and "not in", or 2. percentage rollouts.
31
+ #
32
+ # If the same `TestData` instance is used to configure multiple `LDClient` instances,
33
+ # any changes made to the data will propagate to all of the `LDClient`s.
34
+ #
35
+ # @since 6.3.0
36
+ #
37
+ class TestData
38
+ # Creates a new instance of the test data source.
39
+ #
40
+ # @return [TestData] a new configurable test data source
41
+ def self.data_source
42
+ self.new
43
+ end
44
+
45
+ # @private
46
+ def initialize
47
+ @flag_builders = Hash.new
48
+ @current_flags = Hash.new
49
+ @current_segments = Hash.new
50
+ @instances = Array.new
51
+ @instances_lock = Concurrent::ReadWriteLock.new
52
+ @lock = Concurrent::ReadWriteLock.new
53
+ end
54
+
55
+ #
56
+ # Called internally by the SDK to determine what arguments to pass to call
57
+ # You do not need to call this method.
58
+ #
59
+ # @private
60
+ def arity
61
+ 2
62
+ end
63
+
64
+ #
65
+ # Called internally by the SDK to associate this test data source with an {@code LDClient} instance.
66
+ # You do not need to call this method.
67
+ #
68
+ # @private
69
+ def call(_, config)
70
+ impl = LaunchDarkly::Impl::Integrations::TestData::TestDataSource.new(config.feature_store, self)
71
+ @instances_lock.with_write_lock { @instances.push(impl) }
72
+ impl
73
+ end
74
+
75
+ #
76
+ # Creates or copies a {FlagBuilder} for building a test flag configuration.
77
+ #
78
+ # If this flag key has already been defined in this `TestData` instance, then the builder
79
+ # starts with the same configuration that was last provided for this flag.
80
+ #
81
+ # Otherwise, it starts with a new default configuration in which the flag has `true` and
82
+ # `false` variations, is `true` for all contexts when targeting is turned on and
83
+ # `false` otherwise, and currently has targeting turned on. You can change any of those
84
+ # properties, and provide more complex behavior, using the {FlagBuilder} methods.
85
+ #
86
+ # Once you have set the desired configuration, pass the builder to {#update}.
87
+ #
88
+ # @param key [String] the flag key
89
+ # @return [FlagBuilder] a flag configuration builder
90
+ #
91
+ def flag(key)
92
+ existing_builder = @lock.with_read_lock { @flag_builders[key] }
93
+ if existing_builder.nil?
94
+ FlagBuilder.new(key).boolean_flag
95
+ else
96
+ existing_builder.clone
97
+ end
98
+ end
99
+
100
+ #
101
+ # Updates the test data with the specified flag configuration.
102
+ #
103
+ # This has the same effect as if a flag were added or modified on the LaunchDarkly dashboard.
104
+ # It immediately propagates the flag change to any `LDClient` instance(s) that you have
105
+ # already configured to use this `TestData`. If no `LDClient` has been started yet,
106
+ # it simply adds this flag to the test data which will be provided to any `LDClient` that
107
+ # you subsequently configure.
108
+ #
109
+ # Any subsequent changes to this {FlagBuilder} instance do not affect the test data,
110
+ # unless you call {#update} again.
111
+ #
112
+ # @param flag_builder [FlagBuilder] a flag configuration builder
113
+ # @return [TestData] the TestData instance
114
+ #
115
+ def update(flag_builder)
116
+ new_flag = nil
117
+ @lock.with_write_lock do
118
+ @flag_builders[flag_builder.key] = flag_builder
119
+ version = 0
120
+ flag_key = flag_builder.key.to_sym
121
+ if @current_flags[flag_key]
122
+ version = @current_flags[flag_key][:version]
123
+ end
124
+ new_flag = Impl::Model.deserialize(FEATURES, flag_builder.build(version+1))
125
+ @current_flags[flag_key] = new_flag
126
+ end
127
+ update_item(FEATURES, new_flag)
128
+ self
129
+ end
130
+
131
+ #
132
+ # Copies a full feature flag data model object into the test data.
133
+ #
134
+ # It immediately propagates the flag change to any `LDClient` instance(s) that you have already
135
+ # configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
136
+ # this flag to the test data which will be provided to any LDClient that you subsequently
137
+ # configure.
138
+ #
139
+ # Use this method if you need to use advanced flag configuration properties that are not supported by
140
+ # the simplified {FlagBuilder} API. Otherwise it is recommended to use the regular {flag}/{update}
141
+ # mechanism to avoid dependencies on details of the data model.
142
+ #
143
+ # You cannot make incremental changes with {flag}/{update} to a flag that has been added in this way;
144
+ # you can only replace it with an entirely new flag configuration.
145
+ #
146
+ # @param flag [Hash] the flag configuration
147
+ # @return [TestData] the TestData instance
148
+ #
149
+ def use_preconfigured_flag(flag)
150
+ use_preconfigured_item(FEATURES, flag, @current_flags)
151
+ end
152
+
153
+ #
154
+ # Copies a full segment data model object into the test data.
155
+ #
156
+ # It immediately propagates the change to any `LDClient` instance(s) that you have already
157
+ # configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
158
+ # this segment to the test data which will be provided to any LDClient that you subsequently
159
+ # configure.
160
+ #
161
+ # This method is currently the only way to inject segment data, since there is no builder
162
+ # API for segments. It is mainly intended for the SDK's own tests of segment functionality,
163
+ # since application tests that need to produce a desired evaluation state could do so more easily
164
+ # by just setting flag values.
165
+ #
166
+ # @param segment [Hash] the segment configuration
167
+ # @return [TestData] the TestData instance
168
+ #
169
+ def use_preconfigured_segment(segment)
170
+ use_preconfigured_item(SEGMENTS, segment, @current_segments)
171
+ end
172
+
173
+ private def use_preconfigured_item(kind, item, current)
174
+ item = Impl::Model.deserialize(kind, item)
175
+ key = item.key.to_sym
176
+ @lock.with_write_lock do
177
+ old_item = current[key]
178
+ unless old_item.nil?
179
+ data = item.as_json
180
+ data[:version] = old_item.version + 1
181
+ item = Impl::Model.deserialize(kind, data)
182
+ end
183
+ current[key] = item
184
+ end
185
+ update_item(kind, item)
186
+ self
187
+ end
188
+
189
+ private def update_item(kind, item)
190
+ @instances_lock.with_read_lock do
191
+ @instances.each do | instance |
192
+ instance.upsert(kind, item)
193
+ end
194
+ end
195
+ end
196
+
197
+ # @private
198
+ def make_init_data
199
+ @lock.with_read_lock do
200
+ {
201
+ FEATURES => @current_flags.clone,
202
+ SEGMENTS => @current_segments.clone,
203
+ }
204
+ end
205
+ end
206
+
207
+ # @private
208
+ def closed_instance(instance)
209
+ @instances_lock.with_write_lock { @instances.delete(instance) }
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,246 @@
1
+ require "concurrent/atomics"
2
+
3
+ require "ldclient-rb/expiring_cache"
4
+
5
+ module LaunchDarkly
6
+ module Integrations
7
+ #
8
+ # Support code that may be helpful in creating integrations.
9
+ #
10
+ # @since 5.5.0
11
+ #
12
+ module Util
13
+ #
14
+ # CachingStoreWrapper is a partial implementation of the {LaunchDarkly::Interfaces::FeatureStore}
15
+ # pattern that delegates part of its behavior to another object, while providing optional caching
16
+ # behavior and other logic that would otherwise be repeated in every feature store implementation.
17
+ # This makes it easier to create new database integrations by implementing only the database-specific
18
+ # logic.
19
+ #
20
+ # The mixin {FeatureStoreCore} describes the methods that need to be supported by the inner
21
+ # implementation object.
22
+ #
23
+ class CachingStoreWrapper
24
+ include LaunchDarkly::Interfaces::FeatureStore
25
+
26
+ #
27
+ # Creates a new store wrapper instance.
28
+ #
29
+ # @param core [Object] an object that implements the {FeatureStoreCore} methods
30
+ # @param opts [Hash] a hash that may include cache-related options; all others will be ignored
31
+ # @option opts [Float] :expiration (15) cache TTL; zero means no caching
32
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache
33
+ #
34
+ def initialize(core, opts)
35
+ @core = core
36
+
37
+ expiration_seconds = opts[:expiration] || 15
38
+ if expiration_seconds > 0
39
+ capacity = opts[:capacity] || 1000
40
+ @cache = ExpiringCache.new(capacity, expiration_seconds)
41
+ else
42
+ @cache = nil
43
+ end
44
+
45
+ @inited = Concurrent::AtomicBoolean.new(false)
46
+ @has_available_method = @core.respond_to? :available?
47
+ end
48
+
49
+ def monitoring_enabled?
50
+ @has_available_method
51
+ end
52
+
53
+ def available?
54
+ return false unless @has_available_method
55
+
56
+ @core.available?
57
+ end
58
+
59
+ def init(all_data)
60
+ @core.init_internal(all_data)
61
+ @inited.make_true
62
+
63
+ unless @cache.nil?
64
+ @cache.clear
65
+ all_data.each do |kind, items|
66
+ @cache[kind] = items_if_not_deleted(items)
67
+ items.each do |key, item|
68
+ @cache[item_cache_key(kind, key)] = [item]
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def get(kind, key)
75
+ unless @cache.nil?
76
+ cache_key = item_cache_key(kind, key)
77
+ cached = @cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
78
+ return item_if_not_deleted(cached[0]) unless cached.nil?
79
+ end
80
+
81
+ item = @core.get_internal(kind, key)
82
+
83
+ unless @cache.nil?
84
+ @cache[cache_key] = [item]
85
+ end
86
+
87
+ item_if_not_deleted(item)
88
+ end
89
+
90
+ def all(kind)
91
+ unless @cache.nil?
92
+ items = @cache[all_cache_key(kind)]
93
+ return items unless items.nil?
94
+ end
95
+
96
+ items = items_if_not_deleted(@core.get_all_internal(kind))
97
+ @cache[all_cache_key(kind)] = items unless @cache.nil?
98
+ items
99
+ end
100
+
101
+ def upsert(kind, item)
102
+ new_state = @core.upsert_internal(kind, item)
103
+
104
+ unless @cache.nil?
105
+ @cache[item_cache_key(kind, item[:key])] = [new_state]
106
+ @cache.delete(all_cache_key(kind))
107
+ end
108
+ end
109
+
110
+ def delete(kind, key, version)
111
+ upsert(kind, { key: key, version: version, deleted: true })
112
+ end
113
+
114
+ def initialized?
115
+ return true if @inited.value
116
+
117
+ if @cache.nil?
118
+ result = @core.initialized_internal?
119
+ else
120
+ result = @cache[inited_cache_key]
121
+ if result.nil?
122
+ result = @core.initialized_internal?
123
+ @cache[inited_cache_key] = result
124
+ end
125
+ end
126
+
127
+ @inited.make_true if result
128
+ result
129
+ end
130
+
131
+ def stop
132
+ @core.stop
133
+ end
134
+
135
+ private
136
+
137
+ # We use just one cache for 3 kinds of objects. Individual entities use a key like 'features:my-flag'.
138
+ def item_cache_key(kind, key)
139
+ kind[:namespace] + ":" + key.to_s
140
+ end
141
+
142
+ # The result of a call to get_all_internal is cached using the "kind" object as a key.
143
+ def all_cache_key(kind)
144
+ kind
145
+ end
146
+
147
+ # The result of initialized_internal? is cached using this key.
148
+ def inited_cache_key
149
+ "$inited"
150
+ end
151
+
152
+ def item_if_not_deleted(item)
153
+ (item.nil? || item[:deleted]) ? nil : item
154
+ end
155
+
156
+ def items_if_not_deleted(items)
157
+ items.select { |key, item| !item[:deleted] }
158
+ end
159
+ end
160
+
161
+ #
162
+ # This module describes the methods that you must implement on your own object in order to
163
+ # use {CachingStoreWrapper}.
164
+ #
165
+ module FeatureStoreCore
166
+ #
167
+ # Initializes the store. This is the same as {LaunchDarkly::Interfaces::FeatureStore#init},
168
+ # but the wrapper will take care of updating the cache if caching is enabled.
169
+ #
170
+ # If possible, the store should update the entire data set atomically. If that is not possible,
171
+ # it should iterate through the outer hash and then the inner hash using the existing iteration
172
+ # order of those hashes (the SDK will ensure that the items were inserted into the hashes in
173
+ # the correct order), storing each item, and then delete any leftover items at the very end.
174
+ #
175
+ # @param all_data [Hash] a hash where each key is one of the data kind objects, and each
176
+ # value is in turn a hash of string keys to entities
177
+ # @return [void]
178
+ #
179
+ def init_internal(all_data)
180
+ end
181
+
182
+ #
183
+ # Retrieves a single entity. This is the same as {LaunchDarkly::Interfaces::FeatureStore#get}
184
+ # except that 1. the wrapper will take care of filtering out deleted entities by checking the
185
+ # `:deleted` property, so you can just return exactly what was in the data store, and 2. the
186
+ # wrapper will take care of checking and updating the cache if caching is enabled.
187
+ #
188
+ # @param kind [Object] the kind of entity to get
189
+ # @param key [String] the unique key of the entity to get
190
+ # @return [Hash] the entity; nil if the key was not found
191
+ #
192
+ def get_internal(kind, key)
193
+ end
194
+
195
+ #
196
+ # Retrieves all entities of the specified kind. This is the same as {LaunchDarkly::Interfaces::FeatureStore#all}
197
+ # except that 1. the wrapper will take care of filtering out deleted entities by checking the
198
+ # `:deleted` property, so you can just return exactly what was in the data store, and 2. the
199
+ # wrapper will take care of checking and updating the cache if caching is enabled.
200
+ #
201
+ # @param kind [Object] the kind of entity to get
202
+ # @return [Hash] a hash where each key is the entity's `:key` property and each value
203
+ # is the entity
204
+ #
205
+ def get_all_internal(kind)
206
+ end
207
+
208
+ #
209
+ # Attempts to add or update an entity. This is the same as {LaunchDarkly::Interfaces::FeatureStore#upsert}
210
+ # except that 1. the wrapper will take care of updating the cache if caching is enabled, and 2.
211
+ # the method is expected to return the final state of the entity (i.e. either the `item`
212
+ # parameter if the update succeeded, or the previously existing entity in the store if the
213
+ # update failed; this is used for the caching logic).
214
+ #
215
+ # Note that FeatureStoreCore does not have a `delete` method. This is because {CachingStoreWrapper}
216
+ # implements `delete` by simply calling `upsert` with an item whose `:deleted` property is true.
217
+ #
218
+ # @param kind [Object] the kind of entity to add or update
219
+ # @param item [Hash] the entity to add or update
220
+ # @return [Hash] the entity as it now exists in the store after the update
221
+ #
222
+ def upsert_internal(kind, item)
223
+ end
224
+
225
+ #
226
+ # Checks whether this store has been initialized. This is the same as
227
+ # {LaunchDarkly::Interfaces::FeatureStore#initialized?} except that there is less of a concern
228
+ # for efficiency, because the wrapper will use caching and memoization in order to call the method
229
+ # as little as possible.
230
+ #
231
+ # @return [Boolean] true if the store is in an initialized state
232
+ #
233
+ def initialized_internal?
234
+ end
235
+
236
+ #
237
+ # Performs any necessary cleanup to shut down the store when the client is being shut down.
238
+ #
239
+ # @return [void]
240
+ #
241
+ def stop
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,6 @@
1
+ require "ldclient-rb/integrations/consul"
2
+ require "ldclient-rb/integrations/dynamodb"
3
+ require "ldclient-rb/integrations/file_data"
4
+ require "ldclient-rb/integrations/redis"
5
+ require "ldclient-rb/integrations/test_data"
6
+ require "ldclient-rb/integrations/util/store_wrapper"