launchdarkly-server-sdk 5.5.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|