Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ def initialized?
@store.initialized?
end

#
# Passthrough for the FDv2 store coordinator. If the wrapped store supports
# disabling its cache (e.g. {LaunchDarkly::Integrations::Util::CachingStoreWrapper}),
# forward the call; otherwise no-op.
#
# @return [void]
#
def disable_cache
return unless @store.respond_to?(:disable_cache)
wrapper { @store.disable_cache }
end

# (see LaunchDarkly::Interfaces::FeatureStore#stop)
def stop
poller_to_stop = nil
Expand Down
24 changes: 24 additions & 0 deletions lib/ldclient-rb/impl/data_store/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ def get_data_store_status_provider
# Switch to memory store as active
@active_store = @memory_store

# In-memory store is now authoritative. Disable the persistent-store cache
# so we don't hold a duplicate copy of every flag. Done before the
# persistent_store.init below so the wrapper can skip its cache-population
# loop on this very call.
disable_persistent_cache

# Persist to persistent store if configured and writable
@persistent_store.init(collections) if should_persist?

Expand Down Expand Up @@ -271,6 +277,24 @@ def get_data_store_status_provider
send_change_events(affected_items) unless affected_items.empty?
end

#
# Disable the persistent store's in-memory cache, if it has one. The FDv2 in-memory
# store is now the source of truth, so the cache is dead weight and would just
# hold a duplicate copy of every flag.
#
# @return [void]
#
private def disable_persistent_cache
return if @persistent_store.nil?
return unless @persistent_store.respond_to?(:disable_cache)

begin
@persistent_store.disable_cache
rescue => e
@logger.warn { "[LDClient] Failed to disable persistent store cache: #{e.message}" }
end
end

#
# Returns whether data should be persisted to the persistent store.
#
Expand Down
4 changes: 4 additions & 0 deletions lib/ldclient-rb/impl/integrations/redis_impl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def initialized?
def stop
@wrapper.stop
end

def disable_cache
@wrapper.disable_cache
end
end

class RedisStoreImplBase
Expand Down
8 changes: 6 additions & 2 deletions lib/ldclient-rb/integrations/consul.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ def self.default_prefix
# @option opts [String] :url shortcut for setting the `url` property of the Consul client configuration
# @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
# @option opts [Integer] :capacity (1000) maximum number of items in the cache
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
# When the SDK is configured to use FDv2, the persistent-store cache is automatically
# dropped once the in-memory store has been initialized, so this setting only affects
# the brief bootstrap window before the first set of flag data has been received.
# @option opts [Integer] :capacity (1000) maximum number of items in the cache.
# Same FDv2 caveat as `:expiration` applies.
# @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
#
def self.new_feature_store(opts = {})
Expand Down
8 changes: 6 additions & 2 deletions lib/ldclient-rb/integrations/dynamodb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ module DynamoDB
# @option opts [Object] :existing_client an already-constructed DynamoDB client for the feature store to use
# @option opts [String] :prefix namespace prefix to add to all keys used by LaunchDarkly
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
# @option opts [Integer] :capacity (1000) maximum number of items in the cache
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
# When the SDK is configured to use FDv2, the persistent-store cache is automatically
# dropped once the in-memory store has been initialized, so this setting only affects
# the brief bootstrap window before the first set of flag data has been received.
# @option opts [Integer] :capacity (1000) maximum number of items in the cache.
# Same FDv2 caveat as `:expiration` applies.
# @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
#
def self.new_feature_store(table_name, opts = {})
Expand Down
8 changes: 6 additions & 2 deletions lib/ldclient-rb/integrations/redis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ def self.default_prefix
# @option opts [String] :prefix (default_prefix) namespace prefix to add to all hash keys used by LaunchDarkly
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
# @option opts [Integer] :max_connections size of the Redis connection pool
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
# @option opts [Integer] :capacity (1000) maximum number of items in the cache
# @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching.
# When the SDK is configured to use FDv2, the persistent-store cache is automatically
# dropped once the in-memory store has been initialized, so this setting only affects
# the brief bootstrap window before the first set of flag data has been received.
# @option opts [Integer] :capacity (1000) maximum number of items in the cache.
# Same FDv2 caveat as `:expiration` applies.
# @option opts [Object] :pool custom connection pool, if desired
# @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool;
# this is true by default, and should be set to false only if you are managing the pool yourself and want its
Expand Down
58 changes: 39 additions & 19 deletions lib/ldclient-rb/integrations/util/store_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,50 +61,52 @@ def init(all_data)
@core.init_internal(all_data)
@inited.make_true

