Skip to content

Commit

Permalink
FFM-12192 Use client Singleton / Lock eval & target map clone operati…
Browse files Browse the repository at this point in the history
…on (#48)
  • Loading branch information
erdirowlands authored Nov 12, 2024
1 parent d0750c8 commit a1a708a
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 75 deletions.
55 changes: 11 additions & 44 deletions lib/ff/ruby/server/sdk/api/cf_client.rb
Original file line number Diff line number Diff line change
@@ -1,56 +1,23 @@

require 'singleton'
require_relative "../../generated/lib/openapi_client"
require_relative "../common/closeable"
require_relative "inner_client"

class CfClient < Closeable

# Static:
class << self

@@instance = CfClient.new

def instance

@@instance
include Singleton

def init(api_key, config, connector = nil)
# Only initialize if @client is nil to avoid reinitialization
unless @client
@config = config || ConfigBuilder.new.build
@client = InnerClient.new(api_key, @config, connector)
@config.logger.debug "Client initialized with API key: #{api_key}"
end
@client
end

# Static - End

def initialize(api_key = nil, config = nil, connector = nil)

if config == nil

@config = ConfigBuilder.new.build
else

@config = config
end

@client = InnerClient.new(api_key, config, connector)

@config.logger.debug "Client (1): " + @client.to_s
end

def init(api_key = nil, config = nil, connector = nil)

if @client == nil

@config = config

@client = InnerClient.new(

api_key = api_key,
config = config,
connector = connector
)

@config.logger.debug "Client (2): " + @client.to_s
end
end

def wait_for_initialization(timeout_ms: nil)
def wait_for_initialization(timeout_ms: nil)
if @client != nil
@client.wait_for_initialization(timeout: timeout_ms)
end
Expand Down
28 changes: 18 additions & 10 deletions lib/ff/ruby/server/sdk/api/metrics_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def init(connector, config, callback)

@executor = Concurrent::FixedThreadPool.new(10)

# Used for locking the evalution and target metrics maps before we clone them
@metric_maps_mutex = Mutex.new
@evaluation_metrics = FrequencyMap.new
@target_metrics = Concurrent::Map.new

Expand Down Expand Up @@ -181,21 +183,27 @@ def run_one_iteration
end

def send_data_and_reset_cache(evaluation_metrics_map, target_metrics_map)
# Clone and clear evaluation metrics map

evaluation_metrics_map_clone = Concurrent::Map.new
target_metrics_map_clone = Concurrent::Map.new

evaluation_metrics_map.each_pair do |key, value|
evaluation_metrics_map_clone[key] = value
end
# A single lock is used to synchronise access to both the evaluation and target metrics maps.
# While separate locks could be applied to each map individually, we want an interval's eval/target
# metrics to be processed in an atomic unit.
@metric_maps_mutex.synchronize do
# Clone and clear evaluation metrics map
evaluation_metrics_map.each_pair do |key, value|
evaluation_metrics_map_clone[key] = value
end

evaluation_metrics_map.clear
target_metrics_map_clone = Concurrent::Map.new
evaluation_metrics_map.clear

target_metrics_map.each_pair do |key, value|
target_metrics_map_clone[key] = value
end
target_metrics_map.each_pair do |key, value|
target_metrics_map_clone[key] = value
end

target_metrics_map.clear
target_metrics_map.clear
end

@evaluation_warning_issued.make_false
@target_warning_issued.make_false
Expand Down
81 changes: 60 additions & 21 deletions test/ff/ruby/server/sdk/sdk_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,55 @@ def test_version_number
end

def test_client_singleton_inst

instance = CfClient.instance
(0..@counter).each {
(0..@counter).each do
compare_equal = CfClient.instance
assert_equal(instance, compare_equal)
end
end

def test_client_singleton_init
instance = CfClient.instance
(0..@counter).each do
compare_equal = CfClient.instance
compare_not_equal = CfClient.new("test")
assert_equal(instance, compare_equal)
end
end

refute_nil compare_equal
refute_nil compare_not_equal
def test_client_initialization
config = ConfigBuilder.new.build
connector = HarnessConnector.new(@string, config, nil)
api_key = "test_api_key"

assert_equal(instance, compare_equal)
assert(instance != compare_not_equal)
}
client = CfClient.instance
client.init(api_key, config, connector)

# Verify that @client is initialized and is an instance of InnerClient
inner_client = client.instance_variable_get(:@client)
refute_nil(inner_client, "InnerClient should be initialized")
assert_instance_of(InnerClient, inner_client, "InnerClient should be an instance of InnerClient")
end

def test_client_constructor_inst
def test_client_reinitialization
config1 = ConfigBuilder.new.build
connector1 = HarnessConnector.new(@string, config1, nil)
api_key1 = "api_key_1"

test_string = "test"
config = ConfigBuilder.new.build
connector = HarnessConnector.new(test_string, config, nil)
client = CfClient.instance
client.init(api_key1, config1, connector1)

# Capture the initial @client instance
initial_inner_client = client.instance_variable_get(:@client)

instance_with_no_config = CfClient.new(test_string)
instance_with_config = CfClient.new(test_string, config)
instance_with_connector = CfClient.new(test_string, config, connector)
# Attempt to reinitialize with different parameters
config2 = ConfigBuilder.new.build
connector2 = HarnessConnector.new(@string, config2, nil)
api_key2 = "api_key_2"

refute_nil instance_with_config
refute_nil instance_with_connector
refute_nil instance_with_no_config
client.init(api_key2, config2, connector2)

assert(instance_with_config != instance_with_no_config)
assert(instance_with_connector != instance_with_no_config)
assert(instance_with_connector != instance_with_config)
# Verify that the @client instance remains the same
assert_same(initial_inner_client, client.instance_variable_get(:@client))
end

def test_config_constructor_inst
Expand All @@ -76,6 +93,28 @@ def test_config_constructor_inst
assert(config != config_not_equal)
end

def test_client_methods
config = ConfigBuilder.new.build
connector = HarnessConnector.new(@string, config, nil)
api_key = "test_api_key"

client = CfClient.instance
client.init(api_key, config, connector)

inner_client = client.instance_variable_get(:@client)
inner_client.stub :bool_variation, true do
assert_equal(true, client.bool_variation("identifier", "target", false))
end

inner_client.stub :string_variation, "variation" do
assert_equal("variation", client.string_variation("identifier", "target", "default"))
end

inner_client.stub :number_variation, 42 do
assert_equal(42, client.number_variation("identifier", "target", 0))
end
end

def test_config_properties

config = Config.new
Expand Down

0 comments on commit a1a708a

Please sign in to comment.