Skip to content

[FSSDK-11143] update: Implement CMAB Client #579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2640eaa
Cmab datafile parsed
FarhanAnjum-opti Aug 11, 2025
ad048df
Merge branch 'master' into farhan-anjum/FSSDK-11134-cmab-datafile-par…
FarhanAnjum-opti Aug 11, 2025
2d21ee2
Add CMAB configuration and parsing tests with cmab datafile
FarhanAnjum-opti Aug 12, 2025
0d3a88d
Add copyright notice to CmabTest and CmabParsingTest files
FarhanAnjum-opti Aug 12, 2025
50334f1
Refactor cmab parsing logic to simplify null check in JsonConfigParser
FarhanAnjum-opti Aug 12, 2025
87c553c
Merge branch 'master' into farhan-anjum/FSSDK-11134-cmab-datafile-par…
FarhanAnjum-opti Aug 12, 2025
8258bb0
update: implement remove method in DefaultLRUCache for cache entry re…
FarhanAnjum-opti Aug 12, 2025
4fa8cbe
add: implement remove method tests in DefaultLRUCacheTest for various…
FarhanAnjum-opti Aug 13, 2025
85b26d4
Merge branch 'master' into farhan-anjum/FSSDK-11152-update-lru-cache-…
FarhanAnjum-opti Aug 13, 2025
aa955eb
refactor: remove unused methods from Cache interface
FarhanAnjum-opti Aug 14, 2025
d3fc4bb
update: add reset method to Cache interface
FarhanAnjum-opti Aug 14, 2025
044c230
add: implement CmabClient, CmabClientConfig, and RetryConfig with fet…
FarhanAnjum-opti Aug 20, 2025
8eb694e
update: improve error logging in DefaultCmabClient for fetchDecision …
FarhanAnjum-opti Aug 20, 2025
50e2f7d
add: implement unit tests for DefaultCmabClient with various scenario…
FarhanAnjum-opti Aug 20, 2025
254d308
Merge branch 'master' into farhan-anjum/FSSDK-11143-implement-cmab-cl…
FarhanAnjum-opti Aug 20, 2025
04bb6f4
update: add missing license header to DefaultCmabClient.java
FarhanAnjum-opti Aug 20, 2025
7e9487c
update: add missing license headers to CmabClient, CmabClientConfig, …
FarhanAnjum-opti Aug 20, 2025
d41c775
refactor: update DefaultCmabClient to use synchronous fetchDecision m…
FarhanAnjum-opti Aug 25, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright 2025, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab.cmab.client;

import java.util.Map;

