Skip to content

Commit f6b7ad1

Browse files
update: bugbash fixes. (#377)
1. cacheSize, cacheTtl, and customCache are exposed to public 2. logs and reasons added in DefaultCmabService 3. Default_max_retry set to 1
1 parent 0abc6f0 commit f6b7ad1

File tree

6 files changed

+130
-35
lines changed

6 files changed

+130
-35
lines changed

lib/optimizely.rb

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ module Optimizely
5050
class Project
5151
include Optimizely::Decide
5252

53-
# CMAB Constants
54-
DEFAULT_CMAB_CACHE_TIMEOUT = (30 * 60 * 1000)
55-
DEFAULT_CMAB_CACHE_SIZE = 1000
56-
5753
attr_reader :notification_center
5854
# @api no-doc
5955
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
@@ -90,7 +86,8 @@ def initialize(
9086
event_processor: nil,
9187
default_decide_options: [],
9288
event_processor_options: {},
93-
settings: nil
89+
settings: nil,
90+
cmab_service: nil
9491
)
9592
@logger = logger || NoOpLogger.new
9693
@error_handler = error_handler || NoOpErrorHandler.new
@@ -137,18 +134,22 @@ def initialize(
137134

138135
setup_odp!(@config_manager.sdk_key)
139136

140-
# Initialize CMAB components
141-
@cmab_client = DefaultCmabClient.new(
142-
nil,
143-
CmabRetryConfig.new,
144-
@logger
145-
)
146-
@cmab_cache = LRUCache.new(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT)
147-
@cmab_service = DefaultCmabService.new(
148-
@cmab_cache,
149-
@cmab_client,
150-
@logger
151-
)
137+
# Initialize CMAB components if cmab service is nil
138+
if cmab_service.nil?
139+
@cmab_client = DefaultCmabClient.new(
140+
nil,
141+
CmabRetryConfig.new,
142+
@logger
143+
)
144+
@cmab_cache = LRUCache.new(Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE, Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT)
145+
@cmab_service = DefaultCmabService.new(
146+
@cmab_cache,
147+
@cmab_client,
148+
@logger
149+
)
150+
else
151+
@cmab_service = cmab_service
152+
end
152153

153154
@decision_service = DecisionService.new(@logger, @cmab_service, @user_profile_service)
154155

lib/optimizely/cmab/cmab_client.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
module Optimizely
2222
# Default constants for CMAB requests
23-
DEFAULT_MAX_RETRIES = 3
23+
DEFAULT_MAX_RETRIES = 1
2424
DEFAULT_INITIAL_BACKOFF = 0.1 # in seconds (100 ms)
2525
DEFAULT_MAX_BACKOFF = 10 # in seconds
2626
DEFAULT_BACKOFF_MULTIPLIER = 2.0

lib/optimizely/cmab/cmab_service.rb

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#
1818
require 'optimizely/odp/lru_cache'
1919
require 'optimizely/decide/optimizely_decide_option'
20+
require 'optimizely/logger'
2021
require 'digest'
2122
require 'json'
2223
require 'securerandom'
@@ -26,6 +27,12 @@ module Optimizely
2627
CmabDecision = Struct.new(:variation_id, :cmab_uuid, keyword_init: true)
2728
CmabCacheValue = Struct.new(:attributes_hash, :variation_id, :cmab_uuid, keyword_init: true)
2829

30+
class DefaultCmabCacheOptions
31+
# CMAB Constants
32+
DEFAULT_CMAB_CACHE_TIMEOUT = (30 * 60) # in seconds
33+
DEFAULT_CMAB_CACHE_SIZE = 1000
34+
end
35+
2936
# Default CMAB service implementation
3037
class DefaultCmabService
3138
# Initializes a new instance of the CmabService.
@@ -42,7 +49,7 @@ class DefaultCmabService
4249
def initialize(cmab_cache, cmab_client, logger = nil)
4350
@cmab_cache = cmab_cache
4451
@cmab_client = cmab_client
45-
@logger = logger
52+
@logger = logger || NoOpLogger.new
4653
@locks = Array.new(NUM_LOCK_STRIPES) { Mutex.new }
4754
end
4855

@@ -81,30 +88,65 @@ def get_decision_impl(project_config, user_context, rule_id, options)
8188
# @return [CmabDecision] The decision object containing variation_id and cmab_uuid.
8289

8390
filtered_attributes = filter_attributes(project_config, user_context, rule_id)
91+
reasons = []
92+
93+
if options&.include?(Decide::OptimizelyDecideOption::IGNORE_CMAB_CACHE)
94+
reason = "Ignoring CMAB cache for user '#{user_context.user_id}' and rule '#{rule_id}'"
95+
@logger.log(Logger::DEBUG, reason)
96+
reasons << reason
97+
cmab_decision = fetch_decision(rule_id, user_context.user_id, filtered_attributes)
98+
return [cmab_decision, reasons]
99+
end
84100

85-
return fetch_decision(rule_id, user_context.user_id, filtered_attributes) if options&.include?(Decide::OptimizelyDecideOption::IGNORE_CMAB_CACHE)
86-
87-
@cmab_cache.reset if options&.include?(Decide::OptimizelyDecideOption::RESET_CMAB_CACHE)
101+
if options&.include?(Decide::OptimizelyDecideOption::RESET_CMAB_CACHE)
102+
reason = "Resetting CMAB cache for user '#{user_context.user_id}' and rule '#{rule_id}'"
103+
@logger.log(Logger::DEBUG, reason)
104+
reasons << reason
105+
@cmab_cache.reset
106+
end
88107

89108
cache_key = get_cache_key(user_context.user_id, rule_id)
90109

91-
@cmab_cache.remove(cache_key) if options&.include?(Decide::OptimizelyDecideOption::INVALIDATE_USER_CMAB_CACHE)
110+
if options&.include?(Decide::OptimizelyDecideOption::INVALIDATE_USER_CMAB_CACHE)
111+
reason = "Invalidating CMAB cache for user '#{user_context.user_id}' and rule '#{rule_id}'"
112+
@logger.log(Logger::DEBUG, reason)
113+
reasons << reason
114+
@cmab_cache.remove(cache_key)
115+
end
116+
92117
cached_value = @cmab_cache.lookup(cache_key)
93118
attributes_hash = hash_attributes(filtered_attributes)
94119

95120
if cached_value
96-
return CmabDecision.new(variation_id: cached_value.variation_id, cmab_uuid: cached_value.cmab_uuid) if cached_value.attributes_hash == attributes_hash
97-
98-
@cmab_cache.remove(cache_key)
121+
if cached_value.attributes_hash == attributes_hash
122+
reason = "CMAB cache hit for user '#{user_context.user_id}' and rule '#{rule_id}'"
123+
@logger.log(Logger::DEBUG, reason)
124+
reasons << reason
125+
return [CmabDecision.new(variation_id: cached_value.variation_id, cmab_uuid: cached_value.cmab_uuid), reasons]
126+
else
127+
reason = "CMAB cache attributes mismatch for user '#{user_context.user_id}' and rule '#{rule_id}', fetching new decision."
128+
@logger.log(Logger::DEBUG, reason)
129+
reasons << reason
130+
@cmab_cache.remove(cache_key)
131+
end
132+
else
133+
reason = "CMAB cache miss for user '#{user_context.user_id}' and rule '#{rule_id}'"
134+
@logger.log(Logger::DEBUG, reason)
135+
reasons << reason
99136
end
137+
100138
cmab_decision = fetch_decision(rule_id, user_context.user_id, filtered_attributes)
139+
reason = "CMAB decision is #{cmab_decision.to_h}"
140+
@logger.log(Logger::DEBUG, reason)
141+
reasons << reason
142+
101143
@cmab_cache.save(cache_key,
102144
CmabCacheValue.new(
103145
attributes_hash: attributes_hash,
104146
variation_id: cmab_decision.variation_id,
105147
cmab_uuid: cmab_decision.cmab_uuid
106148
))
107-
cmab_decision
149+
[cmab_decision, reasons]
108150
end
109151

110152
def fetch_decision(rule_id, user_id, attributes)

lib/optimizely/decision_service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,9 +649,10 @@ def get_decision_for_cmab_experiment(project_config, experiment, user_context, b
649649

650650
# User is in CMAB allocation, proceed to CMAB decision
651651
begin
652-
cmab_decision = @cmab_service.get_decision(
652+
cmab_decision, reasons = @cmab_service.get_decision(
653653
project_config, user_context, experiment['id'], decide_options
654654
)
655+
decide_reasons.push(*reasons)
655656
CmabDecisionResult.new(false, cmab_decision, decide_reasons)
656657
rescue StandardError => e
657658
error_message = "Failed to fetch CMAB data for experiment #{experiment['key']}."

lib/optimizely/optimizely_factory.rb

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
require 'optimizely/event/batch_event_processor'
2323
require 'optimizely/logger'
2424
require 'optimizely/notification_center'
25+
require 'optimizely/cmab/cmab_client'
26+
require 'optimizely/cmab/cmab_service'
2527

2628
module Optimizely
2729
class OptimizelyFactory
@@ -83,6 +85,40 @@ def self.blocking_timeout(blocking_timeout)
8385
@blocking_timeout = blocking_timeout
8486
end
8587

88+
# Convenience method for setting CMAB cache size.
89+
# @param cache_size Integer - Maximum number of items in CMAB cache.
90+
# @param logger - Optional LoggerInterface Provides a log method to log messages.
91+
def self.cmab_cache_size(cache_size, logger = NoOpLogger.new)
92+
unless cache_size.is_a?(Integer) && cache_size.positive?
93+
logger.log(
94+
Logger::ERROR,
95+
"CMAB cache size is invalid, setting to default size #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE}."
96+
)
97+
return
98+
end
99+
@cmab_cache_size = cache_size
100+
end
101+
102+
# Convenience method for setting CMAB cache TTL.
103+
# @param cache_ttl Numeric - Time in seconds for cache entries to live.
104+
# @param logger - Optional LoggerInterface Provides a log method to log messages.
105+
def self.cmab_cache_ttl(cache_ttl, logger = NoOpLogger.new)
106+
unless cache_ttl.is_a?(Numeric) && cache_ttl.positive?
107+
logger.log(
108+
Logger::ERROR,
109+
"CMAB cache TTL is invalid, setting to default TTL #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT}."
110+
)
111+
return
112+
end
113+
@cmab_cache_ttl = cache_ttl
114+
end
115+
116+
# Convenience method for setting custom CMAB cache.
117+
# @param custom_cache - Cache implementation responding to lookup, save, remove, and reset methods.
118+
def self.cmab_custom_cache(custom_cache)
119+
@cmab_custom_cache = custom_cache
120+
end
121+
86122
# Returns a new optimizely instance.
87123
#
88124
# @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
@@ -165,6 +201,14 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists
165201
notification_center: notification_center
166202
)
167203

204+
# Initialize CMAB components
205+
cmab_client = DefaultCmabClient.new(logger: logger)
206+
cmab_cache = @cmab_custom_cache || LRUCache.new(
207+
@cmab_cache_size || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE,
208+
@cmab_cache_ttl || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT
209+
)
210+
cmab_service = DefaultCmabService.new(cmab_cache, cmab_client, logger)
211+
168212
Optimizely::Project.new(
169213
datafile: datafile,
170214
event_dispatcher: event_dispatcher,
@@ -176,7 +220,8 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists
176220
config_manager: config_manager,
177221
notification_center: notification_center,
178222
event_processor: event_processor,
179-
settings: settings
223+
settings: settings,
224+
cmab_service: cmab_service
180225
)
181226
end
182227
end

spec/cmab/cmab_service_spec.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
require 'optimizely/odp/lru_cache'
66
require 'optimizely/cmab/cmab_client'
77
require 'optimizely/decide/optimizely_decide_option'
8+
require 'optimizely/logger'
89

910
describe Optimizely::DefaultCmabService do
1011
let(:mock_cmab_cache) { instance_double(Optimizely::LRUCache) }
1112
let(:mock_cmab_client) { instance_double(Optimizely::DefaultCmabClient) }
12-
let(:mock_logger) { double('logger') }
13+
let(:mock_logger) { Optimizely::NoOpLogger.new }
1314
let(:cmab_service) { described_class.new(mock_cmab_cache, mock_cmab_client, mock_logger) }
1415

1516
let(:mock_project_config) { double('project_config') }
@@ -47,18 +48,19 @@
4748

4849
allow(mock_cmab_cache).to receive(:lookup).with(expected_key).and_return(cached_value)
4950

50-
decision = cmab_service.get_decision(mock_project_config, mock_user_context, rule_id, [])
51+
decision, reasons = cmab_service.get_decision(mock_project_config, mock_user_context, rule_id, [])
5152

5253
expect(mock_cmab_cache).to have_received(:lookup).with(expected_key)
5354
expect(decision.variation_id).to eq('varA')
5455
expect(decision.cmab_uuid).to eq('uuid-123')
56+
expect(reasons).to include(match(/CMAB cache hit for user '#{user_id}' and rule '#{rule_id}'/))
5557
end
5658

5759
it 'ignores cache when option given' do
5860
allow(mock_cmab_client).to receive(:fetch_decision).and_return('varB')
5961
expected_attributes = {'age' => 25, 'location' => 'USA'}
6062

61-
decision = cmab_service.get_decision(
63+
decision, reasons = cmab_service.get_decision(
6264
mock_project_config,
6365
mock_user_context,
6466
rule_id,
@@ -73,6 +75,7 @@
7375
expected_attributes,
7476
decision.cmab_uuid
7577
)
78+
expect(reasons).to include(match(/Ignoring CMAB cache for user '#{user_id}' and rule '#{rule_id}'/))
7679
end
7780

7881
it 'invalidates user cache when option given' do
@@ -98,7 +101,7 @@
98101
allow(mock_cmab_cache).to receive(:lookup).and_return(nil)
99102
allow(mock_cmab_cache).to receive(:save)
100103

101-
decision = cmab_service.get_decision(
104+
decision, reasons = cmab_service.get_decision(
102105
mock_project_config,
103106
mock_user_context,
104107
rule_id,
@@ -108,6 +111,7 @@
108111
expect(mock_cmab_cache).to have_received(:reset)
109112
expect(decision.variation_id).to eq('varD')
110113
expect(decision.cmab_uuid).to be_a(String)
114+
expect(reasons).to include(match(/Resetting CMAB cache for user '#{user_id}' and rule '#{rule_id}'/))
111115
end
112116

113117
it 'fetches new decision when hash changes' do
@@ -126,7 +130,7 @@
126130
cmab_service.send(:hash_attributes, expected_attributes)
127131
expected_key = cmab_service.send(:get_cache_key, user_id, rule_id)
128132

129-
decision = cmab_service.get_decision(mock_project_config, mock_user_context, rule_id, [])
133+
decision, reasons = cmab_service.get_decision(mock_project_config, mock_user_context, rule_id, [])
130134

131135
expect(mock_cmab_cache).to have_received(:remove).with(expected_key)
132136
expect(mock_cmab_cache).to have_received(:save).with(
@@ -140,6 +144,7 @@
140144
expected_attributes,
141145
decision.cmab_uuid
142146
)
147+
expect(reasons).to include(match(/CMAB cache attributes mismatch for user '#{user_id}' and rule '#{rule_id}', fetching new decision./))
143148
end
144149

145150
it 'only passes cmab attributes to client' do
@@ -151,7 +156,7 @@
151156
})
152157
allow(mock_cmab_client).to receive(:fetch_decision).and_return('varF')
153158

154-
decision = cmab_service.get_decision(
159+
decision, reasons = cmab_service.get_decision(
155160
mock_project_config,
156161
mock_user_context,
157162
rule_id,
@@ -165,6 +170,7 @@
165170
{'age' => 25, 'location' => 'USA'},
166171
decision.cmab_uuid
167172
)
173+
expect(reasons).to include(match(/Ignoring CMAB cache for user '#{user_id}' and rule '#{rule_id}'/))
168174
end
169175
end
170176

0 commit comments

Comments
 (0)