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 }