launchdarkly-server-sdk 8.13.0 → 8.14.0

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.
@@ -72,6 +72,25 @@ module LaunchDarkly
72
72
 
73
73
  change_set_builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
74
74
  envid = nil
75
+ # The FDv1 Fallback Directive is one-way and terminal: once any
76
+ # connect handshake within this sync invocation carries it, the SDK
77
+ # is committed to engaging FDv1 as soon as the next full payload
78
+ # has been applied. We therefore latch this flag to true on first
79
+ # observation and never reset it -- a mid-sync reconnect whose
80
+ # response no longer carries the directive does NOT cancel a
81
+ # directive seen earlier. This matches the Go and Python SDK
82
+ # implementations, both of which use the same latch pattern.
83
+ #
84
+ # The flag has to bridge two callbacks: on_connect sees the response
85
+ # headers but on_event does not. A local closed over by both blocks
86
+ # is correct because:
87
+ # 1. Scope -- bound to a single sync invocation, so a future sync
88
+ # starts fresh. (Within this invocation, persistence across
89
+ # reconnects is the intended semantics, per above.)
90
+ # 2. Thread safety -- ld-eventsource dispatches on_connect,
91
+ # on_event, and on_error on the same SSE worker thread, so
92
+ # reads and writes here are single-threaded by construction.
93
+ fdv1_fallback_pending = false
75
94
 
76
95
  base_uri = @http_config.base_uri + FDV2_STREAMING_ENDPOINT
77
96
  headers = Impl::Util.default_http_headers(@sdk_key, @config)
@@ -85,30 +104,32 @@ module LaunchDarkly
85
104
 
86
105
  @sse = SSE::Client.new(base_uri, **opts) do |client|
87
106
  client.on_connect do |headers|
88
- # Extract environment ID and check for fallback on successful connection
89
107
  if headers
90
- envid = headers[LD_ENVID_HEADER] || envid
91
-
92
- # Check for fallback header on connection
93
- if headers[LD_FD_FALLBACK_HEADER] == 'true'
94
- log_connection_result(true)
95
- yield LaunchDarkly::Interfaces::DataSystem::Update.new(
96
- state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
97
- revert_to_fdv1: true,
98
- environment_id: envid
99
- )
100
- stop
101
- end
108
+ # Per-environment identifier: server sends it on every connect,
109
+ # but it never changes once known so only assign once.
110
+ envid ||= LaunchDarkly::Impl::DataSystem.lookup_header(headers, LD_ENVID_HEADER)
111
+ fdv1_fallback_pending = true if LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(headers)
102
112
  end
103
113
  end
104
114
 
105
115
  client.on_event do |event|
106
116
  begin
107
- update = process_message(event, change_set_builder, envid)
108
- if update
109
- log_connection_result(true)
110
- @connection_attempt_start_time = 0
111
- yield update
117
+ update = process_message(event, change_set_builder, envid, fdv1_fallback_pending: fdv1_fallback_pending)
118
+ next unless update
119
+
120
+ log_connection_result(true)
121
+ @connection_attempt_start_time = 0
122
+
123
+ yield update
124
+
125
+ # When the FDv1 Fallback Directive rode along on a Valid update, close
126
+ # the stream so the primary synchronizer is stopped once the directive
127
+ # engages. process_message marks the Update with fallback_to_fdv1 only
128
+ # on payloads that complete a transfer, so the consumer has already
129
+ # applied the ChangeSet by the time we get here.
130
+ if update.fallback_to_fdv1
131
+ fdv1_fallback_pending = false
132
+ stop
112
133
  end
113
134
  rescue JSON::ParserError => e
114
135
  @logger.info { "[LDClient] Error parsing stream event; will restart stream: #{e}" }
@@ -147,13 +168,9 @@ module LaunchDarkly
147
168
  log_connection_result(false)
148
169
  fallback = false
149
170
 
150
- # Extract envid and fallback from error headers if available
151
171
  if error.respond_to?(:headers) && error.headers
