launchdarkly-server-sdk 5.5.7

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