From 5292291349cc91cd8c6ee3c328960e4e59ed73d9 Mon Sep 17 00:00:00 2001
From: Saad Khan
Date: Mon, 3 Aug 2026 08:57:29 +0530
Subject: [PATCH 1/5] feat: add experiment_types support to Bulk API
- Add experiment_types field to BulkInput (container/namespace)
- Add resolveExperimentType and namespace experiment creation path
(prepareNamespaceExperimentJSONInput, frameNamespaceExperimentName)
- Add validateExperimentTypes to BulkServiceValidation
- Add BULK_INVALID_EXPERIMENT_TYPES error constant
---
.../analyzer/serviceObjects/BulkInput.java | 25 ++-
.../utils/AnalyzerErrorConstants.java | 1 +
.../analyzer/workerimpl/BulkJobManager.java | 174 ++++++++++++++++--
.../common/bulk/BulkServiceValidation.java | 52 +++++-
4 files changed, 229 insertions(+), 23 deletions(-)
diff --git a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
index e8b344cd4..3a19dd9ce 100644
--- a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
+++ b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
@@ -32,26 +32,33 @@ public class BulkInput {
private String metadata_profile;
private String measurement_duration;
private String requestId; //TODO: to be used for the Kafka consumer case to map requestID with jobID
-
+
/**
* Cluster name to use for all experiments in this bulk job.
* If provided, overrides cluster name from datasource metadata.
* If not provided, cluster name from metadata will be used.
*/
private String cluster_name;
-
+
/**
* Optional model settings to customize which recommendation models to generate.
* If not provided, all models will be generated.
*/
private ModelSettings model_settings;
-
+
/**
* Optional term settings to customize which recommendation terms to generate.
* If not provided, all terms will be generated.
*/
private TermSettings term_settings;
+ /**
+ * Experiment types to create in this bulk job (e.g. "container", "namespace").
+ * If provided, only experiments of the specified type(s) will be created.
+ * If not provided or empty, defaults to container experiments.
+ */
+ private List experiment_types;
+
// Getters and Setters
public String getRequestId() {
@@ -68,7 +75,8 @@ public BulkInput() {
@JsonIgnore
public boolean isEmpty() {
return (filter == null && time_range == null && measurement_duration == null && metadata_profile == null
- && datasource == null && cluster_name == null && model_settings == null && term_settings == null);
+ && datasource == null && cluster_name == null && model_settings == null && term_settings == null
+ && experiment_types == null);
}
public TimeRange getTime_range() {
@@ -135,6 +143,15 @@ public void setTerm_settings(TermSettings term_settings) {
this.term_settings = term_settings;
}
+
+ public List getExperiment_types() {
+ return experiment_types;
+ }
+
+ public void setExperiment_types(List experiment_types) {
+ this.experiment_types = experiment_types;
+ }
+
// Nested class for FilterWrapper that contains 'exclude' and 'include'
public static class FilterWrapper {
private Filter exclude;
diff --git a/src/main/java/com/autotune/analyzer/utils/AnalyzerErrorConstants.java b/src/main/java/com/autotune/analyzer/utils/AnalyzerErrorConstants.java
index 198014c96..b01d016ec 100644
--- a/src/main/java/com/autotune/analyzer/utils/AnalyzerErrorConstants.java
+++ b/src/main/java/com/autotune/analyzer/utils/AnalyzerErrorConstants.java
@@ -215,6 +215,7 @@ public static final class CreateExperimentAPI {
public static final String MISSING_NAMESPACE_DATA = "Missing NamespaceData for experimentType: %s";
public static final String MISSING_NAMESPACE = "Missing namespace for experimentType: %s";
public static final String INVALID_EXPERIMENT_TYPE = "Invalid experiment_type : %s";
+ public static final String BULK_INVALID_EXPERIMENT_TYPES = "Invalid experiment_types value(s): %s. Valid values are: container, namespace";
private CreateExperimentAPI() {
diff --git a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
index a3df491bd..36937aa56 100644
--- a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
+++ b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
@@ -189,7 +189,7 @@ public void run() {
setFinalJobStatus(COMPLETED, String.valueOf(HttpURLConnection.HTTP_OK), NOTHING_INFO, datasource);
} else {
jobData.setMetadata(metadataInfo);
- Map createExperimentAPIObjectMap = getExperimentMap(labelString, jobData, metadataInfo, datasource); //Todo Store this map in buffer and use it if BulkAPI pods restarts and support experiment_type
+ Map createExperimentAPIObjectMap = getExperimentMap(labelString, jobData, metadataInfo, datasource); //Todo Store this map in buffer and use it if BulkAPI pods restarts
// TODO: Remove getExperimentMap and instead collect all metadata, process it, and create experiments dynamically during metadata iteration.
jobData.getSummary().setTotal_experiments(createExperimentAPIObjectMap.size());
jobData.getSummary().setProcessed_experiments(0);
@@ -511,6 +511,9 @@ Map getExperimentMap(String labelString, Bulk
String statusValue = "failure";
Timer.Sample timerGetExpMap = Timer.start(MetricsConfig.meterRegistry());
try {
+ // Resolve the requested experiment type once for the entire job
+ AnalyzerConstants.ExperimentType resolvedType = resolveExperimentType(this.bulkInput.getExperiment_types());
+
Map createExperimentAPIObjectMap = new HashMap<>();
Collection dataSourceCollection = metadataInfo.getDatasources().values();
for (DataSource ds : dataSourceCollection) {
@@ -521,19 +524,29 @@ Map getExperimentMap(String labelString, Bulk
: dsc.getDataSourceClusterName();
HashMap namespaceHashMap = dsc.getNamespaces();
for (DataSourceNamespace namespace : namespaceHashMap.values()) {
- HashMap dataSourceWorkloadHashMap = namespace.getWorkloads();
- if (dataSourceWorkloadHashMap != null) {
- for (DataSourceWorkload dsw : dataSourceWorkloadHashMap.values()) {
- HashMap dataSourceContainerHashMap = dsw.getContainers();
- if (dataSourceContainerHashMap != null) {
- for (DataSourceContainer dc : dataSourceContainerHashMap.values()) {
- // Experiment name - dynamically constructed
- String experiment_name = frameExperimentName(labelString, clusterName, namespace, dsw, dc);
- // create JSON to be passed in the createExperimentAPI
- List createExperimentAPIObjectList = new ArrayList<>();
- CreateExperimentAPIObject apiObject = prepareCreateExperimentJSONInput(dc, clusterName, dsw, namespace,
- experiment_name, createExperimentAPIObjectList);
- createExperimentAPIObjectMap.put(experiment_name, apiObject);
+ if (resolvedType == AnalyzerConstants.ExperimentType.NAMESPACE) {
+ // One namespace experiment per namespace
+ String experiment_name = frameNamespaceExperimentName(labelString, dsc, namespace);
+ List createExperimentAPIObjectList = new ArrayList<>();
+ CreateExperimentAPIObject apiObject = prepareNamespaceExperimentJSONInput(dsc, namespace,
+ experiment_name, createExperimentAPIObjectList);
+ createExperimentAPIObjectMap.put(experiment_name, apiObject);
+ } else {
+ // Default: one container experiment per container
+ HashMap dataSourceWorkloadHashMap = namespace.getWorkloads();
+ if (dataSourceWorkloadHashMap != null) {
+ for (DataSourceWorkload dsw : dataSourceWorkloadHashMap.values()) {
+ HashMap dataSourceContainerHashMap = dsw.getContainers();
+ if (dataSourceContainerHashMap != null) {
+ for (DataSourceContainer dc : dataSourceContainerHashMap.values()) {
+ // Experiment name - dynamically constructed
+ String experiment_name = frameExperimentName(labelString, clusterName, namespace, dsw, dc);
+ // create JSON to be passed in the createExperimentAPI
+ List createExperimentAPIObjectList = new ArrayList<>();
+ CreateExperimentAPIObject apiObject = prepareCreateExperimentJSONInput(dc, clusterName, dsw, namespace,
+ experiment_name, createExperimentAPIObjectList);
+ createExperimentAPIObjectMap.put(experiment_name, apiObject);
+ }
}
}
}
@@ -725,11 +738,11 @@ private CreateExperimentAPIObject prepareCreateExperimentJSONInput(DataSourceCon
kubernetesAPIObject.setNamespace(namespace.getNamespace());
kubernetesAPIObjectList.add(kubernetesAPIObject);
createExperimentAPIObject.setKubernetesObjects(kubernetesAPIObjectList);
-
+
// Create recommendation settings with threshold
RecommendationSettings rs = new RecommendationSettings();
rs.setThreshold(CREATE_EXPERIMENT_CONFIG_BEAN.getThreshold());
-
+
// Pass through model_settings and term_settings from bulk payload if provided
if (bulkInput.getModel_settings() != null) {
rs.setModelSettings(bulkInput.getModel_settings());
@@ -737,7 +750,7 @@ private CreateExperimentAPIObject prepareCreateExperimentJSONInput(DataSourceCon
if (bulkInput.getTerm_settings() != null) {
rs.setTermSettings(bulkInput.getTerm_settings());
}
-
+
createExperimentAPIObject.setRecommendationSettings(rs);
TrialSettings trialSettings = new TrialSettings();
trialSettings.setMeasurement_durationMinutes(CREATE_EXPERIMENT_CONFIG_BEAN.getMeasurementDurationStr());
@@ -752,9 +765,134 @@ private CreateExperimentAPIObject prepareCreateExperimentJSONInput(DataSourceCon
return createExperimentAPIObject;
}
+
+ /**
+ * Resolves the ExperimentType that the bulk job should create.
+ * Defaults to CONTAINER when experiment_types is absent or empty.
+ *
+ * BulkServiceValidation enforces that at most one entry is present and
+ * that it is a recognised value ("container" or "namespace"), so by the
+ * time this method is called the list is guaranteed to be null, empty, or
+ * a single valid string. The {@code IllegalArgumentException} fallback is
+ * kept purely as a defensive measure.
+ *
+ * @param experimentTypes the validated list from BulkInput.experiment_types
+ * (null, empty, or exactly one recognized value)
+ * @return the resolved ExperimentType
+ */
+ private AnalyzerConstants.ExperimentType resolveExperimentType(List experimentTypes) {
+ if (experimentTypes == null || experimentTypes.isEmpty()) {
+ return AnalyzerConstants.ExperimentType.CONTAINER;
+ }
+ // Validation guarantees exactly one entry; get(0) is intentional.
+ try {
+ return AnalyzerConstants.ExperimentType.valueOf(experimentTypes.get(0).trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Unrecognised experiment_type '{}' in bulk input; defaulting to CONTAINER", experimentTypes.get(0));
+ return AnalyzerConstants.ExperimentType.CONTAINER;
+ }
+ }
+
+ /**
+ * Builds a CreateExperimentAPIObject for a namespace-level experiment.
+ * The kubernetes_objects entry contains only a namespaces block (no
+ * workload name/type or containers), matching the payload expected by
+ * CreateExperiment for experiment_type "namespace".
+ *
+ * @param dsc DataSourceCluster for cluster metadata
+ * @param namespace DataSourceNamespace whose namespace is being tracked
+ * @param experiment_name pre-framed experiment name
+ * @param createExperimentAPIObjects accumulator list
+ * @return the constructed CreateExperimentAPIObject
+ */
+ private CreateExperimentAPIObject prepareNamespaceExperimentJSONInput(DataSourceCluster dsc, DataSourceNamespace namespace,
+ String experiment_name, List createExperimentAPIObjects) throws IOException {
+ CreateExperimentAPIObject createExperimentAPIObject = new CreateExperimentAPIObject();
+ createExperimentAPIObject.setMode(CREATE_EXPERIMENT_CONFIG_BEAN.getMode());
+ createExperimentAPIObject.setTargetCluster(CREATE_EXPERIMENT_CONFIG_BEAN.getTarget());
+ createExperimentAPIObject.setApiVersion(CREATE_EXPERIMENT_CONFIG_BEAN.getVersion());
+ createExperimentAPIObject.setExperimentName(experiment_name);
+ createExperimentAPIObject.setDatasource(this.bulkInput.getDatasource());
+
+ // Use cluster_name from bulk payload if provided (trimmed), otherwise use metadata cluster
+ String clusterName = dsc.getDataSourceClusterName();
+ if (this.bulkInput.getCluster_name() != null) {
+ String trimmedClusterName = this.bulkInput.getCluster_name().trim();
+ if (!trimmedClusterName.isEmpty()) {
+ clusterName = trimmedClusterName;
+ }
+ }
+ createExperimentAPIObject.setClusterName(clusterName);
+ createExperimentAPIObject.setPerformanceProfile(CREATE_EXPERIMENT_CONFIG_BEAN.getPerformanceProfile());
+ createExperimentAPIObject.setMetadataProfile(CREATE_EXPERIMENT_CONFIG_BEAN.getMetadataProfile());
+
+ // Namespace experiment: kubernetes_objects has only a namespaces block, no containers
+ List kubernetesAPIObjectList = new ArrayList<>();
+ KubernetesAPIObject kubernetesAPIObject = new KubernetesAPIObject();
+ NamespaceAPIObject namespaceAPIObject = new NamespaceAPIObject(namespace.getNamespace(), null, null);
+ kubernetesAPIObject.setNamespaceAPIObject(namespaceAPIObject);
+ kubernetesAPIObjectList.add(kubernetesAPIObject);
+ createExperimentAPIObject.setKubernetesObjects(kubernetesAPIObjectList);
+
+ // Recommendation settings
+ RecommendationSettings rs = new RecommendationSettings();
+ rs.setThreshold(CREATE_EXPERIMENT_CONFIG_BEAN.getThreshold());
+ if (this.bulkInput.getModel_settings() != null) {
+ rs.setModelSettings(this.bulkInput.getModel_settings());
+ }
+ if (this.bulkInput.getTerm_settings() != null) {
+ rs.setTermSettings(this.bulkInput.getTerm_settings());
+ }
+ createExperimentAPIObject.setRecommendationSettings(rs);
+
+ TrialSettings trialSettings = new TrialSettings();
+ trialSettings.setMeasurement_durationMinutes(CREATE_EXPERIMENT_CONFIG_BEAN.getMeasurementDurationStr());
+ createExperimentAPIObject.setTrialSettings(trialSettings);
+
+ createExperimentAPIObject.setExperiment_id(Utils.generateID(createExperimentAPIObject.toString()));
+ createExperimentAPIObject.setStatus(AnalyzerConstants.ExperimentStatus.IN_PROGRESS);
+ createExperimentAPIObject.setExperimentType(AnalyzerConstants.ExperimentType.NAMESPACE);
+
+ createExperimentAPIObjects.add(createExperimentAPIObject);
+ return createExperimentAPIObject;
+ }
+
+ /**
+ * Frames the experiment name for a namespace-level experiment.
+ * Uses datasource, cluster name, and namespace — workload/container
+ * segments are not meaningful for namespace experiments.
+ *
+ * @param labelString label filter string (may be null)
+ * @param dataSourceCluster cluster metadata
+ * @param namespace namespace metadata
+ * @return framed experiment name
+ */
+ public String frameNamespaceExperimentName(String labelString, DataSourceCluster dataSourceCluster,
+ DataSourceNamespace namespace) {
+ String datasource = this.bulkInput.getDatasource();
+ String clusterName = dataSourceCluster.getDataSourceClusterName();
+ String namespaceName = namespace.getNamespace();
+
+ // Namespace experiment name: datasource|clustername|namespace
+ String experimentName = datasource + "|" + clusterName + "|" + namespaceName;
+
+ if (null != labelString) {
+ Map labelsMap = parseLabelString(labelString);
+ Pattern labelPattern = Pattern.compile("%label:([a-zA-Z0-9_]+)%");
+ Matcher matcher = labelPattern.matcher(experimentName);
+ while (matcher.find()) {
+ String labelKey = matcher.group(1);
+ String labelValue = labelsMap.getOrDefault(labelKey, "unknown" + labelKey);
+ experimentName = experimentName.replace(matcher.group(), labelValue != null ? labelValue : "unknown" + labelKey);
+ }
+ }
+ LOGGER.debug("Namespace experiment name: {}", experimentName);
+ return experimentName;
+ }
+
/**
* @param labelString
- * @param dataSourceCluster
+ * @param clusterName
* @param dataSourceNamespace
* @param dataSourceWorkload
* @param dataSourceContainer
diff --git a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
index e2352b6d8..83c98a9bf 100644
--- a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
+++ b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
@@ -18,6 +18,7 @@
import com.autotune.analyzer.kruizeObject.ModelSettings;
import com.autotune.analyzer.kruizeObject.TermSettings;
import com.autotune.analyzer.serviceObjects.BulkInput;
+import com.autotune.analyzer.utils.AnalyzerErrorConstants;
import com.autotune.common.data.ValidationOutputData;
import com.autotune.common.datasource.DataSourceInfo;
import com.autotune.common.datasource.DataSourceOperatorImpl;
@@ -29,8 +30,9 @@
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
-import java.util.Arrays;
import java.util.List;
+import java.util.Set;
+import java.util.Arrays;
/**
* Utility class that performs validation for bulk service requests.
@@ -49,6 +51,12 @@ public class BulkServiceValidation {
private static final Logger LOGGER = LoggerFactory.getLogger(BulkServiceValidation.class);
+ // Cluster name validation constants
+ private static final int MAX_CLUSTER_NAME_LENGTH = 253;
+
+ // Valid experiment types supported by the bulk engine
+ private static final Set VALID_EXPERIMENT_TYPES = Set.of(KruizeConstants.JSONKeys.CONTAINER, KruizeConstants.JSONKeys.NAMESPACE);
+
// Valid model and term names (case-insensitive)
private static final List VALID_MODELS = Arrays.asList(
KruizeConstants.JSONKeys.PERFORMANCE,
@@ -102,6 +110,10 @@ public static ValidationOutputData validate(BulkInput payload, String jobID) thr
validationOutputData = buildErrorOutput(validateTermSettings(payload.getTerm_settings()), jobID);
if (validationOutputData != null) return validationOutputData;
+ // Validate experiment_types if provided
+ validationOutputData = buildErrorOutput(validateExperimentTypes(payload.getExperiment_types()), jobID);
+ if (validationOutputData != null) return validationOutputData;
+
if (payload.getDatasource() != null) {
validationOutputData = buildErrorOutput(validateDatasourceConnection(payload.getDatasource()), jobID);
}
@@ -292,4 +304,42 @@ public static String validateTermSettings(TermSettings termSettings) {
return "";
}
+ /**
+ * Validates the experiment_types field if provided.
+ * Checks for:
+ *
+ * - Exactly one entry — multiple types are not supported; the bulk engine
+ * creates experiments of a single type per job
+ * - Non-null, non-empty entry value
+ * - Valid type name (container, namespace)
+ *
+ *
+ * @param experimentTypes the list of experiment types to validate (can be null)
+ * @return an error message if validation fails; otherwise an empty string
+ */
+ public static String validateExperimentTypes(List experimentTypes) {
+ if (experimentTypes == null || experimentTypes.isEmpty()) {
+ return ""; // null/empty is valid; defaults to container experiments
+ }
+
+ if (experimentTypes.size() > 1) {
+ return "experiment_types accepts at most one value per bulk job. " +
+ "Provided: " + experimentTypes;
+ }
+
+ String type = experimentTypes.get(0);
+ if (type == null || type.trim().isEmpty()) {
+ return "experiment_types contains a null or empty value";
+ }
+
+ if (!VALID_EXPERIMENT_TYPES.contains(type.trim().toLowerCase())) {
+ return String.format(
+ AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.BULK_INVALID_EXPERIMENT_TYPES,
+ List.of(type)
+ );
+ }
+
+ return "";
+ }
+
}
From 654ea4afa0d2937e7e17f3921a7ecca7a83c42e2 Mon Sep 17 00:00:00 2001
From: Saad Khan
Date: Mon, 10 Aug 2026 16:37:50 +0530
Subject: [PATCH 2/5] update docs, add string constant, handles null for
exp-types
Signed-off-by: Saad Khan
---
design/BulkAPI.md | 7 ++++---
.../com/autotune/analyzer/serviceObjects/BulkInput.java | 4 +++-
.../com/autotune/analyzer/workerimpl/BulkJobManager.java | 5 ++++-
.../java/com/autotune/operator/KruizeDeploymentInfo.java | 1 +
4 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/design/BulkAPI.md b/design/BulkAPI.md
index 3c6a2c92d..5dc054058 100644
--- a/design/BulkAPI.md
+++ b/design/BulkAPI.md
@@ -60,8 +60,7 @@ progress of the job.
"metadata_profile": "cluster-metadata-local-monitoring",
"measurement_duration": "15min",
"experiment_types": [
- "container",
- "namespace"
+ "container"
],
"cluster_name": "prod-cluster",
"model_settings": {
@@ -94,7 +93,9 @@ progress of the job.
- **datasource:** The data source, e.g., `"Cbank1Xyz"`.
-- **experiment_types:** Specifies the type(s) of experiments to run, e.g., `"container"` or `"namespace"`.
+- **experiment_types:** Specifies the type of experiment to create for this bulk job. Currently, only a single value is
+ supported per request — either `"container"` (default) or `"namespace"`. Support for specifying multiple experiment
+ types in one request will be added in a future release.
- **webhook:** The `webhook` parameter allows the system to notify an external service or consumer about the completion
status of
diff --git a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
index 3a19dd9ce..7e5d89efd 100644
--- a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
+++ b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
@@ -149,7 +149,9 @@ public List getExperiment_types() {
}
public void setExperiment_types(List experiment_types) {
- this.experiment_types = experiment_types;
+ if (experiment_types != null) {
+ this.experiment_types = experiment_types;
+ }
}
// Nested class for FilterWrapper that contains 'exclude' and 'include'
diff --git a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
index 36937aa56..e902f8b5e 100644
--- a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
+++ b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
@@ -874,7 +874,10 @@ public String frameNamespaceExperimentName(String labelString, DataSourceCluster
String namespaceName = namespace.getNamespace();
// Namespace experiment name: datasource|clustername|namespace
- String experimentName = datasource + "|" + clusterName + "|" + namespaceName;
+ String experimentName = KruizeDeploymentInfo.namespace_experiment_name_format
+ .replace("%datasource%", datasource)
+ .replace("%clustername%", clusterName)
+ .replace("%namespace%", namespaceName);
if (null != labelString) {
Map labelsMap = parseLabelString(labelString);
diff --git a/src/main/java/com/autotune/operator/KruizeDeploymentInfo.java b/src/main/java/com/autotune/operator/KruizeDeploymentInfo.java
index b9e5eb61f..668cc3407 100644
--- a/src/main/java/com/autotune/operator/KruizeDeploymentInfo.java
+++ b/src/main/java/com/autotune/operator/KruizeDeploymentInfo.java
@@ -85,6 +85,7 @@ public class KruizeDeploymentInfo {
public static int generate_recommendations_date_range_limit_in_days = 15;
public static Integer delete_partition_threshold_in_days = DELETE_PARTITION_THRESHOLD_IN_DAYS;
public static String experiment_name_format = "%datasource%|%clustername%|%namespace%|%workloadname%(%workloadtype%)|%containername%";
+ public static String namespace_experiment_name_format = "%datasource%|%clustername%|%namespace%";
private static Hashtable tunableLayerPair;
//private static KubernetesClient kubernetesClient;
private static KubeEventLogger kubeEventLogger;
From f5cbe89b8997c7dfb842ec6120153343c6ddfc00 Mon Sep 17 00:00:00 2001
From: Saad Khan
Date: Thu, 20 Aug 2026 08:45:16 +0530
Subject: [PATCH 3/5] add empty check
Signed-off-by: Saad Khan
---
.../java/com/autotune/analyzer/serviceObjects/BulkInput.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
index 7e5d89efd..c24227498 100644
--- a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
+++ b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
@@ -149,7 +149,7 @@ public List getExperiment_types() {
}
public void setExperiment_types(List experiment_types) {
- if (experiment_types != null) {
+ if (experiment_types != null && !experiment_types.isEmpty()) {
this.experiment_types = experiment_types;
}
}
From f57ba9433e7a9e1d1638127943ca2ab4c0ca3713 Mon Sep 17 00:00:00 2001
From: Saad Khan
Date: Thu, 27 Aug 2026 17:18:20 +0530
Subject: [PATCH 4/5] replace list with enum, add commons library and other
fixes
Signed-off-by: Saad Khan
---
pom.xml | 7 +++++++
.../analyzer/serviceObjects/BulkInput.java | 12 +++++++-----
.../analyzer/utils/AnalyzerConstants.java | 9 ++++++++-
.../analyzer/workerimpl/BulkJobManager.java | 15 ++++-----------
.../common/bulk/BulkServiceValidation.java | 17 ++++++++++-------
5 files changed, 36 insertions(+), 24 deletions(-)
diff --git a/pom.xml b/pom.xml
index 4ac110905..abfbf263b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
3.6.2
5.23.0
4.2.0
+ 4.4
3.5.2
-Dnet.bytebuddy.experimental=true
@@ -197,6 +198,12 @@
${kafka-version}
+
+ org.apache.commons
+ commons-collections4
+ ${commons-collections4-version}
+
+
diff --git a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
index c24227498..ed56158c6 100644
--- a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
+++ b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
@@ -17,7 +17,9 @@
import com.autotune.analyzer.kruizeObject.ModelSettings;
import com.autotune.analyzer.kruizeObject.TermSettings;
+import com.autotune.analyzer.utils.AnalyzerConstants;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
import java.util.Map;
@@ -53,11 +55,11 @@ public class BulkInput {
private TermSettings term_settings;
/**
- * Experiment types to create in this bulk job (e.g. "container", "namespace").
+ * Experiment types to create in this bulk job (e.g. CONTAINER, NAMESPACE).
* If provided, only experiments of the specified type(s) will be created.
* If not provided or empty, defaults to container experiments.
*/
- private List experiment_types;
+ private List experiment_types;
// Getters and Setters
@@ -144,12 +146,12 @@ public void setTerm_settings(TermSettings term_settings) {
}
- public List getExperiment_types() {
+ public List getExperiment_types() {
return experiment_types;
}
- public void setExperiment_types(List experiment_types) {
- if (experiment_types != null && !experiment_types.isEmpty()) {
+ public void setExperiment_types(List experiment_types) {
+ if (CollectionUtils.isNotEmpty(experiment_types)) {
this.experiment_types = experiment_types;
}
}
diff --git a/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java b/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
index 829bbc4bf..231987da0 100644
--- a/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
+++ b/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
@@ -17,6 +17,7 @@
import com.autotune.utils.KruizeConstants;
import com.autotune.utils.Utils;
+import com.fasterxml.jackson.annotation.JsonCreator;
import java.util.Map;
import java.util.*;
@@ -326,7 +327,13 @@ public enum ExperimentType {
CONTAINER, // For container-level experiments
NAMESPACE, // For namespace-level experiments
CLUSTER, // For cluster-wide experiments
- WORKLOAD // For application-specific experiments
+ WORKLOAD; // For application-specific experiments
+
+ @JsonCreator
+ public static ExperimentType fromString(String value) {
+ if (value == null) return null;
+ return ExperimentType.valueOf(value.trim().toUpperCase());
+ }
}
/**
diff --git a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
index e902f8b5e..36daa992e 100644
--- a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
+++ b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
@@ -771,26 +771,19 @@ private CreateExperimentAPIObject prepareCreateExperimentJSONInput(DataSourceCon
* Defaults to CONTAINER when experiment_types is absent or empty.
*
* BulkServiceValidation enforces that at most one entry is present and
- * that it is a recognised value ("container" or "namespace"), so by the
+ * that it is a valid {@link AnalyzerConstants.ExperimentType}, so by the
* time this method is called the list is guaranteed to be null, empty, or
- * a single valid string. The {@code IllegalArgumentException} fallback is
- * kept purely as a defensive measure.
+ * a single valid enum value.
*
* @param experimentTypes the validated list from BulkInput.experiment_types
* (null, empty, or exactly one recognized value)
* @return the resolved ExperimentType
*/
- private AnalyzerConstants.ExperimentType resolveExperimentType(List experimentTypes) {
+ private AnalyzerConstants.ExperimentType resolveExperimentType(List experimentTypes) {
if (experimentTypes == null || experimentTypes.isEmpty()) {
return AnalyzerConstants.ExperimentType.CONTAINER;
}
- // Validation guarantees exactly one entry; get(0) is intentional.
- try {
- return AnalyzerConstants.ExperimentType.valueOf(experimentTypes.get(0).trim().toUpperCase());
- } catch (IllegalArgumentException e) {
- LOGGER.warn("Unrecognised experiment_type '{}' in bulk input; defaulting to CONTAINER", experimentTypes.get(0));
- return AnalyzerConstants.ExperimentType.CONTAINER;
- }
+ return experimentTypes.get(0);
}
/**
diff --git a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
index 83c98a9bf..49662036e 100644
--- a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
+++ b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
@@ -24,7 +24,9 @@
import com.autotune.common.datasource.DataSourceOperatorImpl;
import com.autotune.common.utils.CommonUtils;
import com.autotune.database.service.ExperimentDBService;
+import com.autotune.analyzer.utils.AnalyzerConstants;
import com.autotune.utils.KruizeConstants;
+import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,7 +57,8 @@ public class BulkServiceValidation {
private static final int MAX_CLUSTER_NAME_LENGTH = 253;
// Valid experiment types supported by the bulk engine
- private static final Set VALID_EXPERIMENT_TYPES = Set.of(KruizeConstants.JSONKeys.CONTAINER, KruizeConstants.JSONKeys.NAMESPACE);
+ private static final Set VALID_EXPERIMENT_TYPES = Set.of(
+ AnalyzerConstants.ExperimentType.CONTAINER, AnalyzerConstants.ExperimentType.NAMESPACE);
// Valid model and term names (case-insensitive)
private static final List VALID_MODELS = Arrays.asList(
@@ -317,8 +320,8 @@ public static String validateTermSettings(TermSettings termSettings) {
* @param experimentTypes the list of experiment types to validate (can be null)
* @return an error message if validation fails; otherwise an empty string
*/
- public static String validateExperimentTypes(List experimentTypes) {
- if (experimentTypes == null || experimentTypes.isEmpty()) {
+ public static String validateExperimentTypes(List experimentTypes) {
+ if (CollectionUtils.isEmpty(experimentTypes)) {
return ""; // null/empty is valid; defaults to container experiments
}
@@ -327,12 +330,12 @@ public static String validateExperimentTypes(List experimentTypes) {
"Provided: " + experimentTypes;
}
- String type = experimentTypes.get(0);
- if (type == null || type.trim().isEmpty()) {
- return "experiment_types contains a null or empty value";
+ AnalyzerConstants.ExperimentType type = experimentTypes.get(0);
+ if (type == null) {
+ return "experiment_types contains a null value";
}
- if (!VALID_EXPERIMENT_TYPES.contains(type.trim().toLowerCase())) {
+ if (!VALID_EXPERIMENT_TYPES.contains(type)) {
return String.format(
AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.BULK_INVALID_EXPERIMENT_TYPES,
List.of(type)
From 02ee1491430e973385576d7a7b2a4b441b2de8b5 Mon Sep 17 00:00:00 2001
From: Saad Khan
Date: Fri, 28 Aug 2026 15:27:37 +0530
Subject: [PATCH 5/5] address review comments
Signed-off-by: Saad Khan
---
pom.xml | 7 ---
.../analyzer/serviceObjects/BulkInput.java | 3 +-
.../analyzer/utils/AnalyzerConstants.java | 8 ++-
.../analyzer/workerimpl/BulkJobManager.java | 54 +++++--------------
.../common/bulk/BulkServiceValidation.java | 3 +-
5 files changed, 21 insertions(+), 54 deletions(-)
diff --git a/pom.xml b/pom.xml
index abfbf263b..4ac110905 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,6 @@
3.6.2
5.23.0
4.2.0
- 4.4
3.5.2
-Dnet.bytebuddy.experimental=true
@@ -198,12 +197,6 @@
${kafka-version}
-
- org.apache.commons
- commons-collections4
- ${commons-collections4-version}
-
-
diff --git a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
index ed56158c6..3c965334f 100644
--- a/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
+++ b/src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
@@ -19,7 +19,6 @@
import com.autotune.analyzer.kruizeObject.TermSettings;
import com.autotune.analyzer.utils.AnalyzerConstants;
import com.fasterxml.jackson.annotation.JsonIgnore;
-import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
import java.util.Map;
@@ -151,7 +150,7 @@ public List getExperiment_types() {
}
public void setExperiment_types(List experiment_types) {
- if (CollectionUtils.isNotEmpty(experiment_types)) {
+ if (experiment_types != null && !experiment_types.isEmpty()) {
this.experiment_types = experiment_types;
}
}
diff --git a/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java b/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
index 231987da0..046f37c2f 100644
--- a/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
+++ b/src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
@@ -331,8 +331,12 @@ public enum ExperimentType {
@JsonCreator
public static ExperimentType fromString(String value) {
- if (value == null) return null;
- return ExperimentType.valueOf(value.trim().toUpperCase());
+ if (value == null || value.trim().isEmpty()) return null;
+ try {
+ return ExperimentType.valueOf(value.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
}
}
diff --git a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
index 36daa992e..adaefabbd 100644
--- a/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
+++ b/src/main/java/com/autotune/analyzer/workerimpl/BulkJobManager.java
@@ -511,8 +511,10 @@ Map getExperimentMap(String labelString, Bulk
String statusValue = "failure";
Timer.Sample timerGetExpMap = Timer.start(MetricsConfig.meterRegistry());
try {
- // Resolve the requested experiment type once for the entire job
- AnalyzerConstants.ExperimentType resolvedType = resolveExperimentType(this.bulkInput.getExperiment_types());
+ List experimentTypes = this.bulkInput.getExperiment_types();
+ AnalyzerConstants.ExperimentType experimentType = (experimentTypes == null || experimentTypes.isEmpty())
+ ? AnalyzerConstants.ExperimentType.CONTAINER
+ : experimentTypes.get(0);
Map createExperimentAPIObjectMap = new HashMap<>();
Collection dataSourceCollection = metadataInfo.getDatasources().values();
@@ -524,11 +526,11 @@ Map getExperimentMap(String labelString, Bulk
: dsc.getDataSourceClusterName();
HashMap namespaceHashMap = dsc.getNamespaces();
for (DataSourceNamespace namespace : namespaceHashMap.values()) {
- if (resolvedType == AnalyzerConstants.ExperimentType.NAMESPACE) {
+ if (experimentType == AnalyzerConstants.ExperimentType.NAMESPACE) {
// One namespace experiment per namespace
- String experiment_name = frameNamespaceExperimentName(labelString, dsc, namespace);
+ String experiment_name = frameNamespaceExperimentName(labelString, clusterName, namespace);
List createExperimentAPIObjectList = new ArrayList<>();
- CreateExperimentAPIObject apiObject = prepareNamespaceExperimentJSONInput(dsc, namespace,
+ CreateExperimentAPIObject apiObject = prepareNamespaceExperimentJSONInput(clusterName, namespace,
experiment_name, createExperimentAPIObjectList);
createExperimentAPIObjectMap.put(experiment_name, apiObject);
} else {
@@ -766,39 +768,19 @@ private CreateExperimentAPIObject prepareCreateExperimentJSONInput(DataSourceCon
}
- /**
- * Resolves the ExperimentType that the bulk job should create.
- * Defaults to CONTAINER when experiment_types is absent or empty.
- *
- * BulkServiceValidation enforces that at most one entry is present and
- * that it is a valid {@link AnalyzerConstants.ExperimentType}, so by the
- * time this method is called the list is guaranteed to be null, empty, or
- * a single valid enum value.
- *
- * @param experimentTypes the validated list from BulkInput.experiment_types
- * (null, empty, or exactly one recognized value)
- * @return the resolved ExperimentType
- */
- private AnalyzerConstants.ExperimentType resolveExperimentType(List experimentTypes) {
- if (experimentTypes == null || experimentTypes.isEmpty()) {
- return AnalyzerConstants.ExperimentType.CONTAINER;
- }
- return experimentTypes.get(0);
- }
-
/**
* Builds a CreateExperimentAPIObject for a namespace-level experiment.
* The kubernetes_objects entry contains only a namespaces block (no
* workload name/type or containers), matching the payload expected by
* CreateExperiment for experiment_type "namespace".
*
- * @param dsc DataSourceCluster for cluster metadata
+ * @param clusterName resolved cluster name (user override or data source cluster name)
* @param namespace DataSourceNamespace whose namespace is being tracked
* @param experiment_name pre-framed experiment name
* @param createExperimentAPIObjects accumulator list
* @return the constructed CreateExperimentAPIObject
*/
- private CreateExperimentAPIObject prepareNamespaceExperimentJSONInput(DataSourceCluster dsc, DataSourceNamespace namespace,
+ private CreateExperimentAPIObject prepareNamespaceExperimentJSONInput(String clusterName, DataSourceNamespace namespace,
String experiment_name, List createExperimentAPIObjects) throws IOException {
CreateExperimentAPIObject createExperimentAPIObject = new CreateExperimentAPIObject();
createExperimentAPIObject.setMode(CREATE_EXPERIMENT_CONFIG_BEAN.getMode());
@@ -806,15 +788,6 @@ private CreateExperimentAPIObject prepareNamespaceExperimentJSONInput(DataSource
createExperimentAPIObject.setApiVersion(CREATE_EXPERIMENT_CONFIG_BEAN.getVersion());
createExperimentAPIObject.setExperimentName(experiment_name);
createExperimentAPIObject.setDatasource(this.bulkInput.getDatasource());
-
- // Use cluster_name from bulk payload if provided (trimmed), otherwise use metadata cluster
- String clusterName = dsc.getDataSourceClusterName();
- if (this.bulkInput.getCluster_name() != null) {
- String trimmedClusterName = this.bulkInput.getCluster_name().trim();
- if (!trimmedClusterName.isEmpty()) {
- clusterName = trimmedClusterName;
- }
- }
createExperimentAPIObject.setClusterName(clusterName);
createExperimentAPIObject.setPerformanceProfile(CREATE_EXPERIMENT_CONFIG_BEAN.getPerformanceProfile());
createExperimentAPIObject.setMetadataProfile(CREATE_EXPERIMENT_CONFIG_BEAN.getMetadataProfile());
@@ -855,15 +828,14 @@ private CreateExperimentAPIObject prepareNamespaceExperimentJSONInput(DataSource
* Uses datasource, cluster name, and namespace — workload/container
* segments are not meaningful for namespace experiments.
*
- * @param labelString label filter string (may be null)
- * @param dataSourceCluster cluster metadata
- * @param namespace namespace metadata
+ * @param labelString label filter string (may be null)
+ * @param clusterName resolved cluster name (user override or data source cluster name)
+ * @param namespace namespace metadata
* @return framed experiment name
*/
- public String frameNamespaceExperimentName(String labelString, DataSourceCluster dataSourceCluster,
+ public String frameNamespaceExperimentName(String labelString, String clusterName,
DataSourceNamespace namespace) {
String datasource = this.bulkInput.getDatasource();
- String clusterName = dataSourceCluster.getDataSourceClusterName();
String namespaceName = namespace.getNamespace();
// Namespace experiment name: datasource|clustername|namespace
diff --git a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
index 49662036e..980c7111e 100644
--- a/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
+++ b/src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
@@ -26,7 +26,6 @@
import com.autotune.database.service.ExperimentDBService;
import com.autotune.analyzer.utils.AnalyzerConstants;
import com.autotune.utils.KruizeConstants;
-import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -321,7 +320,7 @@ public static String validateTermSettings(TermSettings termSettings) {
* @return an error message if validation fails; otherwise an empty string
*/
public static String validateExperimentTypes(List experimentTypes) {
- if (CollectionUtils.isEmpty(experimentTypes)) {
+ if (experimentTypes == null || experimentTypes.isEmpty()) {
return ""; // null/empty is valid; defaults to container experiments
}