152
- envid = error.headers[LD_ENVID_HEADER] || envid
153
-
154
- if error.headers[LD_FD_FALLBACK_HEADER] == 'true'
155
- fallback = true
156
- end
172
+ envid ||= LaunchDarkly::Impl::DataSystem.lookup_header(error.headers, LD_ENVID_HEADER)
173
+ fallback = true if LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(error.headers)
157
174
  end
158
175
 
159
176
  update = handle_error(error, envid, fallback)
@@ -193,9 +210,15 @@ module LaunchDarkly
193
210
  # @param message [SSE::StreamEvent]
194
211
  # @param change_set_builder [LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder]
195
212
  # @param envid [String, nil]
213
+ # @param fdv1_fallback_pending [Boolean] true when the connect-time
214
+ # response headers carried the FDv1 Fallback Directive. When set,
215
+ # the next Update that completes a payload transfer (TRANSFER_NONE
216
+ # or PAYLOAD_TRANSFERRED) is marked with fallback_to_fdv1: true so
217
+ # the consumer can engage the FDv1 Fallback Synchronizer after
218
+ # applying the in-flight ChangeSet.
196
219
  # @return [LaunchDarkly::Interfaces::DataSystem::Update, nil]
197
220
  #
198
- private def process_message(message, change_set_builder, envid)
221
+ private def process_message(message, change_set_builder, envid, fdv1_fallback_pending: false)
199
222
  event_type = message.type
200
223
 
201
224
  # Handle heartbeat
@@ -214,7 +237,8 @@ module LaunchDarkly
214
237
  change_set_builder.expect_changes
215
238
  return LaunchDarkly::Interfaces::DataSystem::Update.new(
216
239
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
217
- environment_id: envid
240
+ environment_id: envid,
241
+ fallback_to_fdv1: fdv1_fallback_pending
218
242
  )
219
243
  end
220
244
  nil
@@ -231,9 +255,7 @@ module LaunchDarkly
231
255
 
232
256
  when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE
233
257
  goodbye = LaunchDarkly::Impl::DataSystem::ProtocolV2::Goodbye.from_h(JSON.parse(message.data, symbolize_names: true))
234
- unless goodbye.silent
235
- @logger.error { "[LDClient] SSE server received error: #{goodbye.reason} (catastrophe: #{goodbye.catastrophe})" }
236
- end
258
+ @logger.info { "[LDClient] SSE server received goodbye: #{goodbye.reason}" }
237
259
  nil
238
260
 
239
261
  when LaunchDarkly::Interfaces::DataSystem::EventName::ERROR
@@ -251,7 +273,8 @@ module LaunchDarkly
251
273
  LaunchDarkly::Interfaces::DataSystem::Update.new(
252
274
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
253
275
  change_set: change_set,
254
- environment_id: envid
276
+ environment_id: envid,
277
+ fallback_to_fdv1: fdv1_fallback_pending
255
278
  )
256
279
 
257
280
  else
@@ -286,7 +309,7 @@ module LaunchDarkly
286
309
  update = LaunchDarkly::Interfaces::DataSystem::Update.new(
287
310
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
288
311
  error: error_info,
289
- revert_to_fdv1: true,
312
+ fallback_to_fdv1: true,
290
313
  environment_id: envid
291
314
  )
292
315
  stop
@@ -247,8 +247,9 @@ module LaunchDarkly
247
247
  # @return [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
248
248
  attr_reader :error
249
249
 
250
- # @return [Boolean] Whether to revert to FDv1
251
- attr_reader :revert_to_fdv1
250
+ # @return [Boolean] Whether the LaunchDarkly server has instructed the SDK to
251
+ # fall back to the FDv1 protocol.
252
+ attr_reader :fallback_to_fdv1
252
253
 
253
254
  # @return [String, nil] The environment ID if available
254
255
  attr_reader :environment_id
@@ -257,14 +258,14 @@ module LaunchDarkly
257
258
  # @param state [Symbol] The state of the data source
