Skip to content

Commit b912f62

Browse files
[FSSDK-11143] update: Implement CMAB Client (#579)
* Cmab datafile parsed * Add CMAB configuration and parsing tests with cmab datafile * Add copyright notice to CmabTest and CmabParsingTest files * Refactor cmab parsing logic to simplify null check in JsonConfigParser * update: implement remove method in DefaultLRUCache for cache entry removal * add: implement remove method tests in DefaultLRUCacheTest for various scenarios * refactor: remove unused methods from Cache interface * update: add reset method to Cache interface * add: implement CmabClient, CmabClientConfig, and RetryConfig with fetchDecision method and retry logic * update: improve error logging in DefaultCmabClient for fetchDecision method * add: implement unit tests for DefaultCmabClient with various scenarios and error handling * update: add missing license header to DefaultCmabClient.java * update: add missing license headers to CmabClient, CmabClientConfig, and RetryConfig classes * refactor: update DefaultCmabClient to use synchronous fetchDecision method with improved error handling and retry logic
1 parent 6367fdf commit b912f62

File tree

7 files changed

+820
-0
lines changed

7 files changed

+820
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.cmab.client;
17+
18+
import java.util.Map;
19+
20+
public interface CmabClient {
21+
/**
22+
* Fetches a decision from the CMAB prediction service.
23+
*
24+
* @param ruleId The rule/experiment ID
25+
* @param userId The user ID
26+
* @param attributes User attributes
27+
* @param cmabUUID The CMAB UUID
28+
* @return CompletableFuture containing the variation ID as a String
29+
*/
30+
String fetchDecision(String ruleId, String userId, Map<String, Object> attributes, String cmabUUID);
31+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.cmab.client;
17+
18+
import javax.annotation.Nullable;
19+
20+
/**
21+
* Configuration for CMAB client operations.
22+
* Contains only retry configuration since HTTP client is handled separately.
23+
*/
24+
public class CmabClientConfig {
25+
private final RetryConfig retryConfig;
26+
27+
public CmabClientConfig(@Nullable RetryConfig retryConfig) {
28+
this.retryConfig = retryConfig;
29+
}
30+
31+
@Nullable
32+
public RetryConfig getRetryConfig() {
33+
return retryConfig;
34+
}
35+
36+
/**
37+
* Creates a config with default retry settings.
38+
*/
39+
public static CmabClientConfig withDefaultRetry() {
40+
return new CmabClientConfig(RetryConfig.defaultConfig());
41+
}
42+
43+
/**
44+
* Creates a config with no retry.
45+
*/
46+
public static CmabClientConfig withNoRetry() {
47+
return new CmabClientConfig(null);
48+
}
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.cmab.client;
17+
18+
import com.optimizely.ab.OptimizelyRuntimeException;
19+
20+
public class CmabFetchException extends OptimizelyRuntimeException {
21+
public CmabFetchException(String message) {
22+
super(message);
23+
}
24+
25+
public CmabFetchException(String message, Throwable cause) {
26+
super(message, cause);
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.cmab.client;
17+
18+
import com.optimizely.ab.OptimizelyRuntimeException;
19+
20+
public class CmabInvalidResponseException extends OptimizelyRuntimeException{
21+
public CmabInvalidResponseException(String message) {
22+
super(message);
23+
}
24+
public CmabInvalidResponseException(String message, Throwable cause) {
25+
super(message, cause);
26+
}
27+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.cmab.client;
17+
/**
18+
* Configuration for retry behavior in CMAB client operations.
19+
*/
20+
public class RetryConfig {
21+
private final int maxRetries;
22+
private final long backoffBaseMs;
23+
private final double backoffMultiplier;
24+
private final int maxTimeoutMs;
25+
26+
/**
27+
* Creates a RetryConfig with custom retry and backoff settings.
28+
*
29+
* @param maxRetries Maximum number of retry attempts
30+
* @param backoffBaseMs Base delay in milliseconds for the first retry
31+
* @param backoffMultiplier Multiplier for exponential backoff (e.g., 2.0 for doubling)
32+
* @param maxTimeoutMs Maximum total timeout in milliseconds for all retry attempts
33+
*/
34+
public RetryConfig(int maxRetries, long backoffBaseMs, double backoffMultiplier, int maxTimeoutMs) {
35+
if (maxRetries < 0) {
36+
throw new IllegalArgumentException("maxRetries cannot be negative");
37+
}
38+
if (backoffBaseMs < 0) {
39+
throw new IllegalArgumentException("backoffBaseMs cannot be negative");
40+
}
41+
if (backoffMultiplier < 1.0) {
42+
throw new IllegalArgumentException("backoffMultiplier must be >= 1.0");
43+
}
44+
if (maxTimeoutMs < 0) {
45+
throw new IllegalArgumentException("maxTimeoutMs cannot be negative");
46+
}
47+
48+
this.maxRetries = maxRetries;
49+
this.backoffBaseMs = backoffBaseMs;
50+
this.backoffMultiplier = backoffMultiplier;
51+
this.maxTimeoutMs = maxTimeoutMs;
52+
}
53+
54+
/**
55+
* Creates a RetryConfig with default backoff settings and timeout (1 second base, 2x multiplier, 10 second timeout).
56+
*
57+
* @param maxRetries Maximum number of retry attempts
58+
*/
59+
public RetryConfig(int maxRetries) {
60+
this(maxRetries, 1000, 2.0, 10000); // Default: 1 second base, exponential backoff, 10 second timeout
61+
}
62+
63+
/**
64+
* Creates a default RetryConfig with 3 retries and exponential backoff.
65+
*/
66+
public static RetryConfig defaultConfig() {
67+
return new RetryConfig(3);
68+
}
69+
70+
/**
71+
* Creates a RetryConfig with no retries (single attempt only).
72+
*/
73+
public static RetryConfig noRetry() {
74+
return new RetryConfig(0, 0, 1.0, 0);
75+
}
76+
77+
public int getMaxRetries() {
78+
return maxRetries;
79+
}
80+
81+
public long getBackoffBaseMs() {
82+
return backoffBaseMs;
83+
}
84+
85+
public double getBackoffMultiplier() {
86+
return backoffMultiplier;
87+
}
88+
89+
public int getMaxTimeoutMs() {
90+
return maxTimeoutMs;
91+
}
92+
93+
/**
94+
* Calculates the delay for a specific retry attempt.
95+
*
96+
* @param attemptNumber The attempt number (0-based, so 0 = first retry)
97+
* @return Delay in milliseconds
98+
*/
99+
public long calculateDelay(int attemptNumber) {
100+
if (attemptNumber < 0) {
101+
return 0;
102+
}
103+
return (long) (backoffBaseMs * Math.pow(backoffMultiplier, attemptNumber));
104+
}
105+
106+
@Override
107+
public String toString() {
108+
return String.format("RetryConfig{maxRetries=%d, backoffBaseMs=%d, backoffMultiplier=%.1f, maxTimeoutMs=%d}",
109+
maxRetries, backoffBaseMs, backoffMultiplier, maxTimeoutMs);
110+
}
111+
112+
@Override
113+
public boolean equals(Object obj) {
114+
if (this == obj) return true;
115+
if (obj == null || getClass() != obj.getClass()) return false;
116+
117+
RetryConfig that = (RetryConfig) obj;
118+
return maxRetries == that.maxRetries &&
119+
backoffBaseMs == that.backoffBaseMs &&
120+
maxTimeoutMs == that.maxTimeoutMs &&
121+
Double.compare(that.backoffMultiplier, backoffMultiplier) == 0;
122+
}
123+
124+
@Override
125+
public int hashCode() {
126+
int result = maxRetries;
127+
result = 31 * result + Long.hashCode(backoffBaseMs);
128+
result = 31 * result + Double.hashCode(backoffMultiplier);
129+
result = 31 * result + Integer.hashCode(maxTimeoutMs);
130+
return result;
131+
}
132+
}

0 commit comments

Comments
 (0)