launchdarkly-server-sdk 5.5.7
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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +134 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +15 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +600 -0
- data/.simplecov +4 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +261 -0
- data/CODEOWNERS +1 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +13 -0
- data/README.md +56 -0
- data/Rakefile +5 -0
- data/azure-pipelines.yml +51 -0
- data/ext/mkrf_conf.rb +11 -0
- data/launchdarkly-server-sdk.gemspec +40 -0
- data/lib/ldclient-rb.rb +29 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +411 -0
- data/lib/ldclient-rb/evaluation.rb +455 -0
- data/lib/ldclient-rb/event_summarizer.rb +55 -0
- data/lib/ldclient-rb/events.rb +468 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/file_data_source.rb +312 -0
- data/lib/ldclient-rb/flags_state.rb +76 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +424 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/newrelic.rb +17 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +78 -0
- data/lib/ldclient-rb/redis_store.rb +87 -0
- data/lib/ldclient-rb/requestor.rb +101 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +141 -0
- data/lib/ldclient-rb/user_filter.rb +51 -0
- data/lib/ldclient-rb/util.rb +50 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/scripts/gendocs.sh +11 -0
- data/scripts/release.sh +27 -0
- data/spec/config_spec.rb +63 -0
- data/spec/evaluation_spec.rb +739 -0
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +642 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/feature_store_spec_base.rb +213 -0
- data/spec/file_data_source_spec.rb +255 -0
- data/spec/fixtures/feature.json +37 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/flags_state_spec.rb +81 -0
- data/spec/http_util.rb +109 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/integrations/consul_feature_store_spec.rb +42 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +471 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/polling_spec.rb +120 -0
- data/spec/redis_feature_store_spec.rb +95 -0
- data/spec/requestor_spec.rb +214 -0
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +60 -0
- data/spec/user_filter_spec.rb +91 -0
- data/spec/util_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- metadata +375 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
require "ldclient-rb/interfaces"
|
2
|
+
require "ldclient-rb/impl/store_data_set_sorter"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
#
|
7
|
+
# Provides additional behavior that the client requires before or after feature store operations.
|
8
|
+
# Currently this just means sorting the data set for init(). In the future we may also use this
|
9
|
+
# to provide an update listener capability.
|
10
|
+
#
|
11
|
+
class FeatureStoreClientWrapper
|
12
|
+
include Interfaces::FeatureStore
|
13
|
+
|
14
|
+
def initialize(store)
|
15
|
+
@store = store
|
16
|
+
end
|
17
|
+
|
18
|
+
def init(all_data)
|
19
|
+
@store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data))
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(kind, key)
|
23
|
+
@store.get(kind, key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def all(kind)
|
27
|
+
@store.all(kind)
|
28
|
+
end
|
29
|
+
|
30
|
+
def upsert(kind, item)
|
31
|
+
@store.upsert(kind, item)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(kind, key, version)
|
35
|
+
@store.delete(kind, key, version)
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialized?
|
39
|
+
@store.initialized?
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop
|
43
|
+
@store.stop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
module Impl
|
4
|
+
#
|
5
|
+
# Implements a dependency graph ordering for data to be stored in a feature store. We must use this
|
6
|
+
# on every data set that will be passed to the feature store's init() method.
|
7
|
+
#
|
8
|
+
class FeatureStoreDataSetSorter
|
9
|
+
#
|
10
|
+
# Returns a copy of the input hash that has the following guarantees: the iteration order of the outer
|
11
|
+
# hash will be in ascending order by the VersionDataKind's :priority property (if any), and for each
|
12
|
+
# data kind that has a :get_dependency_keys function, the inner hash will have an iteration order
|
13
|
+
# where B is before A if A has a dependency on B.
|
14
|
+
#
|
15
|
+
# This implementation relies on the fact that hashes in Ruby have an iteration order that is the same
|
16
|
+
# as the insertion order. Also, due to the way we deserialize JSON received from LaunchDarkly, the
|
17
|
+
# keys in the inner hash will always be symbols.
|
18
|
+
#
|
19
|
+
def self.sort_all_collections(all_data)
|
20
|
+
outer_hash = {}
|
21
|
+
kinds = all_data.keys.sort_by { |k|
|
22
|
+
k[:priority].nil? ? k[:namespace].length : k[:priority] # arbitrary order if priority is unknown
|
23
|
+
}
|
24
|
+
kinds.each do |kind|
|
25
|
+
items = all_data[kind]
|
26
|
+
outer_hash[kind] = self.sort_collection(kind, items)
|
27
|
+
end
|
28
|
+
outer_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.sort_collection(kind, input)
|
32
|
+
dependency_fn = kind[:get_dependency_keys]
|
33
|
+
return input if dependency_fn.nil? || input.empty?
|
34
|
+
remaining_items = input.clone
|
35
|
+
items_out = {}
|
36
|
+
while !remaining_items.empty?
|
37
|
+
# pick a random item that hasn't been updated yet
|
38
|
+
key, item = remaining_items.first
|
39
|
+
self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out)
|
40
|
+
end
|
41
|
+
items_out
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out)
|
45
|
+
item_key = item[:key].to_sym
|
46
|
+
remaining_items.delete(item_key) # we won't need to visit this item again
|
47
|
+
dependency_fn.call(item).each do |dep_key|
|
48
|
+
dep_item = remaining_items[dep_key.to_sym]
|
49
|
+
self.add_with_dependencies_first(dep_item, dependency_fn, remaining_items, items_out) if !dep_item.nil?
|
50
|
+
end
|
51
|
+
items_out[item_key] = item
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
|
5
|
+
# These constants denote the types of data that can be stored in the feature store. If
|
6
|
+
# we add another storable data type in the future, as long as it follows the same pattern
|
7
|
+
# (having "key", "version", and "deleted" properties), we only need to add a corresponding
|
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
|
14
|
+
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] } }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# @private
|
21
|
+
SEGMENTS = {
|
22
|
+
namespace: "segments",
|
23
|
+
priority: 0
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
#
|
27
|
+
# Default implementation of the LaunchDarkly client's feature store, using an in-memory
|
28
|
+
# cache. This object holds feature flags and related data received from LaunchDarkly.
|
29
|
+
# Database-backed implementations are available in {LaunchDarkly::Integrations}.
|
30
|
+
#
|
31
|
+
class InMemoryFeatureStore
|
32
|
+
include LaunchDarkly::Interfaces::FeatureStore
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@items = Hash.new
|
36
|
+
@lock = Concurrent::ReadWriteLock.new
|
37
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(kind, key)
|
41
|
+
@lock.with_read_lock do
|
42
|
+
coll = @items[kind]
|
43
|
+
f = coll.nil? ? nil : coll[key.to_sym]
|
44
|
+
(f.nil? || f[:deleted]) ? nil : f
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def all(kind)
|
49
|
+
@lock.with_read_lock do
|
50
|
+
coll = @items[kind]
|
51
|
+
(coll.nil? ? Hash.new : coll).select { |_k, f| not f[:deleted] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(kind, key, version)
|
56
|
+
@lock.with_write_lock do
|
57
|
+
coll = @items[kind]
|
58
|
+
if coll.nil?
|
59
|
+
coll = Hash.new
|
60
|
+
@items[kind] = coll
|
61
|
+
end
|
62
|
+
old = coll[key.to_sym]
|
63
|
+
|
64
|
+
if old.nil? || old[:version] < version
|
65
|
+
coll[key.to_sym] = { deleted: true, version: version }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def init(all_data)
|
71
|
+
@lock.with_write_lock do
|
72
|
+
@items.replace(all_data)
|
73
|
+
@initialized.make_true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def upsert(kind, item)
|
78
|
+
@lock.with_write_lock do
|
79
|
+
coll = @items[kind]
|
80
|
+
if coll.nil?
|
81
|
+
coll = Hash.new
|
82
|
+
@items[kind] = coll
|
83
|
+
end
|
84
|
+
old = coll[item[:key].to_sym]
|
85
|
+
|
86
|
+
if old.nil? || old[:version] < item[:version]
|
87
|
+
coll[item[:key].to_sym] = item
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def initialized?
|
93
|
+
@initialized.value
|
94
|
+
end
|
95
|
+
|
96
|
+
def stop
|
97
|
+
# nothing to do
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -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 (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 (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 (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 (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
|