unless @cache.nil?
@cache.clear
cache = @cache
unless cache.nil?
cache.clear
all_data.each do |kind, items|
@cache[kind] = items_if_not_deleted(items)
cache[kind] = items_if_not_deleted(items)
items.each do |key, item|
@cache[item_cache_key(kind, key)] = [item]
cache[item_cache_key(kind, key)] = [item]
end
end
end
end

def get(kind, key)
unless @cache.nil?
cache_key = item_cache_key(kind, key)
cached = @cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
cache = @cache
cache_key = item_cache_key(kind, key)
unless cache.nil?
cached = cache[cache_key] # note, item entries in the cache are wrapped in an array so we can cache nil values
return item_if_not_deleted(cached[0]) unless cached.nil?
end

item = @core.get_internal(kind, key)

unless @cache.nil?
@cache[cache_key] = [item]
end
cache[cache_key] = [item] unless cache.nil?

item_if_not_deleted(item)
end

def all(kind)
unless @cache.nil?
items = @cache[all_cache_key(kind)]
cache = @cache
unless cache.nil?
items = cache[all_cache_key(kind)]
return items unless items.nil?
end

items = items_if_not_deleted(@core.get_all_internal(kind))
@cache[all_cache_key(kind)] = items unless @cache.nil?
cache[all_cache_key(kind)] = items unless cache.nil?
items
end

def upsert(kind, item)
new_state = @core.upsert_internal(kind, item)

unless @cache.nil?
@cache[item_cache_key(kind, item[:key])] = [new_state]
@cache.delete(all_cache_key(kind))
cache = @cache
unless cache.nil?
cache[item_cache_key(kind, item[:key])] = [new_state]
cache.delete(all_cache_key(kind))
end
end

Expand All @@ -115,20 +117,38 @@ def delete(kind, key, version)
def initialized?
return true if @inited.value

if @cache.nil?
cache = @cache
if cache.nil?
result = @core.initialized_internal?
else
result = @cache[inited_cache_key]
result = cache[inited_cache_key]
if result.nil?
result = @core.initialized_internal?
@cache[inited_cache_key] = result
cache[inited_cache_key] = result
end
end

@inited.make_true if result
result
end

#
# Disable the in-memory cache. Releases the cache reference so subsequent operations
# bypass it and go directly to the underlying core. Safe to call multiple times.
#
# Called by the FDv2 store coordinator once the in-memory store has become the
# source of truth and the persistent-store cache is no longer useful. Internal --
# not part of the public API.
#
# @return [void]
#
def disable_cache
cache = @cache
return if cache.nil?
@cache = nil
cache.clear
end

def stop
@core.stop
end
Expand Down
40 changes: 40 additions & 0 deletions spec/impl/data_store/feature_store_client_wrapper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "spec_helper"
require "ldclient-rb/impl/data_store/feature_store_client_wrapper"

module LaunchDarkly
module Impl
module DataStore
describe FeatureStoreClientWrapperV2 do
let(:logger) { double.as_null_object }
let(:status_sink) { double(update_status: nil) }

describe "#disable_cache" do
it "forwards to the underlying store when supported" do
inner = double("store", disable_cache: nil, init: nil, get: nil, all: nil, delete: nil, upsert: nil, initialized?: false, stop: nil)
expect(inner).to receive(:disable_cache).once

wrapper = described_class.new(inner, status_sink, logger)
wrapper.disable_cache
end

it "is a no-op when the underlying store does not respond to disable_cache" do
inner = Class.new do
def init(_); end
def get(_, _); end
def all(_); end
def delete(_, _, _); end
def upsert(_, _); end
def initialized?; false; end
def stop; end
end.new

wrapper = described_class.new(inner, status_sink, logger)
expect { wrapper.disable_cache }.not_to raise_error
end
end
end
end
end
end
Loading
Loading