258
259
  # @param change_set [ChangeSet, nil] The change set if available
259
260
  # @param error [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
260
- # @param revert_to_fdv1 [Boolean] Whether to revert to FDv1
261
+ # @param fallback_to_fdv1 [Boolean] Whether to fall back to FDv1
261
262
  # @param environment_id [String, nil] The environment ID if available
262
263
  #
263
- def initialize(state:, change_set: nil, error: nil, revert_to_fdv1: false, environment_id: nil)
264
+ def initialize(state:, change_set: nil, error: nil, fallback_to_fdv1: false, environment_id: nil)
264
265
  @state = state
265
266
  @change_set = change_set
266
267
  @error = error
267
- @revert_to_fdv1 = revert_to_fdv1
268
+ @fallback_to_fdv1 = fallback_to_fdv1
268
269
  @environment_id = environment_id
269
270
  end
270
271
  end
@@ -71,31 +71,38 @@ module LaunchDarkly
71
71
  # Implementation of the Initializer.fetch method.
72
72
  #
73
73
  # Reads all configured files once and returns their contents as a Basis.
74
+ # File-based data sources never request the FDv1 Fallback Directive,
75
+ # so the returned {FetchResult} always reports `fallback_to_fdv1: false`.
74
76
  #
75
77
  # @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for file data)
76
- # @return [LaunchDarkly::Result] A Result containing either a Basis or an error message
78
+ # @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
77
79
  #
78
80
  def fetch(selector_store)
79
- @lock.synchronize do
80
- if @closed
81
- return LaunchDarkly::Result.fail('FileDataV2 source has been closed')
82
- end
81
+ result =
82
+ begin
83
+ @lock.synchronize do
84
+ if @closed
85
+ next LaunchDarkly::Result.fail('FileDataV2 source has been closed')
86
+ end
83
87
 
84
- result = load_all_to_changeset
85
- return result unless result.success?
88
+ load_result = load_all_to_changeset
89
+ next load_result unless load_result.success?
86
90
 
87
- change_set = result.value
88
- basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
89
- change_set: change_set,
90
- persist: false,
91
- environment_id: nil
92
- )
91
+ change_set = load_result.value
92
+ basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
93
+ change_set: change_set,
94
+ persist: false,
95
+ environment_id: nil
96
+ )
93
97
 
94
- LaunchDarkly::Result.success(basis)
95
- end
96
- rescue => e
97
- @logger.error { "[LDClient] Error fetching file data: #{e.message}" }
98
- LaunchDarkly::Result.fail("Error fetching file data: #{e.message}", e)
98
+ LaunchDarkly::Result.success(basis)
99
+ end
100
+ rescue => e
101
+ @logger.error { "[LDClient] Error fetching file data: #{e.message}" }
102
+ LaunchDarkly::Result.fail("Error fetching file data: #{e.message}", e)
103
+ end
104
+
105
+ LaunchDarkly::Interfaces::DataSystem::FetchResult.new(result: result, fallback_to_fdv1: false)
99
106
  end
100
107
 
101
108
  #
@@ -110,14 +117,14 @@ module LaunchDarkly
110
117
  #
111
118
  def sync(selector_store)
112
119
  # First yield initial data
113
- initial_result = fetch(selector_store)
114
- unless initial_result.success?
120
+ initial_fetch = fetch(selector_store)
121
+ unless initial_fetch.success?
115
122
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
116
123
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
117
124
  error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
118
125
  LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
119
126
  0,
120
- initial_result.error,
127
+ initial_fetch.error,
121
128
  Time.now
122
129
  )
123
130
  )
@@ -126,7 +133,7 @@ module LaunchDarkly
126
133
 
127
134
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
128
135
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
129
- change_set: initial_result.value.change_set
136
+ change_set: initial_fetch.value.change_set
130
137
  )
131
138
 
132
139
  # Start watching for file changes
