ldclient-rb 5.4.3 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +33 -6
  3. data/CHANGELOG.md +19 -0
  4. data/CONTRIBUTING.md +0 -12
  5. data/Gemfile.lock +22 -3
  6. data/README.md +41 -35
  7. data/ldclient-rb.gemspec +4 -3
  8. data/lib/ldclient-rb.rb +9 -1
  9. data/lib/ldclient-rb/cache_store.rb +1 -0
  10. data/lib/ldclient-rb/config.rb +201 -90
  11. data/lib/ldclient-rb/evaluation.rb +56 -8
  12. data/lib/ldclient-rb/event_summarizer.rb +3 -0
  13. data/lib/ldclient-rb/events.rb +16 -0
  14. data/lib/ldclient-rb/expiring_cache.rb +1 -0
  15. data/lib/ldclient-rb/file_data_source.rb +18 -13
  16. data/lib/ldclient-rb/flags_state.rb +3 -2
  17. data/lib/ldclient-rb/impl.rb +13 -0
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  20. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  21. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  22. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  23. data/lib/ldclient-rb/in_memory_store.rb +15 -4
  24. data/lib/ldclient-rb/integrations.rb +55 -0
  25. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  26. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  27. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  28. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  29. data/lib/ldclient-rb/interfaces.rb +153 -0
  30. data/lib/ldclient-rb/ldclient.rb +135 -77
  31. data/lib/ldclient-rb/memoized_value.rb +2 -0
  32. data/lib/ldclient-rb/newrelic.rb +1 -0
  33. data/lib/ldclient-rb/non_blocking_thread_pool.rb +3 -3
  34. data/lib/ldclient-rb/polling.rb +1 -0
  35. data/lib/ldclient-rb/redis_store.rb +24 -190
  36. data/lib/ldclient-rb/requestor.rb +3 -2
  37. data/lib/ldclient-rb/simple_lru_cache.rb +1 -0
  38. data/lib/ldclient-rb/stream.rb +22 -10
  39. data/lib/ldclient-rb/user_filter.rb +1 -0
  40. data/lib/ldclient-rb/util.rb +1 -0
  41. data/lib/ldclient-rb/version.rb +1 -1
  42. data/scripts/gendocs.sh +12 -0
  43. data/spec/feature_store_spec_base.rb +173 -72
  44. data/spec/file_data_source_spec.rb +2 -2
  45. data/spec/http_util.rb +103 -0
  46. data/spec/in_memory_feature_store_spec.rb +1 -1
  47. data/spec/integrations/consul_feature_store_spec.rb +41 -0
  48. data/spec/integrations/dynamodb_feature_store_spec.rb +104 -0
  49. data/spec/integrations/store_wrapper_spec.rb +276 -0
  50. data/spec/ldclient_spec.rb +83 -4
  51. data/spec/redis_feature_store_spec.rb +25 -16
  52. data/spec/requestor_spec.rb +44 -38
  53. data/spec/stream_spec.rb +18 -18
  54. metadata +55 -33
  55. data/lib/sse_client.rb +0 -4
  56. data/lib/sse_client/backoff.rb +0 -38
  57. data/lib/sse_client/sse_client.rb +0 -171
  58. data/lib/sse_client/sse_events.rb +0 -67
  59. data/lib/sse_client/streaming_http.rb +0 -199
  60. data/spec/sse_client/sse_client_spec.rb +0 -177
  61. data/spec/sse_client/sse_events_spec.rb +0 -100
  62. data/spec/sse_client/sse_shared.rb +0 -82
  63. data/spec/sse_client/streaming_http_spec.rb +0 -263
@@ -6,20 +6,31 @@ module LaunchDarkly
6
6
  # we add another storable data type in the future, as long as it follows the same pattern
7
7
  # (having "key", "version", and "deleted" properties), we only need to add a corresponding
8
8
  # constant here and the existing store should be able to handle it.
9
+ #
10
+ # The :priority and :get_dependency_keys properties are used by FeatureStoreDataSetSorter
11
+ # to ensure data consistency during non-atomic updates.
12
+
13
+ # @private
9
14
  FEATURES = {
10
- namespace: "features"
15
+ namespace: "features",
16
+ priority: 1, # that is, features should be stored after segments
17
+ get_dependency_keys: lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } }
11
18
  }.freeze
12
19
 
