Skip to content

Commit 5c10e8f

Browse files
[FSSDK-11995] chore: expose cmab cache config (#609)
* chore: update SDK links in README.md - Remove extra space at the end of the Ruby SDK link * fix: update cmab retry configuration - Update max retries from 3 to 1 in CmabRetryConfig struct feat: add default values for cmab cache in DefaultCmabService - Add constants for DEFAULT_CMAB_CACHE_TIMEOUT and DEFAULT_CMAB_CACHE_SIZE - Assign DEFAULT_CMAB_CACHE_SIZE as 100 and DEFAULT_CMAB_CACHE_TIMEOUT as 600 - Change access control of cmabCache property to 'let' in DefaultCmabService feat: allow custom cache size and timeout for cmab cache in DefaultCmabService - Add new static function createDefault with parameters for cache size and timeout - Implement creation of DefaultCmabService with custom cache size and timeout feat: add cmab cache settings to OptimizelySdkSettings - Add cmabCacheSize and cmabCacheTitmeoutInSecs properties to OptimizelySdkSettings struct - Initialize properties with default values and update init method fix: update cmab cache settings initialization in OptimizelyClient - Update initialization of cmabService with size and timeout values from sdkSettings chore: refactor tests for cmab cache settings in OptimizelyClientTests_ODP - Modify test cases to check the cmabCacheSize and cmabCacheTitmeoutInSecs values build: remove unnecessary code from FakeDecisionService - Remove empty lines for cleanliness refactor: improve parameter passing in FakeDecisionService constructor - Modify the constructor to pass cmabService parameter explicitly in super.init * chore: add CmabCache.swift and update references in project files * fix: fix cmab cache descriptions in OptimizelySdkSettings.swift * chore: update default CMAB cache timeout to 30 minutes * refactor: adjust CmabCache inheritance to typealias and update cache timeout calculation * refactor: remove unused CmabCache.swift and update references to DEFAULT_CMAB_CACHE_TIMEOUT and DEFAULT_CMAB_CACHE_SIZE
1 parent 983fa51 commit 5c10e8f

11 files changed

+269
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@ Used to enforce Swift style and conventions.
129129

130130
- React - https://github.com/optimizely/react-sdk
131131

132-
- Ruby - https://github.com/optimizely/ruby-sdk
132+
- Ruby - https://github.com/optimizely/ruby-sdk

Sources/CMAB/CmabClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ enum CmabClientError: Error, Equatable {
3232
}
3333

3434
struct CmabRetryConfig {
35-
var maxRetries: Int = 3
35+
var maxRetries: Int = 1
3636
var initialBackoff: TimeInterval = 0.1 // seconds
3737
var maxBackoff: TimeInterval = 10.0 // seconds
3838
var backoffMultiplier: Double = 2.0

Sources/CMAB/CmabService.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,22 @@ protocol CmabService {
4141
completion: @escaping CmabDecisionCompletionHandler)
4242
}
4343

44+
let DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 // 30 minutes
45+
let DEFAULT_CMAB_CACHE_SIZE = 100
46+
47+
typealias CmabCache = LruCache<String, CmabCacheValue>
48+
4449
class DefaultCmabService: CmabService {
4550
typealias UserAttributes = [String : Any?]
4651

4752
private let cmabClient: CmabClient
48-
private let cmabCache: LruCache<String, CmabCacheValue>
53+
let cmabCache: CmabCache
4954
private let logger = OPTLoggerFactory.getLogger()
5055

5156
private static let NUM_LOCKS = 1000
5257
private let locks: [NSLock]
5358

54-
init(cmabClient: CmabClient, cmabCache: LruCache<String, CmabCacheValue>) {
59+
init(cmabClient: CmabClient, cmabCache: CmabCache) {
5560
self.cmabClient = cmabClient
5661
self.cmabCache = cmabCache
5762
self.locks = (0..<Self.NUM_LOCKS).map { _ in NSLock() }
@@ -193,9 +198,14 @@ class DefaultCmabService: CmabService {
193198

194199
extension DefaultCmabService {
195200
static func createDefault() -> DefaultCmabService {
196-
let DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 * 1000 // 30 minutes in milliseconds
197-
let DEFAULT_CMAB_CACHE_SIZE = 1000
198-
let cache = LruCache<String, CmabCacheValue>(size: DEFAULT_CMAB_CACHE_SIZE, timeoutInSecs: DEFAULT_CMAB_CACHE_TIMEOUT)
201+
let cache = CmabCache(size: DEFAULT_CMAB_CACHE_SIZE, timeoutInSecs: DEFAULT_CMAB_CACHE_TIMEOUT)
202+
return DefaultCmabService(cmabClient: DefaultCmabClient(), cmabCache: cache)
203+
}
204+
205+
static func createDefault(cacheSize: Int, cacheTimeout: Int) -> DefaultCmabService {
206+
// if cache timeout is set to 0 or negative, use default timeout
207+
let timeout = cacheTimeout <= 0 ? DEFAULT_CMAB_CACHE_TIMEOUT : cacheTimeout
208+
let cache = CmabCache(size: cacheSize, timeoutInSecs: timeout)
199209
return DefaultCmabService(cmabClient: DefaultCmabClient(), cmabCache: cache)
200210
}
201211
}

Sources/ODP/OptimizelySdkSettings.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public struct OptimizelySdkSettings {
2525
let timeoutForSegmentFetchInSecs: Int
2626
/// The timeout in seconds of odp event dispatch - OS default timeout will be used if this is set to zero.
2727
let timeoutForOdpEventInSecs: Int
28+
/// The maximum size of cmab cache
29+
let cmabCacheSize: Int
30+
/// The timeout in seconds of cmab cache
31+
let cmabCacheTimeoutInSecs: Int
2832
/// ODP features are disabled if this is set to true.
2933
let disableOdp: Bool
3034
/// VUID is enabled if this is set to true.
@@ -37,6 +41,8 @@ public struct OptimizelySdkSettings {
3741
/// - segmentsCacheTimeoutInSecs: The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout.
3842
/// - timeoutForSegmentFetchInSecs: The timeout in seconds of odp segment fetch (optional. default = 10) - OS default timeout will be used if this is set to zero.
3943
/// - timeoutForOdpEventInSecs: The timeout in seconds of odp event dispatch (optional. default = 10) - OS default timeout will be used if this is set to zero.
44+
/// - cmabCacheSize: The maximum size of cmab cache (optional. default = 100).
45+
/// - cmabCacheTimeoutInSecs: The timeout in seconds of amb cache (optional. default = 30 * 60).
4046
/// - disableOdp: Set this flag to true (default = false) to disable ODP features
4147
/// - enableVuid: Set this flag to true (default = false) to enable vuid.
4248
/// - sdkName: Set this flag to override sdkName included in events
@@ -45,12 +51,16 @@ public struct OptimizelySdkSettings {
4551
segmentsCacheTimeoutInSecs: Int = 600,
4652
timeoutForSegmentFetchInSecs: Int = 10,
4753
timeoutForOdpEventInSecs: Int = 10,
54+
cmabCacheSize: Int = 100,
55+
cmabCacheTimeoutInSecs: Int = 30 * 60,
4856
disableOdp: Bool = false,
4957
enableVuid: Bool = false,
5058
sdkName: String? = nil,
5159
sdkVersion: String? = nil) {
5260
self.segmentsCacheSize = segmentsCacheSize
5361
self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs
62+
self.cmabCacheSize = cmabCacheSize
63+
self.cmabCacheTimeoutInSecs = cmabCacheTimeoutInSecs
5464
self.timeoutForSegmentFetchInSecs = timeoutForSegmentFetchInSecs
5565
self.timeoutForOdpEventInSecs = timeoutForOdpEventInSecs
5666
self.disableOdp = disableOdp

Sources/Optimizely/OptimizelyClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ open class OptimizelyClient: NSObject {
108108
let logger = logger ?? DefaultLogger()
109109
type(of: logger).logLevel = defaultLogLevel ?? .info
110110

111-
let cmabService = DefaultCmabService.createDefault()
111+
let cmabService = DefaultCmabService.createDefault(cacheSize: self.sdkSettings.cmabCacheSize, cacheTimeout: self.sdkSettings.cmabCacheTimeoutInSecs)
112112

113113
self.registerServices(sdkKey: sdkKey,
114114
logger: logger,

Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ class OptimizelyClientTests_ODP: XCTestCase {
3737

3838
func testSdkSettings_default() {
3939
let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey)
40-
40+
let cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache
41+
XCTAssertEqual(100, cmabCache.maxSize)
42+
XCTAssertEqual(30 * 60, cmabCache.timeoutInSecs)
4143
XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.maxSize)
4244
XCTAssertEqual(600, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs)
4345
XCTAssertEqual(10, optimizely.odpManager.segmentManager?.apiMgr.resourceTimeoutInSecs)
@@ -47,8 +49,13 @@ class OptimizelyClientTests_ODP: XCTestCase {
4749

4850
func testSdkSettings_custom() {
4951
var sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12,
50-
segmentsCacheTimeoutInSecs: 345)
52+
segmentsCacheTimeoutInSecs: 345,
53+
cmabCacheSize: 50,
54+
cmabCacheTimeoutInSecs: 120)
5155
var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings)
56+
var cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache
57+
XCTAssertEqual(50, cmabCache.maxSize)
58+
XCTAssertEqual(120, cmabCache.timeoutInSecs)
5259
XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.maxSize)
5360
XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs)
5461

@@ -57,6 +64,13 @@ class OptimizelyClientTests_ODP: XCTestCase {
5764
optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings)
5865
XCTAssertEqual(34, optimizely.odpManager.segmentManager?.apiMgr.resourceTimeoutInSecs)
5966
XCTAssertEqual(45, optimizely.odpManager.eventManager?.apiMgr.resourceTimeoutInSecs)
67+
68+
sdkSettings = OptimizelySdkSettings(cmabCacheSize: 50, cmabCacheTimeoutInSecs: -10)
69+
70+
optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings)
71+
cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache
72+
XCTAssertEqual(50, cmabCache.maxSize)
73+
XCTAssertEqual(1800, cmabCache.timeoutInSecs)
6074

6175
sdkSettings = OptimizelySdkSettings(disableOdp: true)
6276
optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings)

Tests/OptimizelyTests-Common/CmabServiceTests.swift

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class MockUserContext: OptimizelyUserContext {
111111
class DefaultCmabServiceTests: XCTestCase {
112112
fileprivate var cmabClient: MockCmabClient!
113113
fileprivate var config: MockProjectConfig!
114-
var cmabCache: LruCache<String, CmabCacheValue>!
114+
var cmabCache: CmabCache!
115115
var cmabService: DefaultCmabService!
116116
var userContext: OptimizelyUserContext!
117117
let userAttributes: [String: Any] = ["age": 25, "location": "San Francisco"]
@@ -120,7 +120,7 @@ class DefaultCmabServiceTests: XCTestCase {
120120
super.setUp()
121121
config = MockProjectConfig()
122122
cmabClient = MockCmabClient()
123-
cmabCache = LruCache<String, CmabCacheValue>(size: 10, timeoutInSecs: 10)
123+
cmabCache = CmabCache(size: 10, timeoutInSecs: 10)
124124
cmabService = DefaultCmabService(cmabClient: cmabClient, cmabCache: cmabCache)
125125
// Set up user context
126126
userContext = MockUserContext(userId: "test-user", attributes: userAttributes)
@@ -657,3 +657,222 @@ extension DefaultCmabServiceTests {
657657
}
658658

659659
}
660+
661+
extension DefaultCmabServiceTests {
662+
663+
func testCacheSizeZero() {
664+
// Create a cache with size 0 (no caching)
665+
let zeroCmabCache = CmabCache(size: 0, timeoutInSecs: 10)
666+
let zeroCacheService = DefaultCmabService(cmabClient: cmabClient, cmabCache: zeroCmabCache)
667+
668+
cmabClient.fetchDecisionResult = .success("variation-first")
669+
670+
let expectation1 = self.expectation(description: "first request")
671+
672+
// First request
673+
zeroCacheService.getDecision(
674+
config: config,
675+
userContext: userContext,
676+
ruleId: "exp-123",
677+
options: []
678+
) { result in
679+
switch result {
680+
case .success(let decision):
681+
XCTAssertEqual(decision.variationId, "variation-first")
682+
XCTAssertTrue(self.cmabClient.fetchDecisionCalled, "Should call API on first request")
683+
684+
case .failure(let error):
685+
XCTFail("Expected success but got error: \(error)")
686+
}
687+
expectation1.fulfill()
688+
}
689+
690+
wait(for: [expectation1], timeout: 1.0)
691+
692+
// Reset and change the variation
693+
cmabClient.reset()
694+
cmabClient.fetchDecisionResult = .success("variation-second")
695+
696+
let expectation2 = self.expectation(description: "second request")
697+
698+
// Second request - should NOT use cache (size = 0)
699+
zeroCacheService.getDecision(
700+
config: config,
701+
userContext: userContext,
702+
ruleId: "exp-123",
703+
options: []
704+
) { result in
705+
switch result {
706+
case .success(let decision):
707+
XCTAssertEqual(decision.variationId, "variation-second")
708+
XCTAssertTrue(self.cmabClient.fetchDecisionCalled, "Should call API again when cache size is 0")
709+
710+
case .failure(let error):
711+
XCTFail("Expected success but got error: \(error)")
712+
}
713+
expectation2.fulfill()
714+
}
715+
716+
wait(for: [expectation2], timeout: 1.0)
717+
}
718+
719+
func testCacheTimeoutZero() {
720+
// When timeout is 0, LruCache uses default timeout (30 * 60 seconds = 1800 seconds)
721+
// So cache will NOT expire within the test timeframe
722+
let zeroTimeoutCache = CmabCache(size: 10, timeoutInSecs: 0)
723+
let zeroTimeoutService = DefaultCmabService(cmabClient: cmabClient, cmabCache: zeroTimeoutCache)
724+
725+
cmabClient.fetchDecisionResult = .success("variation-first")
726+
727+
let expectation1 = self.expectation(description: "first request")
728+
729+
// First request
730+
zeroTimeoutService.getDecision(
731+
config: config,
732+
userContext: userContext,
733+
ruleId: "exp-123",
734+
options: []
735+
) { result in
736+
switch result {
737+
case .success(let decision):
738+
XCTAssertEqual(decision.variationId, "variation-first")
739+
XCTAssertTrue(self.cmabClient.fetchDecisionCalled, "Should call API on first request")
740+
741+
case .failure(let error):
742+
XCTFail("Expected success but got error: \(error)")
743+
}
744+
expectation1.fulfill()
745+
}
746+
747+
wait(for: [expectation1], timeout: 1.0)
748+
749+
// Reset client but don't change the result
750+
cmabClient.reset()
751+
cmabClient.fetchDecisionResult = .success("variation-second")
752+
753+
// Small delay (but not enough to expire default 1800s timeout)
754+
Thread.sleep(forTimeInterval: 0.1)
755+
756+
let expectation2 = self.expectation(description: "second request")
757+
758+
// Second request - SHOULD use cache (timeout defaults to 1800s, not expired)
759+
zeroTimeoutService.getDecision(
760+
config: config,
761+
userContext: userContext,
762+
ruleId: "exp-123",
763+
options: []
764+
) { result in
765+
switch result {
766+
case .success(let decision):
767+
// Should get cached value, not the new one
768+
XCTAssertEqual(decision.variationId, "variation-first")
769+
XCTAssertFalse(self.cmabClient.fetchDecisionCalled, "Should use cache when timeout defaults to 1800s")
770+
771+
case .failure(let error):
772+
XCTFail("Expected success but got error: \(error)")
773+
}
774+
expectation2.fulfill()
775+
}
776+
777+
wait(for: [expectation2], timeout: 1.0)
778+
}
779+
780+
func testCacheSizeZeroAndTimeoutZero() {
781+
// Create a cache with both size 0 and timeout 0 (completely disabled)
782+
let disabledCache = CmabCache(size: 0, timeoutInSecs: 0)
783+
let disabledCacheService = DefaultCmabService(cmabClient: cmabClient, cmabCache: disabledCache)
784+
785+
cmabClient.fetchDecisionResult = .success("variation-1")
786+
787+
let expectation1 = self.expectation(description: "first request")
788+
789+
// First request
790+
disabledCacheService.getDecision(
791+
config: config,
792+
userContext: userContext,
793+
ruleId: "exp-123",
794+
options: []
795+
) { result in
796+
switch result {
797+
case .success(let decision):
798+
XCTAssertEqual(decision.variationId, "variation-1")
799+
800+
case .failure(let error):
801+
XCTFail("Expected success but got error: \(error)")
802+
}
803+
expectation1.fulfill()
804+
}
805+
806+
wait(for: [expectation1], timeout: 1.0)
807+
808+
let apiCallCount1 = cmabClient.fetchDecisionCalled ? 1 : 0
809+
810+
// Reset and make multiple requests
811+
cmabClient.reset()
812+
cmabClient.fetchDecisionResult = .success("variation-2")
813+
814+
let expectation2 = self.expectation(description: "second request")
815+
816+
disabledCacheService.getDecision(
817+
config: config,
818+
userContext: userContext,
819+
ruleId: "exp-123",
820+
options: []
821+
) { result in
822+
expectation2.fulfill()
823+
}
824+
825+
wait(for: [expectation2], timeout: 1.0)
826+
827+
let apiCallCount2 = cmabClient.fetchDecisionCalled ? 1 : 0
828+
829+
XCTAssertEqual(apiCallCount1, 1, "First request should call API")
830+
XCTAssertEqual(apiCallCount2, 1, "Second request should also call API (no caching)")
831+
}
832+
833+
func testSyncCacheSizeZero() {
834+
// Create a cache with size 0
835+
let zeroCmabCache = CmabCache(size: 0, timeoutInSecs: 10)
836+
let zeroCacheService = DefaultCmabService(cmabClient: cmabClient, cmabCache: zeroCmabCache)
837+
838+
cmabClient.fetchDecisionResult = .success("variation-first")
839+
840+
// First request
841+
let result1 = zeroCacheService.getDecision(
842+
config: config,
843+
userContext: userContext,
844+
ruleId: "exp-123",
845+
options: []
846+
)
847+
848+
switch result1 {
849+
case .success(let decision):
850+
XCTAssertEqual(decision.variationId, "variation-first")
851+
XCTAssertTrue(cmabClient.fetchDecisionCalled, "Should call API on first request")
852+
853+
case .failure(let error):
854+
XCTFail("Expected success but got error: \(error)")
855+
}
856+
857+
// Reset and change variation
858+
cmabClient.reset()
859+
cmabClient.fetchDecisionResult = .success("variation-second")
860+
861+
// Second request - should NOT use cache
862+
let result2 = zeroCacheService.getDecision(
863+
config: config,
864+
userContext: userContext,
865+
ruleId: "exp-123",
866+
options: []
867+
)
868+
869+
switch result2 {
870+
case .success(let decision):
871+
XCTAssertEqual(decision.variationId, "variation-second")
872+
XCTAssertTrue(cmabClient.fetchDecisionCalled, "Should call API again when cache size is 0")
873+
874+
case .failure(let error):
875+
XCTFail("Expected success but got error: \(error)")
876+
}
877+
}
878+
}

0 commit comments

Comments
 (0)