@@ -93,6 +93,10 @@ module LaunchDarkly
93
93
  def stop
94
94
  @wrapper.stop
95
95
  end
96
+
97
+ def disable_cache
98
+ @wrapper.disable_cache
99
+ end
96
100
  end
97
101
 
98
102
  class RedisStoreImplBase
@@ -46,56 +46,61 @@ module LaunchDarkly
46
46
  # Implementation of the Initializer.fetch method.
47
47
  #
48
48
  # Returns the current test data as a Basis for initial data loading.
49
+ # Test data sources never request the FDv1 Fallback Directive, so the
50
+ # returned {FetchResult} always reports `fallback_to_fdv1: false`.
49
51
  #
50
52
  # @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for test data)
51
- # @return [LaunchDarkly::Result] A Result containing either a Basis or an error message
53
+ # @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
52
54
  #
53
55
  def fetch(selector_store)
54
- begin
55
- @lock.synchronize do
56
- if @closed
57
- return LaunchDarkly::Result.fail('TestDataV2 source has been closed')
58
- end
59
-
60
- # Get all current flags and segments from test data
61
- init_data = @test_data.make_init_data
62
- version = @test_data.get_version
63
-
64
- # Build a full transfer changeset
65
- builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
66
- builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
67
-
68
- # Add all flags to the changeset
69
- init_data[:flags].each do |key, flag_data|
70
- builder.add_put(
71
- LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
72
- key,
73
- flag_data[:version] || 1,
74
- flag_data
75
- )
76
- end
77
-
78
- # Add all segments to the changeset
79
- init_data[:segments].each do |key, segment_data|
80
- builder.add_put(
81
- LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
82
- key,
83
- segment_data[:version] || 1,
84
- segment_data
85
- )
56
+ result =
57
+ begin
58
+ @lock.synchronize do
59
+ if @closed
60
+ next LaunchDarkly::Result.fail('TestDataV2 source has been closed')
61
+ end
62
+
63
+ # Get all current flags and segments from test data
64
+ init_data = @test_data.make_init_data
65
+ version = @test_data.get_version
66
+
67
+ # Build a full transfer changeset
68
+ builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
69
+ builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
70
+
71
+ # Add all flags to the changeset
72
+ init_data[:flags].each do |key, flag_data|
73
+ builder.add_put(
74
+ LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
75
+ key,
76
+ flag_data[:version] || 1,
77
+ flag_data
78
+ )
79
+ end
80
+
81
+ # Add all segments to the changeset
82
+ init_data[:segments].each do |key, segment_data|
83
+ builder.add_put(
84
+ LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
85
+ key,
86
+ segment_data[:version] || 1,
87
+ segment_data
88
+ )
89
+ end
90
+
91
+ # Create selector for this version
92
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.new_selector(version.to_s, version)
93
+ change_set = builder.finish(selector)
94
+
95
+ basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(change_set: change_set, persist: false, environment_id: nil)
96
+
97
+ LaunchDarkly::Result.success(basis)
86
98
  end
87
-
88
- # Create selector for this version
89
- selector = LaunchDarkly::Interfaces::DataSystem::Selector.new_selector(version.to_s, version)
90
- change_set = builder.finish(selector)
91
-
92
- basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(change_set: change_set, persist: false, environment_id: nil)
93
-
94
- LaunchDarkly::Result.success(basis)
99
+ rescue => e
100
+ LaunchDarkly::Result.fail("Error fetching test data: #{e.message}", e)
95
101
  end
96
- rescue => e
97
- LaunchDarkly::Result.fail("Error fetching test data: #{e.message}", e)
98
- end
102
+
103
+ LaunchDarkly::Interfaces::DataSystem::FetchResult.new(result: result, fallback_to_fdv1: false)
99
104
  end
100
105
 
101
106
  #
@@ -109,14 +114,14 @@ module LaunchDarkly
109
114
  #
110
115
  def sync(selector_store)
111
116
  # First yield initial data