public interface CmabClient {
/**
* Fetches a decision from the CMAB prediction service.
*
* @param ruleId The rule/experiment ID
* @param userId The user ID
* @param attributes User attributes
* @param cmabUUID The CMAB UUID
* @return CompletableFuture containing the variation ID as a String
*/
String fetchDecision(String ruleId, String userId, Map<String, Object> attributes, String cmabUUID);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2025, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab.cmab.client;

import javax.annotation.Nullable;

/**
* Configuration for CMAB client operations.
* Contains only retry configuration since HTTP client is handled separately.
*/
public class CmabClientConfig {
private final RetryConfig retryConfig;

public CmabClientConfig(@Nullable RetryConfig retryConfig) {
this.retryConfig = retryConfig;
}

@Nullable
public RetryConfig getRetryConfig() {
return retryConfig;
}

/**
* Creates a config with default retry settings.
*/
public static CmabClientConfig withDefaultRetry() {
return new CmabClientConfig(RetryConfig.defaultConfig());
}

/**
* Creates a config with no retry.
*/
public static CmabClientConfig withNoRetry() {
return new CmabClientConfig(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright 2025, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab.cmab.client;

import com.optimizely.ab.OptimizelyRuntimeException;

public class CmabFetchException extends OptimizelyRuntimeException {
public CmabFetchException(String message) {
super(message);
}

public CmabFetchException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright 2025, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab.cmab.client;

import com.optimizely.ab.OptimizelyRuntimeException;

public class CmabInvalidResponseException extends OptimizelyRuntimeException{
public CmabInvalidResponseException(String message) {
super(message);
}
public CmabInvalidResponseException(String message, Throwable cause) {
super(message, cause);
}
}
132 changes: 132 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/cmab/client/RetryConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Copyright 2025, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab.cmab.client;
/**
* Configuration for retry behavior in CMAB client operations.
*/
public class RetryConfig {
private final int maxRetries;
private final long backoffBaseMs;
private final double backoffMultiplier;
private final int maxTimeoutMs;

/**
* Creates a RetryConfig with custom retry and backoff settings.
*
* @param maxRetries Maximum number of retry attempts
* @param backoffBaseMs Base delay in milliseconds for the first retry
* @param backoffMultiplier Multiplier for exponential backoff (e.g., 2.0 for doubling)
* @param maxTimeoutMs Maximum total timeout in milliseconds for all retry attempts
*/
public RetryConfig(int maxRetries, long backoffBaseMs, double backoffMultiplier, int maxTimeoutMs) {
if (maxRetries < 0) {
throw new IllegalArgumentException("maxRetries cannot be negative");
}
if (backoffBaseMs < 0) {
throw new IllegalArgumentException("backoffBaseMs cannot be negative");
}
if (backoffMultiplier < 1.0) {
throw new IllegalArgumentException("backoffMultiplier must be >= 1.0");
}
if (maxTimeoutMs < 0) {
throw new IllegalArgumentException("maxTimeoutMs cannot be negative");
}

this.maxRetries = maxRetries;
this.backoffBaseMs = backoffBaseMs;
this.backoffMultiplier = backoffMultiplier;
this.maxTimeoutMs = maxTimeoutMs;
}

/**
* Creates a RetryConfig with default backoff settings and timeout (1 second base, 2x multiplier, 10 second timeout).
*
* @param maxRetries Maximum number of retry attempts
*/
public RetryConfig(int maxRetries) {
this(maxRetries, 1000, 2.0, 10000); // Default: 1 second base, exponential backoff, 10 second timeout
}

/**
* Creates a default RetryConfig with 3 retries and exponential backoff.
*/
public static RetryConfig defaultConfig() {
return new RetryConfig(3);
}

/**
* Creates a RetryConfig with no retries (single attempt only).
*/
public static RetryConfig noRetry() {
return new RetryConfig(0, 0, 1.0, 0);
}

public int getMaxRetries() {
return maxRetries;
}

public long getBackoffBaseMs() {
return backoffBaseMs;
}

public double getBackoffMultiplier() {
return backoffMultiplier;
}

public int getMaxTimeoutMs() {
return maxTimeoutMs;
}

/**
* Calculates the delay for a specific retry attempt.
*
* @param attemptNumber The attempt number (0-based, so 0 = first retry)
* @return Delay in milliseconds
*/
public long calculateDelay(int attemptNumber) {
if (attemptNumber < 0) {
return 0;
}
return (long) (backoffBaseMs * Math.pow(backoffMultiplier, attemptNumber));
}

@Override
public String toString() {
return String.format("RetryConfig{maxRetries=%d, backoffBaseMs=%d, backoffMultiplier=%.1f, maxTimeoutMs=%d}",
maxRetries, backoffBaseMs, backoffMultiplier, maxTimeoutMs);
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;

RetryConfig that = (RetryConfig) obj;
return maxRetries == that.maxRetries &&
backoffBaseMs == that.backoffBaseMs &&
maxTimeoutMs == that.maxTimeoutMs &&
Double.compare(that.backoffMultiplier, backoffMultiplier) == 0;
}

@Override
public int hashCode() {
int result = maxRetries;
result = 31 * result + Long.hashCode(backoffBaseMs);
result = 31 * result + Double.hashCode(backoffMultiplier);
result = 31 * result + Integer.hashCode(maxTimeoutMs);
return result;
}
}
Loading
Loading