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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. 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