112
- initial_result = fetch(selector_store)
113
- unless initial_result.success?
117
+ initial_fetch = fetch(selector_store)
118
+ unless initial_fetch.success?
114
119
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
115
120
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
116
121
  error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
117
122
  LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR,
118
123
  0,
119
- initial_result.error,
124
+ initial_fetch.error,
120
125
  Time.now
121
126
  )
122
127
  )
@@ -126,7 +131,7 @@ module LaunchDarkly
126
131
  # Yield the initial successful state
127
132
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
128
133
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
129
- change_set: initial_result.value.change_set
134
+ change_set: initial_fetch.value.change_set
130
135
  )
131
136
 
132
137
  # Continue yielding updates as they arrive
@@ -32,8 +32,12 @@ module LaunchDarkly
32
32
  # @option opts [String] :url shortcut for setting the `url` property of the Consul client configuration
33
33
  # @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
34
34
  # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
35
- # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
36
- # @option opts [Integer] :capacity (1000) maximum number of items in the cache
35
+ # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
36
+ # When the SDK is configured to use FDv2, the persistent-store cache is automatically
37
+ # dropped once the in-memory store has been initialized, so this setting only affects
38
+ # the brief bootstrap window before the first set of flag data has been received.
39
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache.
40
+ # Same FDv2 caveat as `:expiration` applies.
37
41
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
38
42
  #
39
43
  def self.new_feature_store(opts = {})
@@ -42,8 +42,12 @@ module LaunchDarkly
42
42
  # @option opts [Object] :existing_client an already-constructed DynamoDB client for the feature store to use
43
43
  # @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
44
44
  # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
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
45
+ # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
46
+ # When the SDK is configured to use FDv2, the persistent-store cache is automatically
47
+ # dropped once the in-memory store has been initialized, so this setting only affects
48
+ # the brief bootstrap window before the first set of flag data has been received.
49
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache.
50
+ # Same FDv2 caveat as `:expiration` applies.
47
51
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
48
52
  #
49
53
  def self.new_feature_store(table_name, opts = {})
@@ -108,9 +108,6 @@ module LaunchDarkly
108
108
  #
109
109
  # Returns a builder for the FDv2-compatible file data source.
110
110
  #
111
- # This method is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
112
- # It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
113
- #
114
111
  # This method returns a builder proc that can be used with the FDv2 data system
115
112
  # configuration as both an Initializer and a Synchronizer. When used as an Initializer
116
113
  # (via `fetch`), it reads files once. When used as a Synchronizer (via `sync`), it
@@ -50,8 +50,12 @@ module LaunchDarkly
50
50
  # @option opts [String] :prefix (default_prefix) namespace prefix to add to all hash keys used by LaunchDarkly
51
51
  # @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
52
52
  # @option opts [Integer] :max_connections size of the Redis connection pool
53
- # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
54
- # @option opts [Integer] :capacity (1000) maximum number of items in the cache
53
+ # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
54
+ # When the SDK is configured to use FDv2, the persistent-store cache is automatically
55
+ # dropped once the in-memory store has been initialized, so this setting only affects
56
+ # the brief bootstrap window before the first set of flag data has been received.
57
+ # @option opts [Integer] :capacity (1000) maximum number of items in the cache.
58
+ # Same FDv2 caveat as `:expiration` applies.
55
59
  # @option opts [Object] :pool custom connection pool, if desired
56
60
  # @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool;
57
61
  # this is true by default, and should be set to false only if you are managing the pool yourself and want its
@@ -9,9 +9,6 @@ module LaunchDarkly
9
9
  # A mechanism for providing dynamically updatable feature flag state in a
10
10
  # simplified form to an SDK client in test scenarios using the FDv2 protocol.
11
11
  #
12
- # This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
13
- # It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
14
- #
15
12
  # Unlike {LaunchDarkly::Integrations::FileData}, this mechanism does not use any external resources. It
16
13
  # provides only the data that the application has put into it using the {#update} method.
