Skip to content
20 changes: 20 additions & 0 deletions src/main/java/com/autotune/analyzer/serviceObjects/BulkInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*******************************************************************************/
package com.autotune.analyzer.serviceObjects;

import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -43,6 +44,12 @@ public void setRequestId(String requestId) {
public BulkInput() {
}

@JsonIgnore
public boolean isEmpty() {
return (filter == null && time_range == null && measurement_duration == null && metadata_profile == null
&& datasource == null);
}

public TimeRange getTime_range() {
return time_range;
}
Expand Down Expand Up @@ -166,6 +173,19 @@ public String getEnd() {
public void setEnd(String end) {
this.end = end;
}

@JsonIgnore
public boolean isEmpty() {
return (start == null && end == null);
}

@Override
public String toString() {
return "TimeRange{" +
"start='" + start + '\'' +
", end='" + end + '\'' +
'}';
}
}

public static class Webhook {
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/com/autotune/analyzer/services/BulkService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.autotune.analyzer.serviceObjects.BulkInput;
import com.autotune.analyzer.serviceObjects.BulkJobStatus;
import com.autotune.analyzer.workerimpl.BulkJobManager;
import com.autotune.common.bulk.BulkServiceValidation;
import com.autotune.common.data.ValidationOutputData;
import com.autotune.database.dao.ExperimentDAO;
import com.autotune.database.dao.ExperimentDAOImpl;
import com.autotune.database.table.lm.KruizeBulkJobEntry;
Expand Down Expand Up @@ -283,6 +285,13 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

// Generate a unique jobID
String jobID = UUID.randomUUID().toString();
// validate the input params
if (payload != null && !payload.isEmpty()) {
ValidationOutputData validationOutputData = BulkServiceValidation.validate(payload, jobID);
if (!validationOutputData.isSuccess()) {
throw new Exception(validationOutputData.getMessage());
}
}
BulkJobStatus jobStatus = new BulkJobStatus(jobID, IN_PROGRESS, Instant.now(), payload);

if (KruizeDeploymentInfo.TEST_USE_ONLY_CACHE_JOB_IN_MEM)
Expand All @@ -303,6 +312,13 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
jsonObject.put(JOB_ID, jobID);
response.getWriter().write(jsonObject.toString());
statusValue = "success";
} catch (Exception e) {
sendErrorResponse(
response,
null,
HttpServletResponse.SC_BAD_REQUEST,
e.getMessage()
);
} finally {
if (null != timerCreateBulkJob) {
MetricsConfig.timerCreateBulkJob = MetricsConfig.timerBCreateBulkJob.tag("status", statusValue).register(MetricsConfig.meterRegistry());
Expand All @@ -320,7 +336,6 @@ public void sendErrorResponse(HttpServletResponse response, Exception e, int htt
IOException {
if (null != e) {
LOGGER.error(e.toString());
e.printStackTrace();
if (null == errorMsg) errorMsg = e.getMessage();
}
response.sendError(httpStatusCode, errorMsg);
Expand Down
170 changes: 170 additions & 0 deletions src/main/java/com/autotune/common/bulk/BulkServiceValidation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*******************************************************************************
* Copyright (c) 2025, IBM Corporation and others.
*
* 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
*
* http://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.autotune.common.bulk;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khansaad Thanks for raising the validations PR. Please kindly add the Copyrights header, as it's a new file.


import com.autotune.analyzer.serviceObjects.BulkInput;
import com.autotune.common.data.ValidationOutputData;
import com.autotune.common.datasource.DataSourceInfo;
import com.autotune.common.datasource.DataSourceOperatorImpl;
import com.autotune.common.utils.CommonUtils;
import com.autotune.database.service.ExperimentDBService;
import com.autotune.utils.KruizeConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;

/**
* Utility class that performs validation for bulk service requests.
* This class provides methods to validate various fields in a BulkInput
* object such as time range and datasource reachability, and generates
* appropriate ValidationOutputData objects based on validation results.
*
* The validation flow primarily checks:
* <ul>
* <li>Time range consistency and format</li>
* <li>Datasource connectivity and serviceability</li>
* </ul>
* If all validations pass, a successful response is returned.
*/
public class BulkServiceValidation {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a Javadoc could be a bit helpful for the devs to understand about this class.


private static final Logger LOGGER = LoggerFactory.getLogger(BulkServiceValidation.class);

/**
* Validates the bulk request payload and returns the corresponding validation output.
* The following validations occur in sequence:
* <ol>
* <li>Validate the time range specified in the payload.</li>
* <li>If a datasource name is provided, validate its connection and serviceability.</li>
* </ol>
* If an error is detected in any step, an appropriate ValidationOutputData object
* is returned containing the error message and status code.
* Otherwise, a success response (HTTP status 200) is returned.
*
* @param payload the bulk input payload to validate
* @param jobID the job id used for contextualizing validation errors
* @return a populated ValidationOutputData object representing success or the first encountered validation error
* @throws Exception if an unexpected error occurs during validation
*/
public static ValidationOutputData validate(BulkInput payload, String jobID) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java doc for this and other methods would be helpful as well.


ValidationOutputData validationOutputData;

validationOutputData = buildErrorOutput(validateTimeRange(payload.getTime_range()), jobID);
if (validationOutputData != null) return validationOutputData;

if (payload.getDatasource() != null) {
validationOutputData = buildErrorOutput(validateDatasourceConnection(payload.getDatasource()), jobID);
}

if (validationOutputData == null) {
validationOutputData = new ValidationOutputData(true, "", 200);
}
return validationOutputData;
}

/**
* Builds an error output object if the given error message is non-empty.
* Utility method that appends the job ID to the message and wraps it into
* a ValidationOutputData object with HTTP 400 status.
*
* @param errorMsg the validation error message; may be empty or null
* @param jobID the job identifier appended for context
* @return a ValidationOutputData object if an error exists, otherwise null
*/
private static ValidationOutputData buildErrorOutput(String errorMsg, String jobID) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a java doc would be helpful

if (errorMsg != null && !errorMsg.isEmpty()) {
return new ValidationOutputData(false, errorMsg + " for the jobId: " + jobID, 400);
}
return null;
}


/**
* Validates the connectivity and serviceability of the given datasource.
* This method attempts to:
* <ol>
* <li>Load the datasource metadata from database.</li>
* <li>Verify reachability using the registered DataSourceOperatorImpl.</li>
* </ol>
* If any step fails or the datasource is not serviceable, an appropriate error message
* is returned. Otherwise, an empty string signifies a successful validation.
*
* @param datasourceName the name of the datasource to validate
* @return an error message if validation fails; otherwise an empty string
*/
public static String validateDatasourceConnection(String datasourceName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a java doc would be helpful

String errorMessage = "";
try {
DataSourceInfo dataSourceInfo = null;
try {
dataSourceInfo = new ExperimentDBService().loadDataSourceFromDBByName(datasourceName);
} catch (Exception e) {
errorMessage = String.format(KruizeConstants.DataSourceConstants.DataSourceMetadataErrorMsgs.LOAD_DATASOURCE_FROM_DB_ERROR, datasourceName, e.getMessage());
LOGGER.error(errorMessage);
return errorMessage;
}
LOGGER.info(KruizeConstants.DataSourceConstants.DataSourceInfoMsgs.VERIFYING_DATASOURCE_REACHABILITY, datasourceName);
DataSourceOperatorImpl op = DataSourceOperatorImpl.getInstance().getOperator(KruizeConstants.SupportedDatasources.PROMETHEUS);
if (dataSourceInfo == null || op.isServiceable(dataSourceInfo) == CommonUtils.DatasourceReachabilityStatus.NOT_REACHABLE) {
errorMessage = KruizeConstants.DataSourceConstants.DataSourceErrorMsgs.DATASOURCE_NOT_SERVICEABLE;
LOGGER.error(errorMessage);
}
} catch (Exception ex) {
errorMessage = ex.getMessage();
LOGGER.error(errorMessage);
}
return errorMessage;
}

/**
* Validates the time range provided in the bulk request.
* This method checks for:
* <ul>
* <li>Presence of time range (null or empty is allowed and considered valid)</li>
* <li>Correct ISO-8601 datetime format</li>
* <li>Start time that is not after end time</li>
* </ul>
* If validation fails, it returns a specific error message; otherwise returns an empty string.
*
* @param timeRange the time range object containing start and end timestamps
* @return an error message if validation fails; otherwise an empty string
*/
public static String validateTimeRange(BulkInput.TimeRange timeRange) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a java doc would be helpful

String errorMessage = "";
if (timeRange == null || timeRange.isEmpty()) {
LOGGER.debug("No time range specified");
return errorMessage;
}
try {
OffsetDateTime startTime = OffsetDateTime.parse(timeRange.getStart());
OffsetDateTime endTime = OffsetDateTime.parse(timeRange.getEnd());

if (startTime.isAfter(endTime)) {
errorMessage = KruizeConstants.KRUIZE_BULK_API.INVALID_START_TIME;
return errorMessage;
}

} catch (DateTimeParseException ex) {
errorMessage = KruizeConstants.KRUIZE_BULK_API.INVALID_DATE_FORMAT;
} catch (Exception e) {
errorMessage = KruizeConstants.KRUIZE_BULK_API.TIME_RANGE_EXCEPTION;
}
return errorMessage;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/autotune/utils/KruizeConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,16 @@ public static final class KRUIZE_BULK_API {
public static final String BULK_JOB_SAVE_ERROR = "Not able to save experiment due to {}";
public static final String BULK_JOB_LOAD_ERROR = "Not able to load bulk JOB {} due to {}";

// Validation error messages
public static final String DUPLICATE_REQ_ID_WITH_SAME_PAYLOAD = "Duplicate requestId found with different payload: %s";
public static final String MISSING_REQUEST_ID = "RequestId parameter is missing";
public static final String INVALID_REQUEST_ID = "Invalid requestId format. Must be 36-character alphanumeric";
public static final String INVALID_START_TIME = "Start time should be before end time";
public static final String INVALID_TIME_RANGE = "Time range must be between 24 hours and 15 days";
public static final String INVALID_DATE_FORMAT = "Invalid date format. Must follow ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)";
public static final String TIME_RANGE_EXCEPTION = "Exception occurred while validating the time range";



// TODO : Bulk API Create Experiments defaults
public static final CreateExperimentConfigBean CREATE_EXPERIMENT_CONFIG_BEAN;
Expand Down