20
+ # @private
13
21
  SEGMENTS = {
14
- namespace: "segments"
22
+ namespace: "segments",
23
+ priority: 0
15
24
  }.freeze
16
25
 
17
26
  #
18
27
  # Default implementation of the LaunchDarkly client's feature store, using an in-memory
19
- # cache. This object holds feature flags and related data received from the
20
- # streaming API.
28
+ # cache. This object holds feature flags and related data received from LaunchDarkly.
29
+ # Database-backed implementations are available in {LaunchDarkly::Integrations}.
21
30
  #
22
31
  class InMemoryFeatureStore
32
+ include LaunchDarkly::Interfaces::FeatureStore
33
+
23
34
  def initialize
24
35
  @items = Hash.new
25
36
  @lock = Concurrent::ReadWriteLock.new
@@ -0,0 +1,55 @@
1
+ require "ldclient-rb/integrations/consul"
2
+ require "ldclient-rb/integrations/dynamodb"
3
+ require "ldclient-rb/integrations/redis"
4
+ require "ldclient-rb/integrations/util/store_wrapper"
5
+
6
+ module LaunchDarkly
7
+ #
8
+ # Tools for connecting the LaunchDarkly client to other software.
9
+ #
10
+ module Integrations
11
+ #
12
+ # Integration with [Consul](https://www.consul.io/).
13
+ #
14
+ # Note that in order to use this integration, you must first install the gem `diplomat`.
15
+ #
16
+ # @since 5.5.0
17
+ #
18
+ module Consul
19
+ # code is in ldclient-rb/impl/integrations/consul_impl
20
+ end
21
+
22
+ #
23
+ # Integration with [DynamoDB](https://aws.amazon.com/dynamodb/).
24
+ #
25
+ # Note that in order to use this integration, you must first install one of the AWS SDK gems: either
26
+ # `aws-sdk-dynamodb`, or the full `aws-sdk`.
27
+ #
28
+ # @since 5.5.0
29
+ #
30
+ module DynamoDB
31
+ # code is in ldclient-rb/impl/integrations/dynamodb_impl
32
+ end
33
+
34
+ #
35
+ # Integration with [Redis](https://redis.io/).
36
+ #
37
+ # Note that in order to use this integration, you must first install the `redis` and `connection-pool`
38
+ # gems.
39
+ #
40
+ # @since 5.5.0
41
+ #
42
+ module Redis
43
+ # code is in ldclient-rb/impl/integrations/redis_impl
44
+ end
45
+
46
+ #
47
+ # Support code that may be helpful in creating integrations.
48
+ #
49
+ # @since 5.5.0
50
+ #
51
+ module Util
52
+ # code is in ldclient-rb/integrations/util/
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ require "ldclient-rb/impl/integrations/consul_impl"
2
+ require "ldclient-rb/integrations/util/store_wrapper"
3
+
4
+ module LaunchDarkly
5
+ module Integrations
6
+ module Consul
7
+ #
8
+ # Default value for the `prefix` option for {new_feature_store}.
9
+ #
10
+ # @return [String] the default key prefix
11
+ #
12
+ def self.default_prefix
13
+ 'launchdarkly'
14
+ end
15
+
16
+ #
17
+ # Creates a Consul-backed persistent feature store.
18
+ #
19
+ # To use this method, you must first install the gem `diplomat`. Then, put the object returned by
20
+ # this method into the `feature_store` property of your client configuration ({LaunchDarkly::Config}).
21
+ #
22
+ # @param opts [Hash] the configuration options
23
+ # @option opts [Hash] :consul_config an instance of `Diplomat::Configuration` to replace the default
24
+ # Consul client configuration (note that this is exactly the same as modifying `Diplomat.configuration`)
25
+ # @option opts [String] :url shortcut for setting the `url` property of the Consul client configuration
26
+ # @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
27
+ # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
28
+ # @option opts [Integer] :expiration_seconds (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
29
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache
30
+ # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
31
+ #
32
+ def self.new_feature_store(opts, &block)
33
+ core = LaunchDarkly::Impl::Integrations::Consul::ConsulFeatureStoreCore.new(opts)
34
+ return LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ require "ldclient-rb/impl/integrations/dynamodb_impl"
2
+ require "ldclient-rb/integrations/util/store_wrapper"
3
+
4
+ module LaunchDarkly
5
+ module Integrations
6
+ module DynamoDB
7
+ #
8
+ # Creates a DynamoDB-backed persistent feature store. For more details about how and why you can
9
+ # use a persistent feature store, see the
10
+ # [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
11
+ #
12
+ # To use this method, you must first install one of the AWS SDK gems: either `aws-sdk-dynamodb`, or
13
+ # the full `aws-sdk`. Then, put the object returned by this method into the `feature_store` property
14
+ # of your client configuration ({LaunchDarkly::Config}).
15
+ #
16
+ # @example Configuring the feature store
17
+ # store = LaunchDarkly::Integrations::DynamoDB::new_feature_store("my-table-name")
18
+ # config = LaunchDarkly::Config.new(feature_store: store)
19
+ # client = LaunchDarkly::LDClient.new(my_sdk_key, config)
20
+ #
21
+ # Note that the specified table must already exist in DynamoDB. It must have a partition key called
22
+ # "namespace", and a sort key called "key" (both strings). The SDK does not create the table
23
+ # automatically because it has no way of knowing what additional properties (such as permissions
24
+ # and throughput) you would want it to have.
25
+ #
26
+ # By default, the DynamoDB client will try to get your AWS credentials and region name from
27
+ # environment variables and/or local configuration files, as described in the AWS SDK documentation.
28
+ # You can also specify any supported AWS SDK options in `dynamodb_opts`-- or, provide an
29
+ # already-configured DynamoDB client in `existing_client`.
30
+ #
31
+ # @param table_name [String] name of an existing DynamoDB table
32
+ # @param opts [Hash] the configuration options
33
+ # @option opts [Hash] :dynamodb_opts options to pass to the DynamoDB client constructor (ignored if you specify `:existing_client`)
34
+ # @option opts [Object] :existing_client an already-constructed DynamoDB client for the feature store to use
35
+ # @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
36
+ # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
37
+ # @option opts [Integer] :expiration_seconds (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
38
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache
39
+ # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
40
+ #
41
+ def self.new_feature_store(table_name, opts)
42
+ core = LaunchDarkly::Impl::Integrations::DynamoDB::DynamoDBFeatureStoreCore.new(table_name, opts)
43
+ return LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ require "ldclient-rb/redis_store" # eventually we will just refer to impl/integrations/redis_impl directly
2
+
3
+ module LaunchDarkly
4
+ module Integrations
5
+ module Redis
6
+ #
7
+ # Default value for the `redis_url` option for {new_feature_store}. This points to an instance of
8
+ # Redis running at `localhost` with its default port.
9
+ #
10
+ # @return [String] the default Redis URL
11
+ #
12
+ def self.default_redis_url
13
+ 'redis://localhost:6379/0'
14
+ end
15
+
16
+ #
17
+ # Default value for the `prefix` option for {new_feature_store}.
18
+ #
19
+ # @return [String] the default key prefix
20
+ #
21
+ def self.default_prefix
22
+ 'launchdarkly'
23
+ end
24
+
25
+ #
26
+ # Creates a Redis-backed persistent feature store. For more details about how and why you can
27
+ # use a persistent feature store, see the
28
+ # [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
29
+ #
30
+ # To use this method, you must first have the `redis` and `connection-pool` gems installed. Then,
31
+ # put the object returned by this method into the `feature_store` property of your
32
+ # client configuration.
33
+ #
34
+ # @example Configuring the feature store
35
+ # store = LaunchDarkly::Integrations::Redis::new_feature_store(redis_url: "redis://my-server")
36
+ # config = LaunchDarkly::Config.new(feature_store: store)
37
+ # client = LaunchDarkly::LDClient.new(my_sdk_key, config)
38
+ #
39
+ # @param opts [Hash] the configuration options
40
+ # @option opts [String] :redis_url (default_redis_url) URL of the Redis instance (shortcut for omitting `redis_opts`)
41
+ # @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just `redis_url`)
42
+ # @option opts [String] :prefix (default_prefix) namespace prefix to add to all hash keys used by LaunchDarkly
43
+ # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
44
+ # @option opts [Integer] :max_connections size of the Redis connection pool
45
+ # @option opts [Integer] :expiration_seconds (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
46
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache
47
+ # @option opts [Object] :pool custom connection pool, if desired
48
+ # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
49
+ #
50
+ def self.new_feature_store(opts)
51
+ return RedisFeatureStore.new(opts)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,230 @@
1
+ require "concurrent/atomics"
2
+
3
+ require "ldclient-rb/expiring_cache"
4
+
5
+ module LaunchDarkly
6
+ module Integrations
7
+ module Util
8
+ #
9
+ # CachingStoreWrapper is a partial implementation of the {LaunchDarkly::Interfaces::FeatureStore}
10
+ # pattern that delegates part of its behavior to another object, while providing optional caching
11
+ # behavior and other logic that would otherwise be repeated in every feature store implementation.
12
+ # This makes it easier to create new database integrations by implementing only the database-specific
13
+ # logic.
14
+ #
15
+ # The mixin {FeatureStoreCore} describes the methods that need to be supported by the inner
16
+ # implementation object.
17
+ #
18
+ class CachingStoreWrapper
19
+ include LaunchDarkly::Interfaces::FeatureStore
20
+
21
+ #
22
+ # Creates a new store wrapper instance.
23
+ #
24
+ # @param core [Object] an object that implements the {FeatureStoreCore} methods
25
+ # @param opts [Hash] a hash that may include cache-related options; all others will be ignored
26
+ # @option opts [Float] :expiration_seconds (15) cache TTL; zero means no caching
27
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache
28
+ #
29
+ def initialize(core, opts)
30
+ @core = core
31
+
32
+ expiration_seconds = opts[:expiration] || 15
33
+ if expiration_seconds > 0
34
+ capacity = opts[:capacity] || 1000
35
+ @cache = ExpiringCache.new(capacity, expiration_seconds)
36
+ else
37
+ @cache = nil
38
+ end
39
+
40
+ @inited = Concurrent::AtomicBoolean.new(false)
41
+ end
42
+
43
+ def init(all_data)
44
+ @core.init_internal(all_data)
45
+ @inited.make_true
46
+
47
+ if !@cache.nil?
48
+ @cache.clear
49
+ all_data.each do |kind, items|
50
+ @cache[kind] = items_if_not_deleted(items)
51
+ items.each do |key, item|
52
+ @cache[item_cache_key(kind, key)] = [item]
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def get(kind, key)
59
+ if !@cache.nil?
60
+ cache_key = item_cache_key(kind, key)
61
+ cached = @cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
62
+ return item_if_not_deleted(cached[0]) if !cached.nil?
63
+ end
64
+
65
+ item = @core.get_internal(kind, key)
66
+
67
+ if !@cache.nil?
68
+ @cache[cache_key] = [item]
69
+ end
70
+
71
+ item_if_not_deleted(item)
72
+ end
73
+
74
+ def all(kind)
75
+ if !@cache.nil?
76
+ items = @cache[all_cache_key(kind)]
77
+ return items if !items.nil?
78
+ end
79
+
80
+ items = items_if_not_deleted(@core.get_all_internal(kind))
81
+ @cache[all_cache_key(kind)] = items if !@cache.nil?
82
+ items
83
+ end
84
+
85
+ def upsert(kind, item)
86
+ new_state = @core.upsert_internal(kind, item)
87
+
88
+ if !@cache.nil?
89
+ @cache[item_cache_key(kind, item[:key])] = [new_state]
90
+ @cache.delete(all_cache_key(kind))
91
+ end
92
+ end
93
+
94
+ def delete(kind, key, version)
95
+ upsert(kind, { key: key, version: version, deleted: true })
96
+ end
97
+
98
+ def initialized?
99
+ return true if @inited.value
100
+
101
+ if @cache.nil?
102
+ result = @core.initialized_internal?
103
+ else
104
+ result = @cache[inited_cache_key]
105
+ if result.nil?
106
+ result = @core.initialized_internal?
107
+ @cache[inited_cache_key] = result
108
+ end
109
+ end
110
+
111
+ @inited.make_true if result
112
+ result
113
+ end
114
+
115
+ def stop
116
+ @core.stop
117
+ end
118
+
119
+ private
120
+
121
+ # We use just one cache for 3 kinds of objects. Individual entities use a key like 'features:my-flag'.
122
+ def item_cache_key(kind, key)
123
+ kind[:namespace] + ":" + key.to_s
124
+ end
125
+
126
+ # The result of a call to get_all_internal is cached using the "kind" object as a key.
127
+ def all_cache_key(kind)
128
+ kind
129
+ end
130
+
131
+ # The result of initialized_internal? is cached using this key.
132
+ def inited_cache_key
133
+ "$inited"
134
+ end
135
+
136
+ def item_if_not_deleted(item)
137
+ (item.nil? || item[:deleted]) ? nil : item
138
+ end
139
+
140
+ def items_if_not_deleted(items)
141
+ items.select { |key, item| !item[:deleted] }
142
+ end
143
+ end
144
+
145
+ #
146
+ # This module describes the methods that you must implement on your own object in order to
147
+ # use {CachingStoreWrapper}.
148
+ #
149
+ module FeatureStoreCore
150
+ #
151
+ # Initializes the store. This is the same as {LaunchDarkly::Interfaces::FeatureStore#init},
152
+ # but the wrapper will take care of updating the cache if caching is enabled.
153
+ #
154
+ # If possible, the store should update the entire data set atomically. If that is not possible,
155
+ # it should iterate through the outer hash and then the inner hash using the existing iteration
156
+ # order of those hashes (the SDK will ensure that the items were inserted into the hashes in
157
+ # the correct order), storing each item, and then delete any leftover items at the very end.
158
+ #
159
+ # @param all_data [Hash] a hash where each key is one of the data kind objects, and each
160
+ # value is in turn a hash of string keys to entities
161
+ # @return [void]
162
+ #
163
+ def init_internal(all_data)
164
+ end
165
+
166
+ #
167
+ # Retrieves a single entity. This is the same as {LaunchDarkly::Interfaces::FeatureStore#get}
168
+ # except that 1. the wrapper will take care of filtering out deleted entities by checking the
169
+ # `:deleted` property, so you can just return exactly what was in the data store, and 2. the
170
+ # wrapper will take care of checking and updating the cache if caching is enabled.
171
+ #
172
+ # @param kind [Object] the kind of entity to get
173
+ # @param key [String] the unique key of the entity to get
174
+ # @return [Hash] the entity; nil if the key was not found
175
+ #
176
+ def get_internal(kind, key)
177
+ end
178
+
179
+ #
180
+ # Retrieves all entities of the specified kind. This is the same as {LaunchDarkly::Interfaces::FeatureStore#all}
181
+ # except that 1. the wrapper will take care of filtering out deleted entities by checking the
182
+ # `:deleted` property, so you can just return exactly what was in the data store, and 2. the
183
+ # wrapper will take care of checking and updating the cache if caching is enabled.
184
+ #
185
+ # @param kind [Object] the kind of entity to get
186
+ # @return [Hash] a hash where each key is the entity's `:key` property and each value
187
+ # is the entity
188
+ #
189
+ def get_all_internal(kind)
190
+ end
191
+
192
+ #
193
+ # Attempts to add or update an entity. This is the same as {LaunchDarkly::Interfaces::FeatureStore#upsert}
194
+ # except that 1. the wrapper will take care of updating the cache if caching is enabled, and 2.
195
+ # the method is expected to return the final state of the entity (i.e. either the `item`
196
+ # parameter if the update succeeded, or the previously existing entity in the store if the
197
+ # update failed; this is used for the caching logic).
198
+ #
199
+ # Note that FeatureStoreCore does not have a `delete` method. This is because {CachingStoreWrapper}
200
+ # implements `delete` by simply calling `upsert` with an item whose `:deleted` property is true.
201
+ #
202
+ # @param kind [Object] the kind of entity to add or update
203
+ # @param item [Hash] the entity to add or update
204
+ # @return [Hash] the entity as it now exists in the store after the update
205
+ #
206
+ def upsert_internal(kind, item)
207
+ end
208
+
209
+ #
210
+ # Checks whether this store has been initialized. This is the same as
211
+ # {LaunchDarkly::Interfaces::FeatureStore#initialized?} except that there is less of a concern
212
+ # for efficiency, because the wrapper will use caching and memoization in order to call the method
213
+ # as little as possible.
214
+ #
215
+ # @return [Boolean] true if the store is in an initialized state
216
+ #
217
+ def initialized_internal?
218
+ end
219
+
220
+ #
221
+ # Performs any necessary cleanup to shut down the store when the client is being shut down.
222
+ #
223
+ # @return [void]
224
+ #
225
+ def stop
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end