17
14
  #
@@ -61,50 +61,52 @@ module LaunchDarkly
61
61
  @core.init_internal(all_data)
62
62
  @inited.make_true
63
63
 
64
- unless @cache.nil?
65
- @cache.clear
64
+ cache = @cache
65
+ unless cache.nil?
66
+ cache.clear
66
67
  all_data.each do |kind, items|
67
- @cache[kind] = items_if_not_deleted(items)
68
+ cache[kind] = items_if_not_deleted(items)
68
69
  items.each do |key, item|
69
- @cache[item_cache_key(kind, key)] = [item]
70
+ cache[item_cache_key(kind, key)] = [item]
70
71
  end
71
72
  end
72
73
  end
73
74
  end
74
75
 
75
76
  def get(kind, key)
76
- unless @cache.nil?
77
- cache_key = item_cache_key(kind, key)
78
- cached = @cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
77
+ cache = @cache
78
+ cache_key = item_cache_key(kind, key)
79
+ unless cache.nil?
80
+ cached = cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
79
81
  return item_if_not_deleted(cached[0]) unless cached.nil?
80
82
  end
81
83
 
82
84
  item = @core.get_internal(kind, key)
83
85
 
84
- unless @cache.nil?
85
- @cache[cache_key] = [item]
86
- end
86
+ cache[cache_key] = [item] unless cache.nil?
87
87
 
88
88
  item_if_not_deleted(item)
89
89
  end
90
90
 
91
91
  def all(kind)
92
- unless @cache.nil?
93
- items = @cache[all_cache_key(kind)]
92
+ cache = @cache
93
+ unless cache.nil?
94
+ items = cache[all_cache_key(kind)]
94
95
  return items unless items.nil?
95
96
  end
96
97
 
97
98
  items = items_if_not_deleted(@core.get_all_internal(kind))
98
- @cache[all_cache_key(kind)] = items unless @cache.nil?
99
+ cache[all_cache_key(kind)] = items unless cache.nil?
99
100
  items
100
101
  end
101
102
 
102
103
  def upsert(kind, item)
103
104
  new_state = @core.upsert_internal(kind, item)
104
105
 
105
- unless @cache.nil?
106
- @cache[item_cache_key(kind, item[:key])] = [new_state]
107
- @cache.delete(all_cache_key(kind))
106
+ cache = @cache
107
+ unless cache.nil?
108
+ cache[item_cache_key(kind, item[:key])] = [new_state]
109
+ cache.delete(all_cache_key(kind))
108
110
  end
109
111
  end
110
112
 
@@ -115,13 +117,14 @@ module LaunchDarkly
115
117
  def initialized?
116
118
  return true if @inited.value
117
119
 
118
- if @cache.nil?
120
+ cache = @cache
121
+ if cache.nil?
119
122
  result = @core.initialized_internal?
120
123
  else
121
- result = @cache[inited_cache_key]
124
+ result = cache[inited_cache_key]
122
125
  if result.nil?
123
126
  result = @core.initialized_internal?
124
- @cache[inited_cache_key] = result
127
+ cache[inited_cache_key] = result
125
128
  end
126
129
  end
127
130
 
@@ -129,6 +132,23 @@ module LaunchDarkly
129
132
  result
130
133
  end
131
134
 
135
+ #
136
+ # Disable the in-memory cache. Releases the cache reference so subsequent operations
137
+ # bypass it and go directly to the underlying core. Safe to call multiple times.
138
+ #
139
+ # Called by the FDv2 store coordinator once the in-memory store has become the
140
+ # source of truth and the persistent-store cache is no longer useful. Internal --
141
+ # not part of the public API.
142
+ #
143
+ # @return [void]
144
+ #
145
+ def disable_cache
146
+ cache = @cache
147
+ return if cache.nil?
148
+ @cache = nil
149
+ cache.clear
150
+ end
151
+
132
152
  def stop
133
153
  @core.stop
134
154
  end