diff --git a/build.gradle b/build.gradle index 936412e3e..8fe138e29 100644 --- a/build.gradle +++ b/build.gradle @@ -732,7 +732,9 @@ List jacocoExclusions = [ 'org.opensearch.ad.feature.CompositeRetriever.PageIterator', 'org.opensearch.ad.cluster.ADDataMigrator', 'org.opensearch.ad.AnomalyDetectorExtension', - 'org.opensearch.ad.transport.ADJobRunnerTransportAction*' + 'org.opensearch.ad.transport.ADJobRunnerTransportAction*', + 'org.opensearch.ad.AnomalyDetectorProfileRunner', + 'org.opensearch.ad.EntityProfileRunner' ] diff --git a/src/main/java/org/opensearch/ad/AnomalyDetectorExtension.java b/src/main/java/org/opensearch/ad/AnomalyDetectorExtension.java index ee3469bd0..ff69889ef 100644 --- a/src/main/java/org/opensearch/ad/AnomalyDetectorExtension.java +++ b/src/main/java/org/opensearch/ad/AnomalyDetectorExtension.java @@ -22,9 +22,10 @@ import org.opensearch.action.ActionResponse; import org.opensearch.action.support.TransportAction; import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; import org.opensearch.ad.model.AnomalyResult; import org.opensearch.ad.model.DetectorInternalState; -import org.opensearch.ad.rest.RestGetDetectorAction; +import org.opensearch.ad.rest.RestGetAnomalyDetectorAction; import org.opensearch.ad.rest.RestIndexAnomalyDetectorAction; import org.opensearch.ad.rest.RestValidateAnomalyDetectorAction; import org.opensearch.ad.settings.AnomalyDetectorSettings; @@ -60,7 +61,7 @@ public List getExtensionRestHandlers() { .of( new RestIndexAnomalyDetectorAction(extensionsRunner(), this), new RestValidateAnomalyDetectorAction(extensionsRunner(), this), - new RestGetDetectorAction() + new RestGetAnomalyDetectorAction(extensionsRunner(), this) ); } @@ -108,10 +109,13 @@ public List> getSettings() { @Override public List getNamedXContent() { // Copied from AnomalyDetectorPlugin getNamedXContent - return ImmutableList.of(AnomalyDetector.XCONTENT_REGISTRY, AnomalyResult.XCONTENT_REGISTRY, DetectorInternalState.XCONTENT_REGISTRY - // Pending Job Scheduler Integration - // AnomalyDetectorJob.XCONTENT_REGISTRY - ); + return ImmutableList + .of( + AnomalyDetector.XCONTENT_REGISTRY, + AnomalyResult.XCONTENT_REGISTRY, + DetectorInternalState.XCONTENT_REGISTRY, + AnomalyDetectorJob.XCONTENT_REGISTRY + ); } // TODO: replace or override client object on BaseExtension @@ -129,11 +133,7 @@ public OpenSearchClient getClient() { @Deprecated public SDKRestClient getRestClient() { @SuppressWarnings("resource") - SDKRestClient client = new SDKClient() - .initializeRestClient( - getExtensionSettings().getOpensearchAddress(), - Integer.parseInt(getExtensionSettings().getOpensearchPort()) - ); + SDKRestClient client = new SDKClient().initializeRestClient(getExtensionSettings()); return client; } diff --git a/src/main/java/org/opensearch/ad/AnomalyDetectorProfileRunner.java b/src/main/java/org/opensearch/ad/AnomalyDetectorProfileRunner.java index e7c12e6c9..2a469adcc 100644 --- a/src/main/java/org/opensearch/ad/AnomalyDetectorProfileRunner.java +++ b/src/main/java/org/opensearch/ad/AnomalyDetectorProfileRunner.java @@ -1,628 +1,626 @@ // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility -/// * -// * SPDX-License-Identifier: Apache-2.0 -// * -// * The OpenSearch Contributors require contributions made to -// * this file be licensed under the Apache-2.0 license or a -// * compatible open source license. -// * -// * Modifications Copyright OpenSearch Contributors. See -// * GitHub history for details. -// */ -// -// package org.opensearch.ad; -// -// import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG; -// import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_PARSE_DETECTOR_MSG; -// import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; -// import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; -// import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -// import static org.opensearch.rest.RestStatus.BAD_REQUEST; -// import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; -// -// import java.util.List; -// import java.util.Map; -// import java.util.Set; -// import java.util.stream.Collectors; -// -// import org.apache.logging.log4j.LogManager; -// import org.apache.logging.log4j.Logger; -// import org.apache.logging.log4j.core.util.Throwables; -// import org.apache.logging.log4j.message.ParameterizedMessage; -// import org.opensearch.OpenSearchStatusException; -// import org.opensearch.action.ActionListener; -// import org.opensearch.action.get.GetRequest; -// import org.opensearch.action.search.SearchRequest; -// import org.opensearch.action.search.SearchResponse; -// import org.opensearch.ad.common.exception.NotSerializedADExceptionName; -// import org.opensearch.ad.common.exception.ResourceNotFoundException; -// import org.opensearch.ad.constant.CommonErrorMessages; -// import org.opensearch.ad.constant.CommonName; -// import org.opensearch.ad.model.ADTaskType; -// import org.opensearch.ad.model.AnomalyDetector; -// import org.opensearch.ad.model.AnomalyDetectorJob; -// import org.opensearch.ad.model.AnomalyResult; -// import org.opensearch.ad.model.DetectorProfile; -// import org.opensearch.ad.model.DetectorProfileName; -// import org.opensearch.ad.model.DetectorState; -// import org.opensearch.ad.model.InitProgressProfile; -// import org.opensearch.ad.model.IntervalTimeConfiguration; -// import org.opensearch.ad.settings.AnomalyDetectorSettings; -// import org.opensearch.ad.settings.NumericSetting; -// import org.opensearch.ad.task.ADTaskManager; -// import org.opensearch.ad.transport.ProfileAction; -// import org.opensearch.ad.transport.ProfileRequest; -// import org.opensearch.ad.transport.ProfileResponse; -// import org.opensearch.ad.transport.RCFPollingAction; -// import org.opensearch.ad.transport.RCFPollingRequest; -// import org.opensearch.ad.transport.RCFPollingResponse; -// import org.opensearch.ad.util.DiscoveryNodeFilterer; -// import org.opensearch.ad.util.ExceptionUtil; -// import org.opensearch.ad.util.MultiResponsesDelegateActionListener; -// import org.opensearch.client.Client; -// import org.opensearch.cluster.node.DiscoveryNode; -// import org.opensearch.common.xcontent.LoggingDeprecationHandler; -// import org.opensearch.common.xcontent.NamedXContentRegistry; -// import org.opensearch.common.xcontent.XContentParser; -// import org.opensearch.common.xcontent.XContentType; -// import org.opensearch.index.query.BoolQueryBuilder; -// import org.opensearch.index.query.QueryBuilders; -// import org.opensearch.search.SearchHits; -// import org.opensearch.search.aggregations.Aggregation; -// import org.opensearch.search.aggregations.AggregationBuilder; -// import org.opensearch.search.aggregations.AggregationBuilders; -// import org.opensearch.search.aggregations.Aggregations; -// import org.opensearch.search.aggregations.bucket.composite.CompositeAggregation; -// import org.opensearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; -// import org.opensearch.search.aggregations.metrics.CardinalityAggregationBuilder; -// import org.opensearch.search.aggregations.metrics.InternalCardinality; -// import org.opensearch.search.builder.SearchSourceBuilder; -// import org.opensearch.transport.TransportService; -// -// public class AnomalyDetectorProfileRunner extends AbstractProfileRunner { -// private final Logger logger = LogManager.getLogger(AnomalyDetectorProfileRunner.class); -// private Client client; -// private NamedXContentRegistry xContentRegistry; -// private DiscoveryNodeFilterer nodeFilter; -// private final TransportService transportService; -// private final ADTaskManager adTaskManager; -// private final int maxTotalEntitiesToTrack; -// -// public AnomalyDetectorProfileRunner( -// Client client, -// NamedXContentRegistry xContentRegistry, -// DiscoveryNodeFilterer nodeFilter, -// long requiredSamples, -// TransportService transportService, -// ADTaskManager adTaskManager -// ) { -// super(requiredSamples); -// this.client = client; -// this.xContentRegistry = xContentRegistry; -// this.nodeFilter = nodeFilter; -// if (requiredSamples <= 0) { -// throw new IllegalArgumentException("required samples should be a positive number, but was " + requiredSamples); -// } -// this.transportService = transportService; -// this.adTaskManager = adTaskManager; -// this.maxTotalEntitiesToTrack = AnomalyDetectorSettings.MAX_TOTAL_ENTITIES_TO_TRACK; -// } -// -// public void profile(String detectorId, ActionListener listener, Set profilesToCollect) { -// if (profilesToCollect.isEmpty()) { -// listener.onFailure(new IllegalArgumentException(CommonErrorMessages.EMPTY_PROFILES_COLLECT)); -// return; -// } -// calculateTotalResponsesToWait(detectorId, profilesToCollect, listener); -// } -// -// private void calculateTotalResponsesToWait( -// String detectorId, -// Set profilesToCollect, -// ActionListener listener -// ) { -// GetRequest getDetectorRequest = new GetRequest(ANOMALY_DETECTORS_INDEX, detectorId); -// client.get(getDetectorRequest, ActionListener.wrap(getDetectorResponse -> { -// if (getDetectorResponse != null && getDetectorResponse.isExists()) { -// try ( -// XContentParser xContentParser = XContentType.JSON -// .xContent() -// .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getDetectorResponse.getSourceAsString()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, xContentParser.nextToken(), xContentParser); -// AnomalyDetector detector = AnomalyDetector.parse(xContentParser, detectorId); -// prepareProfile(detector, listener, profilesToCollect); -// } catch (Exception e) { -// logger.error(FAIL_TO_PARSE_DETECTOR_MSG + detectorId, e); -// listener.onFailure(new OpenSearchStatusException(FAIL_TO_PARSE_DETECTOR_MSG + detectorId, BAD_REQUEST)); -// } -// } else { -// listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, BAD_REQUEST)); -// } -// }, exception -> { -// logger.error(FAIL_TO_FIND_DETECTOR_MSG + detectorId, exception); -// listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, INTERNAL_SERVER_ERROR)); -// })); -// } -// -// private void prepareProfile( -// AnomalyDetector detector, -// ActionListener listener, -// Set profilesToCollect -// ) { -// String detectorId = detector.getDetectorId(); -// GetRequest getRequest = new GetRequest(ANOMALY_DETECTOR_JOB_INDEX, detectorId); -// client.get(getRequest, ActionListener.wrap(getResponse -> { -// if (getResponse != null && getResponse.isExists()) { -// try ( -// XContentParser parser = XContentType.JSON -// .xContent() -// .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); -// AnomalyDetectorJob job = AnomalyDetectorJob.parse(parser); -// long enabledTimeMs = job.getEnabledTime().toEpochMilli(); -// -// boolean isMultiEntityDetector = detector.isMultientityDetector(); -// -// int totalResponsesToWait = 0; -// if (profilesToCollect.contains(DetectorProfileName.ERROR)) { -// totalResponsesToWait++; -// } -// -// // total number of listeners we need to define. Needed by MultiResponsesDelegateActionListener to decide -// // when to consolidate results and return to users -// if (isMultiEntityDetector) { -// if (profilesToCollect.contains(DetectorProfileName.TOTAL_ENTITIES)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) -// || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) -// || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) -// || profilesToCollect.contains(DetectorProfileName.MODELS) -// || profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES) -// || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) -// || profilesToCollect.contains(DetectorProfileName.STATE)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { -// totalResponsesToWait++; -// } -// } else { -// if (profilesToCollect.contains(DetectorProfileName.STATE) -// || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) -// || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) -// || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) -// || profilesToCollect.contains(DetectorProfileName.MODELS)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { -// totalResponsesToWait++; -// } -// } -// -// MultiResponsesDelegateActionListener delegateListener = -// new MultiResponsesDelegateActionListener( -// listener, -// totalResponsesToWait, -// CommonErrorMessages.FAIL_FETCH_ERR_MSG + detectorId, -// false -// ); -// if (profilesToCollect.contains(DetectorProfileName.ERROR)) { -// adTaskManager.getAndExecuteOnLatestDetectorLevelTask(detectorId, ADTaskType.REALTIME_TASK_TYPES, adTask -> { -// DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); -// if (adTask.isPresent()) { -// long lastUpdateTimeMs = adTask.get().getLastUpdateTime().toEpochMilli(); -// -// // if state index hasn't been updated, we should not use the error field -// // For example, before a detector is enabled, if the error message contains -// // the phrase "stopped due to blah", we should not show this when the detector -// // is enabled. -// if (lastUpdateTimeMs > enabledTimeMs && adTask.get().getError() != null) { -// profileBuilder.error(adTask.get().getError()); -// } -// delegateListener.onResponse(profileBuilder.build()); -// } else { -// // detector state for this detector does not exist -// delegateListener.onResponse(profileBuilder.build()); -// } -// }, transportService, false, delegateListener); -// } -// -// // total number of listeners we need to define. Needed by MultiResponsesDelegateActionListener to decide -// // when to consolidate results and return to users -// if (isMultiEntityDetector) { -// if (profilesToCollect.contains(DetectorProfileName.TOTAL_ENTITIES)) { -// profileEntityStats(delegateListener, detector); -// } -// if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) -// || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) -// || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) -// || profilesToCollect.contains(DetectorProfileName.MODELS) -// || profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES) -// || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) -// || profilesToCollect.contains(DetectorProfileName.STATE)) { -// profileModels(detector, profilesToCollect, job, true, delegateListener); -// } -// if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { -// adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, null, delegateListener); -// } -// } else { -// if (profilesToCollect.contains(DetectorProfileName.STATE) -// || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { -// profileStateRelated(detector, delegateListener, job.isEnabled(), profilesToCollect); -// } -// if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) -// || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) -// || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) -// || profilesToCollect.contains(DetectorProfileName.MODELS)) { -// profileModels(detector, profilesToCollect, job, false, delegateListener); -// } -// if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { -// adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, null, delegateListener); -// } -// } -// -// } catch (Exception e) { -// logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG, e); -// listener.onFailure(e); -// } -// } else { -// onGetDetectorForPrepare(detectorId, listener, profilesToCollect); -// } -// }, exception -> { -// if (ExceptionUtil.isIndexNotAvailable(exception)) { -// logger.info(exception.getMessage()); -// onGetDetectorForPrepare(detectorId, listener, profilesToCollect); -// } else { -// logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG + detectorId); -// listener.onFailure(exception); -// } -// })); -// } -// -// private void profileEntityStats(MultiResponsesDelegateActionListener listener, AnomalyDetector detector) { -// List categoryField = detector.getCategoryField(); -// if (!detector.isMultientityDetector() || categoryField.size() > NumericSetting.maxCategoricalFields()) { -// listener.onResponse(new DetectorProfile.Builder().build()); -// } else { -// if (categoryField.size() == 1) { -// // Run a cardinality aggregation to count the cardinality of single category fields -// SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); -// CardinalityAggregationBuilder aggBuilder = new CardinalityAggregationBuilder(CommonName.TOTAL_ENTITIES); -// aggBuilder.field(categoryField.get(0)); -// searchSourceBuilder.aggregation(aggBuilder); -// -// SearchRequest request = new SearchRequest(detector.getIndices().toArray(new String[0]), searchSourceBuilder); -// client.search(request, ActionListener.wrap(searchResponse -> { -// Map aggMap = searchResponse.getAggregations().asMap(); -// InternalCardinality totalEntities = (InternalCardinality) aggMap.get(CommonName.TOTAL_ENTITIES); -// long value = totalEntities.getValue(); -// DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); -// DetectorProfile profile = profileBuilder.totalEntities(value).build(); -// listener.onResponse(profile); -// }, searchException -> { -// logger.warn(CommonErrorMessages.FAIL_TO_GET_TOTAL_ENTITIES + detector.getDetectorId()); -// listener.onFailure(searchException); -// })); -// } else { -// // Run a composite query and count the number of buckets to decide cardinality of multiple category fields -// AggregationBuilder bucketAggs = AggregationBuilders -// .composite( -// CommonName.TOTAL_ENTITIES, -// detector.getCategoryField().stream().map(f -> new TermsValuesSourceBuilder(f).field(f)).collect(Collectors.toList()) -// ) -// .size(maxTotalEntitiesToTrack); -// SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().aggregation(bucketAggs).trackTotalHits(false).size(0); -// SearchRequest searchRequest = new SearchRequest() -// .indices(detector.getIndices().toArray(new String[0])) -// .source(searchSourceBuilder); -// client.search(searchRequest, ActionListener.wrap(searchResponse -> { -// DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); -// Aggregations aggs = searchResponse.getAggregations(); -// if (aggs == null) { -// // This would indicate some bug or some opensearch core changes that we are not aware of (we don't keep up-to-date -// // with -// // the large amounts of changes there). For example, they may change to if there are results return it; otherwise -// // return -// // null instead of an empty Aggregations as they currently do. -// logger.warn("Unexpected null aggregation."); -// listener.onResponse(profileBuilder.totalEntities(0L).build()); -// return; -// } -// -// Aggregation aggrResult = aggs.get(CommonName.TOTAL_ENTITIES); -// if (aggrResult == null) { -// listener.onFailure(new IllegalArgumentException("Fail to find valid aggregation result")); -// return; -// } -// -// CompositeAggregation compositeAgg = (CompositeAggregation) aggrResult; -// DetectorProfile profile = profileBuilder.totalEntities(Long.valueOf(compositeAgg.getBuckets().size())).build(); -// listener.onResponse(profile); -// }, searchException -> { -// logger.warn(CommonErrorMessages.FAIL_TO_GET_TOTAL_ENTITIES + detector.getDetectorId()); -// listener.onFailure(searchException); -// })); -// } -// -// } -// } -// -// private void onGetDetectorForPrepare(String detectorId, ActionListener listener, Set profiles) { -// DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); -// if (profiles.contains(DetectorProfileName.STATE)) { -// profileBuilder.state(DetectorState.DISABLED); -// } -// if (profiles.contains(DetectorProfileName.AD_TASK)) { -// adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, profileBuilder.build(), listener); -// } else { -// listener.onResponse(profileBuilder.build()); -// } -// } -// -// /** -// * We expect three kinds of states: -// * -Disabled: if get ad job api says the job is disabled; -// * -Init: if rcf model's total updates is less than required -// * -Running: if neither of the above applies and no exceptions. -// * @param detector anomaly detector -// * @param listener listener to process the returned state or exception -// * @param enabled whether the detector job is enabled or not -// * @param profilesToCollect target profiles to fetch -// */ -// private void profileStateRelated( -// AnomalyDetector detector, -// MultiResponsesDelegateActionListener listener, -// boolean enabled, -// Set profilesToCollect -// ) { -// if (enabled) { -// RCFPollingRequest request = new RCFPollingRequest(detector.getDetectorId()); -// client.execute(RCFPollingAction.INSTANCE, request, onPollRCFUpdates(detector, profilesToCollect, listener)); -// } else { -// DetectorProfile.Builder builder = new DetectorProfile.Builder(); -// if (profilesToCollect.contains(DetectorProfileName.STATE)) { -// builder.state(DetectorState.DISABLED); -// } -// listener.onResponse(builder.build()); -// } -// } -// -// private void profileModels( -// AnomalyDetector detector, -// Set profiles, -// AnomalyDetectorJob job, -// boolean forMultiEntityDetector, -// MultiResponsesDelegateActionListener listener -// ) { -// DiscoveryNode[] dataNodes = nodeFilter.getEligibleDataNodes(); -// ProfileRequest profileRequest = new ProfileRequest(detector.getDetectorId(), profiles, forMultiEntityDetector, dataNodes); -// client.execute(ProfileAction.INSTANCE, profileRequest, onModelResponse(detector, profiles, job, listener));// get init progress -// } -// -// private ActionListener onModelResponse( -// AnomalyDetector detector, -// Set profilesToCollect, -// AnomalyDetectorJob job, -// MultiResponsesDelegateActionListener listener -// ) { -// boolean isMultientityDetector = detector.isMultientityDetector(); -// return ActionListener.wrap(profileResponse -> { -// DetectorProfile.Builder profile = new DetectorProfile.Builder(); -// if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE)) { -// profile.coordinatingNode(profileResponse.getCoordinatingNode()); -// } -// if (profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE)) { -// profile.shingleSize(profileResponse.getShingleSize()); -// } -// if (profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES)) { -// profile.totalSizeInBytes(profileResponse.getTotalSizeInBytes()); -// } -// if (profilesToCollect.contains(DetectorProfileName.MODELS)) { -// profile.modelProfile(profileResponse.getModelProfile()); -// profile.modelCount(profileResponse.getModelCount()); -// } -// if (isMultientityDetector && profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES)) { -// profile.activeEntities(profileResponse.getActiveEntities()); -// } -// -// if (isMultientityDetector -// && (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) -// || profilesToCollect.contains(DetectorProfileName.STATE))) { -// profileMultiEntityDetectorStateRelated(job, profilesToCollect, profileResponse, profile, detector, listener); -// } else { -// listener.onResponse(profile.build()); -// } -// }, listener::onFailure); -// } -// -// private void profileMultiEntityDetectorStateRelated( -// AnomalyDetectorJob job, -// Set profilesToCollect, -// ProfileResponse profileResponse, -// DetectorProfile.Builder profileBuilder, -// AnomalyDetector detector, -// MultiResponsesDelegateActionListener listener -// ) { -// if (job.isEnabled()) { -// if (profileResponse.getTotalUpdates() < requiredSamples) { -// // need to double check since what ProfileResponse returns is the highest priority entity currently in memory, but -// // another entity might have already been initialized and sit somewhere else (in memory or on disk). -// confirmMultiEntityDetectorInitStatus( -// detector, -// job.getEnabledTime().toEpochMilli(), -// profileBuilder, -// profilesToCollect, -// profileResponse.getTotalUpdates(), -// listener -// ); -// } else { -// createRunningStateAndInitProgress(profilesToCollect, profileBuilder); -// listener.onResponse(profileBuilder.build()); -// } -// } else { -// if (profilesToCollect.contains(DetectorProfileName.STATE)) { -// profileBuilder.state(DetectorState.DISABLED); -// } -// listener.onResponse(profileBuilder.build()); -// } -// } -// -// private void confirmMultiEntityDetectorInitStatus( -// AnomalyDetector detector, -// long enabledTime, -// DetectorProfile.Builder profile, -// Set profilesToCollect, -// long totalUpdates, -// MultiResponsesDelegateActionListener listener -// ) { -// SearchRequest searchLatestResult = createInittedEverRequest(detector.getDetectorId(), enabledTime, detector.getResultIndex()); -// client.search(searchLatestResult, onInittedEver(enabledTime, profile, profilesToCollect, detector, totalUpdates, listener)); -// } -// -// private ActionListener onInittedEver( -// long lastUpdateTimeMs, -// DetectorProfile.Builder profileBuilder, -// Set profilesToCollect, -// AnomalyDetector detector, -// long totalUpdates, -// MultiResponsesDelegateActionListener listener -// ) { -// return ActionListener.wrap(searchResponse -> { -// SearchHits hits = searchResponse.getHits(); -// if (hits.getTotalHits().value == 0L) { -// processInitResponse(detector, profilesToCollect, totalUpdates, false, profileBuilder, listener); -// } else { -// createRunningStateAndInitProgress(profilesToCollect, profileBuilder); -// listener.onResponse(profileBuilder.build()); -// } -// }, exception -> { -// if (ExceptionUtil.isIndexNotAvailable(exception)) { -// // anomaly result index is not created yet -// processInitResponse(detector, profilesToCollect, totalUpdates, false, profileBuilder, listener); -// } else { -// logger -// .error( -// "Fail to find any anomaly result with anomaly score larger than 0 after AD job enabled time for detector {}", -// detector.getDetectorId() -// ); -// listener.onFailure(exception); -// } -// }); -// } -// -// /** -// * Listener for polling rcf updates through transport messaging -// * @param detector anomaly detector -// * @param profilesToCollect profiles to collect like state -// * @param listener delegate listener -// * @return Listener for polling rcf updates through transport messaging -// */ -// private ActionListener onPollRCFUpdates( -// AnomalyDetector detector, -// Set profilesToCollect, -// MultiResponsesDelegateActionListener listener -// ) { -// return ActionListener.wrap(rcfPollResponse -> { -// long totalUpdates = rcfPollResponse.getTotalUpdates(); -// if (totalUpdates < requiredSamples) { -// processInitResponse(detector, profilesToCollect, totalUpdates, false, new DetectorProfile.Builder(), listener); -// } else { -// DetectorProfile.Builder builder = new DetectorProfile.Builder(); -// createRunningStateAndInitProgress(profilesToCollect, builder); -// listener.onResponse(builder.build()); -// } -// }, exception -> { -// // we will get an AnomalyDetectionException wrapping the real exception inside -// Throwable cause = Throwables.getRootCause(exception); -// -// // exception can be a RemoteTransportException -// Exception causeException = (Exception) cause; -// if (ExceptionUtil -// .isException( -// causeException, -// ResourceNotFoundException.class, -// NotSerializedADExceptionName.RESOURCE_NOT_FOUND_EXCEPTION_NAME_UNDERSCORE.getName() -// ) -// || (ExceptionUtil.isIndexNotAvailable(causeException) -// && causeException.getMessage().contains(CommonName.CHECKPOINT_INDEX_NAME))) { -// // cannot find checkpoint -// // We don't want to show the estimated time remaining to initialize -// // a detector before cold start finishes, where the actual -// // initialization time may be much shorter if sufficient historical -// // data exists. -// processInitResponse(detector, profilesToCollect, 0L, true, new DetectorProfile.Builder(), listener); -// } else { -// logger -// .error( -// new ParameterizedMessage("Fail to get init progress through messaging for {}", detector.getDetectorId()), -// exception -// ); -// listener.onFailure(exception); -// } -// }); -// } -// -// private void createRunningStateAndInitProgress(Set profilesToCollect, DetectorProfile.Builder builder) { -// if (profilesToCollect.contains(DetectorProfileName.STATE)) { -// builder.state(DetectorState.RUNNING).build(); -// } -// -// if (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { -// InitProgressProfile initProgress = new InitProgressProfile("100%", 0, 0); -// builder.initProgress(initProgress); -// } -// } -// -// private void processInitResponse( -// AnomalyDetector detector, -// Set profilesToCollect, -// long totalUpdates, -// boolean hideMinutesLeft, -// DetectorProfile.Builder builder, -// MultiResponsesDelegateActionListener listener -// ) { -// if (profilesToCollect.contains(DetectorProfileName.STATE)) { -// builder.state(DetectorState.INIT); -// } -// -// if (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { -// if (hideMinutesLeft) { -// InitProgressProfile initProgress = computeInitProgressProfile(totalUpdates, 0); -// builder.initProgress(initProgress); -// } else { -// long intervalMins = ((IntervalTimeConfiguration) detector.getDetectionInterval()).toDuration().toMinutes(); -// InitProgressProfile initProgress = computeInitProgressProfile(totalUpdates, intervalMins); -// builder.initProgress(initProgress); -// } -// } -// -// listener.onResponse(builder.build()); -// } -// -// /** -// * Create search request to check if we have at least 1 anomaly score larger than 0 after AD job enabled time -// * @param detectorId detector id -// * @param enabledTime the time when AD job is enabled in milliseconds -// * @return the search request -// */ -// private SearchRequest createInittedEverRequest(String detectorId, long enabledTime, String resultIndex) { -// BoolQueryBuilder filterQuery = new BoolQueryBuilder(); -// filterQuery.filter(QueryBuilders.termQuery(AnomalyResult.DETECTOR_ID_FIELD, detectorId)); -// filterQuery.filter(QueryBuilders.rangeQuery(AnomalyResult.EXECUTION_END_TIME_FIELD).gte(enabledTime)); -// filterQuery.filter(QueryBuilders.rangeQuery(AnomalyResult.ANOMALY_SCORE_FIELD).gt(0)); -// -// SearchSourceBuilder source = new SearchSourceBuilder().query(filterQuery).size(1); -// -// SearchRequest request = new SearchRequest(CommonName.ANOMALY_RESULT_INDEX_ALIAS); -// request.source(source); -// if (resultIndex != null) { -// request.indices(resultIndex); -// } -// return request; -// } -// } +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.ad; + +import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG; +import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_PARSE_DETECTOR_MSG; +import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; +import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.opensearch.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.util.Throwables; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.opensearch.OpenSearchStatusException; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.ad.common.exception.NotSerializedADExceptionName; +import org.opensearch.ad.common.exception.ResourceNotFoundException; +import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; +import org.opensearch.ad.model.ADTaskType; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; +import org.opensearch.ad.model.AnomalyResult; +import org.opensearch.ad.model.DetectorProfile; +import org.opensearch.ad.model.DetectorProfileName; +import org.opensearch.ad.model.DetectorState; +import org.opensearch.ad.model.InitProgressProfile; +import org.opensearch.ad.model.IntervalTimeConfiguration; +import org.opensearch.ad.settings.AnomalyDetectorSettings; +import org.opensearch.ad.settings.NumericSetting; +import org.opensearch.ad.task.ADTaskManager; +import org.opensearch.ad.transport.ProfileRequest; +import org.opensearch.ad.transport.ProfileResponse; +import org.opensearch.ad.transport.RCFPollingRequest; +import org.opensearch.ad.transport.RCFPollingResponse; +import org.opensearch.ad.util.DiscoveryNodeFilterer; +import org.opensearch.ad.util.ExceptionUtil; +import org.opensearch.ad.util.MultiResponsesDelegateActionListener; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.search.SearchHits; +import org.opensearch.search.aggregations.Aggregation; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.AggregationBuilders; +import org.opensearch.search.aggregations.Aggregations; +import org.opensearch.search.aggregations.bucket.composite.CompositeAggregation; +import org.opensearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; +import org.opensearch.search.aggregations.metrics.CardinalityAggregationBuilder; +import org.opensearch.search.aggregations.metrics.InternalCardinality; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.transport.TransportService; + +public class AnomalyDetectorProfileRunner extends AbstractProfileRunner { + private final Logger logger = LogManager.getLogger(AnomalyDetectorProfileRunner.class); + private SDKRestClient client; + private NamedXContentRegistry xContentRegistry; + private DiscoveryNodeFilterer nodeFilter; + private final TransportService transportService; + private final ADTaskManager adTaskManager; + private final int maxTotalEntitiesToTrack; + + public AnomalyDetectorProfileRunner( + SDKRestClient client, + NamedXContentRegistry xContentRegistry, + DiscoveryNodeFilterer nodeFilter, + long requiredSamples, + TransportService transportService, + ADTaskManager adTaskManager + ) { + super(requiredSamples); + this.client = client; + this.xContentRegistry = xContentRegistry; + this.nodeFilter = nodeFilter; + if (requiredSamples <= 0) { + throw new IllegalArgumentException("required samples should be a positive number, but was " + requiredSamples); + } + this.transportService = transportService; + this.adTaskManager = adTaskManager; + this.maxTotalEntitiesToTrack = AnomalyDetectorSettings.MAX_TOTAL_ENTITIES_TO_TRACK; + } + + public void profile(String detectorId, ActionListener listener, Set profilesToCollect) { + if (profilesToCollect.isEmpty()) { + listener.onFailure(new IllegalArgumentException(CommonErrorMessages.EMPTY_PROFILES_COLLECT)); + return; + } + calculateTotalResponsesToWait(detectorId, profilesToCollect, listener); + } + + private void calculateTotalResponsesToWait( + String detectorId, + Set profilesToCollect, + ActionListener listener + ) { + GetRequest getDetectorRequest = new GetRequest(ANOMALY_DETECTORS_INDEX, detectorId); + client.get(getDetectorRequest, ActionListener.wrap(getDetectorResponse -> { + if (getDetectorResponse != null && getDetectorResponse.isExists()) { + try ( + XContentParser xContentParser = XContentType.JSON + .xContent() + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getDetectorResponse.getSourceAsString()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, xContentParser.nextToken(), xContentParser); + AnomalyDetector detector = AnomalyDetector.parse(xContentParser, detectorId); + prepareProfile(detector, listener, profilesToCollect); + } catch (Exception e) { + logger.error(FAIL_TO_PARSE_DETECTOR_MSG + detectorId, e); + listener.onFailure(new OpenSearchStatusException(FAIL_TO_PARSE_DETECTOR_MSG + detectorId, BAD_REQUEST)); + } + } else { + listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, BAD_REQUEST)); + } + }, exception -> { + logger.error(FAIL_TO_FIND_DETECTOR_MSG + detectorId, exception); + listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, INTERNAL_SERVER_ERROR)); + })); + } + + private void prepareProfile( + AnomalyDetector detector, + ActionListener listener, + Set profilesToCollect + ) { + String detectorId = detector.getDetectorId(); + GetRequest getRequest = new GetRequest(ANOMALY_DETECTOR_JOB_INDEX, detectorId); + client.get(getRequest, ActionListener.wrap(getResponse -> { + if (getResponse != null && getResponse.isExists()) { + try ( + XContentParser parser = XContentType.JSON + .xContent() + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + AnomalyDetectorJob job = AnomalyDetectorJob.parse(parser); + long enabledTimeMs = job.getEnabledTime().toEpochMilli(); + + boolean isMultiEntityDetector = detector.isMultientityDetector(); + + int totalResponsesToWait = 0; + if (profilesToCollect.contains(DetectorProfileName.ERROR)) { + totalResponsesToWait++; + } + + // total number of listeners we need to define. Needed by MultiResponsesDelegateActionListener to decide + // when to consolidate results and return to users + if (isMultiEntityDetector) { + if (profilesToCollect.contains(DetectorProfileName.TOTAL_ENTITIES)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) + || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) + || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) + || profilesToCollect.contains(DetectorProfileName.MODELS) + || profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES) + || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) + || profilesToCollect.contains(DetectorProfileName.STATE)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { + totalResponsesToWait++; + } + } else { + if (profilesToCollect.contains(DetectorProfileName.STATE) + || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) + || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) + || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) + || profilesToCollect.contains(DetectorProfileName.MODELS)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { + totalResponsesToWait++; + } + } + + MultiResponsesDelegateActionListener delegateListener = + new MultiResponsesDelegateActionListener( + listener, + totalResponsesToWait, + CommonErrorMessages.FAIL_FETCH_ERR_MSG + detectorId, + false + ); + if (profilesToCollect.contains(DetectorProfileName.ERROR)) { + adTaskManager.getAndExecuteOnLatestDetectorLevelTask(detectorId, ADTaskType.REALTIME_TASK_TYPES, adTask -> { + DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); + if (adTask.isPresent()) { + long lastUpdateTimeMs = adTask.get().getLastUpdateTime().toEpochMilli(); + + // if state index hasn't been updated, we should not use the error field + // For example, before a detector is enabled, if the error message contains + // the phrase "stopped due to blah", we should not show this when the detector + // is enabled. + if (lastUpdateTimeMs > enabledTimeMs && adTask.get().getError() != null) { + profileBuilder.error(adTask.get().getError()); + } + delegateListener.onResponse(profileBuilder.build()); + } else { + // detector state for this detector does not exist + delegateListener.onResponse(profileBuilder.build()); + } + }, transportService, false, delegateListener); + } + + // total number of listeners we need to define. Needed by MultiResponsesDelegateActionListener to decide + // when to consolidate results and return to users + if (isMultiEntityDetector) { + if (profilesToCollect.contains(DetectorProfileName.TOTAL_ENTITIES)) { + profileEntityStats(delegateListener, detector); + } + if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) + || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) + || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) + || profilesToCollect.contains(DetectorProfileName.MODELS) + || profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES) + || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) + || profilesToCollect.contains(DetectorProfileName.STATE)) { + profileModels(detector, profilesToCollect, job, true, delegateListener); + } + if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { + adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, null, delegateListener); + } + } else { + if (profilesToCollect.contains(DetectorProfileName.STATE) + || profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { + profileStateRelated(detector, delegateListener, job.isEnabled(), profilesToCollect); + } + if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE) + || profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE) + || profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES) + || profilesToCollect.contains(DetectorProfileName.MODELS)) { + profileModels(detector, profilesToCollect, job, false, delegateListener); + } + if (profilesToCollect.contains(DetectorProfileName.AD_TASK)) { + adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, null, delegateListener); + } + } + + } catch (Exception e) { + logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG, e); + listener.onFailure(e); + } + } else { + onGetDetectorForPrepare(detectorId, listener, profilesToCollect); + } + }, exception -> { + if (ExceptionUtil.isIndexNotAvailable(exception)) { + logger.info(exception.getMessage()); + onGetDetectorForPrepare(detectorId, listener, profilesToCollect); + } else { + logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG + detectorId); + listener.onFailure(exception); + } + })); + } + + private void profileEntityStats(MultiResponsesDelegateActionListener listener, AnomalyDetector detector) { + List categoryField = detector.getCategoryField(); + if (!detector.isMultientityDetector() || categoryField.size() > NumericSetting.maxCategoricalFields()) { + listener.onResponse(new DetectorProfile.Builder().build()); + } else { + if (categoryField.size() == 1) { + // Run a cardinality aggregation to count the cardinality of single category fields + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + CardinalityAggregationBuilder aggBuilder = new CardinalityAggregationBuilder(CommonName.TOTAL_ENTITIES); + aggBuilder.field(categoryField.get(0)); + searchSourceBuilder.aggregation(aggBuilder); + + SearchRequest request = new SearchRequest(detector.getIndices().toArray(new String[0]), searchSourceBuilder); + client.search(request, ActionListener.wrap(searchResponse -> { + Map aggMap = searchResponse.getAggregations().asMap(); + InternalCardinality totalEntities = (InternalCardinality) aggMap.get(CommonName.TOTAL_ENTITIES); + long value = totalEntities.getValue(); + DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); + DetectorProfile profile = profileBuilder.totalEntities(value).build(); + listener.onResponse(profile); + }, searchException -> { + logger.warn(CommonErrorMessages.FAIL_TO_GET_TOTAL_ENTITIES + detector.getDetectorId()); + listener.onFailure(searchException); + })); + } else { + // Run a composite query and count the number of buckets to decide cardinality of multiple category fields + AggregationBuilder bucketAggs = AggregationBuilders + .composite( + CommonName.TOTAL_ENTITIES, + detector.getCategoryField().stream().map(f -> new TermsValuesSourceBuilder(f).field(f)).collect(Collectors.toList()) + ) + .size(maxTotalEntitiesToTrack); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().aggregation(bucketAggs).trackTotalHits(false).size(0); + SearchRequest searchRequest = new SearchRequest() + .indices(detector.getIndices().toArray(new String[0])) + .source(searchSourceBuilder); + client.search(searchRequest, ActionListener.wrap(searchResponse -> { + DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); + Aggregations aggs = searchResponse.getAggregations(); + if (aggs == null) { + // This would indicate some bug or some opensearch core changes that we are not aware of (we don't keep up-to-date + // with + // the large amounts of changes there). For example, they may change to if there are results return it; otherwise + // return + // null instead of an empty Aggregations as they currently do. + logger.warn("Unexpected null aggregation."); + listener.onResponse(profileBuilder.totalEntities(0L).build()); + return; + } + + Aggregation aggrResult = aggs.get(CommonName.TOTAL_ENTITIES); + if (aggrResult == null) { + listener.onFailure(new IllegalArgumentException("Fail to find valid aggregation result")); + return; + } + + CompositeAggregation compositeAgg = (CompositeAggregation) aggrResult; + DetectorProfile profile = profileBuilder.totalEntities(Long.valueOf(compositeAgg.getBuckets().size())).build(); + listener.onResponse(profile); + }, searchException -> { + logger.warn(CommonErrorMessages.FAIL_TO_GET_TOTAL_ENTITIES + detector.getDetectorId()); + listener.onFailure(searchException); + })); + } + + } + } + + private void onGetDetectorForPrepare(String detectorId, ActionListener listener, Set profiles) { + DetectorProfile.Builder profileBuilder = new DetectorProfile.Builder(); + if (profiles.contains(DetectorProfileName.STATE)) { + profileBuilder.state(DetectorState.DISABLED); + } + if (profiles.contains(DetectorProfileName.AD_TASK)) { + adTaskManager.getLatestHistoricalTaskProfile(detectorId, transportService, profileBuilder.build(), listener); + } else { + listener.onResponse(profileBuilder.build()); + } + } + + /** + * We expect three kinds of states: + * -Disabled: if get ad job api says the job is disabled; + * -Init: if rcf model's total updates is less than required + * -Running: if neither of the above applies and no exceptions. + * @param detector anomaly detector + * @param listener listener to process the returned state or exception + * @param enabled whether the detector job is enabled or not + * @param profilesToCollect target profiles to fetch + */ + private void profileStateRelated( + AnomalyDetector detector, + MultiResponsesDelegateActionListener listener, + boolean enabled, + Set profilesToCollect + ) { + if (enabled) { + RCFPollingRequest request = new RCFPollingRequest(detector.getDetectorId()); + // client.execute(RCFPollingAction.INSTANCE, request, onPollRCFUpdates(detector, profilesToCollect, listener)); + } else { + DetectorProfile.Builder builder = new DetectorProfile.Builder(); + if (profilesToCollect.contains(DetectorProfileName.STATE)) { + builder.state(DetectorState.DISABLED); + } + listener.onResponse(builder.build()); + } + } + + private void profileModels( + AnomalyDetector detector, + Set profiles, + AnomalyDetectorJob job, + boolean forMultiEntityDetector, + MultiResponsesDelegateActionListener listener + ) { + DiscoveryNode[] dataNodes = nodeFilter.getEligibleDataNodes(); + ProfileRequest profileRequest = new ProfileRequest(detector.getDetectorId(), profiles, forMultiEntityDetector, dataNodes); + // client.execute(ProfileAction.INSTANCE, profileRequest, onModelResponse(detector, profiles, job, listener));// get init progress + } + + private ActionListener onModelResponse( + AnomalyDetector detector, + Set profilesToCollect, + AnomalyDetectorJob job, + MultiResponsesDelegateActionListener listener + ) { + boolean isMultientityDetector = detector.isMultientityDetector(); + return ActionListener.wrap(profileResponse -> { + DetectorProfile.Builder profile = new DetectorProfile.Builder(); + if (profilesToCollect.contains(DetectorProfileName.COORDINATING_NODE)) { + profile.coordinatingNode(profileResponse.getCoordinatingNode()); + } + if (profilesToCollect.contains(DetectorProfileName.SHINGLE_SIZE)) { + profile.shingleSize(profileResponse.getShingleSize()); + } + if (profilesToCollect.contains(DetectorProfileName.TOTAL_SIZE_IN_BYTES)) { + profile.totalSizeInBytes(profileResponse.getTotalSizeInBytes()); + } + if (profilesToCollect.contains(DetectorProfileName.MODELS)) { + profile.modelProfile(profileResponse.getModelProfile()); + profile.modelCount(profileResponse.getModelCount()); + } + if (isMultientityDetector && profilesToCollect.contains(DetectorProfileName.ACTIVE_ENTITIES)) { + profile.activeEntities(profileResponse.getActiveEntities()); + } + + if (isMultientityDetector + && (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS) + || profilesToCollect.contains(DetectorProfileName.STATE))) { + profileMultiEntityDetectorStateRelated(job, profilesToCollect, profileResponse, profile, detector, listener); + } else { + listener.onResponse(profile.build()); + } + }, listener::onFailure); + } + + private void profileMultiEntityDetectorStateRelated( + AnomalyDetectorJob job, + Set profilesToCollect, + ProfileResponse profileResponse, + DetectorProfile.Builder profileBuilder, + AnomalyDetector detector, + MultiResponsesDelegateActionListener listener + ) { + if (job.isEnabled()) { + if (profileResponse.getTotalUpdates() < requiredSamples) { + // need to double check since what ProfileResponse returns is the highest priority entity currently in memory, but + // another entity might have already been initialized and sit somewhere else (in memory or on disk). + confirmMultiEntityDetectorInitStatus( + detector, + job.getEnabledTime().toEpochMilli(), + profileBuilder, + profilesToCollect, + profileResponse.getTotalUpdates(), + listener + ); + } else { + createRunningStateAndInitProgress(profilesToCollect, profileBuilder); + listener.onResponse(profileBuilder.build()); + } + } else { + if (profilesToCollect.contains(DetectorProfileName.STATE)) { + profileBuilder.state(DetectorState.DISABLED); + } + listener.onResponse(profileBuilder.build()); + } + } + + private void confirmMultiEntityDetectorInitStatus( + AnomalyDetector detector, + long enabledTime, + DetectorProfile.Builder profile, + Set profilesToCollect, + long totalUpdates, + MultiResponsesDelegateActionListener listener + ) { + SearchRequest searchLatestResult = createInittedEverRequest(detector.getDetectorId(), enabledTime, detector.getResultIndex()); + client.search(searchLatestResult, onInittedEver(enabledTime, profile, profilesToCollect, detector, totalUpdates, listener)); + } + + private ActionListener onInittedEver( + long lastUpdateTimeMs, + DetectorProfile.Builder profileBuilder, + Set profilesToCollect, + AnomalyDetector detector, + long totalUpdates, + MultiResponsesDelegateActionListener listener + ) { + return ActionListener.wrap(searchResponse -> { + SearchHits hits = searchResponse.getHits(); + if (hits.getTotalHits().value == 0L) { + processInitResponse(detector, profilesToCollect, totalUpdates, false, profileBuilder, listener); + } else { + createRunningStateAndInitProgress(profilesToCollect, profileBuilder); + listener.onResponse(profileBuilder.build()); + } + }, exception -> { + if (ExceptionUtil.isIndexNotAvailable(exception)) { + // anomaly result index is not created yet + processInitResponse(detector, profilesToCollect, totalUpdates, false, profileBuilder, listener); + } else { + logger + .error( + "Fail to find any anomaly result with anomaly score larger than 0 after AD job enabled time for detector {}", + detector.getDetectorId() + ); + listener.onFailure(exception); + } + }); + } + + /** + * Listener for polling rcf updates through transport messaging + * @param detector anomaly detector + * @param profilesToCollect profiles to collect like state + * @param listener delegate listener + * @return Listener for polling rcf updates through transport messaging + */ + private ActionListener onPollRCFUpdates( + AnomalyDetector detector, + Set profilesToCollect, + MultiResponsesDelegateActionListener listener + ) { + return ActionListener.wrap(rcfPollResponse -> { + long totalUpdates = rcfPollResponse.getTotalUpdates(); + if (totalUpdates < requiredSamples) { + processInitResponse(detector, profilesToCollect, totalUpdates, false, new DetectorProfile.Builder(), listener); + } else { + DetectorProfile.Builder builder = new DetectorProfile.Builder(); + createRunningStateAndInitProgress(profilesToCollect, builder); + listener.onResponse(builder.build()); + } + }, exception -> { + // we will get an AnomalyDetectionException wrapping the real exception inside + Throwable cause = Throwables.getRootCause(exception); + + // exception can be a RemoteTransportException + Exception causeException = (Exception) cause; + if (ExceptionUtil + .isException( + causeException, + ResourceNotFoundException.class, + NotSerializedADExceptionName.RESOURCE_NOT_FOUND_EXCEPTION_NAME_UNDERSCORE.getName() + ) + || (ExceptionUtil.isIndexNotAvailable(causeException) + && causeException.getMessage().contains(CommonName.CHECKPOINT_INDEX_NAME))) { + // cannot find checkpoint + // We don't want to show the estimated time remaining to initialize + // a detector before cold start finishes, where the actual + // initialization time may be much shorter if sufficient historical + // data exists. + processInitResponse(detector, profilesToCollect, 0L, true, new DetectorProfile.Builder(), listener); + } else { + logger + .error( + new ParameterizedMessage("Fail to get init progress through messaging for {}", detector.getDetectorId()), + exception + ); + listener.onFailure(exception); + } + }); + } + + private void createRunningStateAndInitProgress(Set profilesToCollect, DetectorProfile.Builder builder) { + if (profilesToCollect.contains(DetectorProfileName.STATE)) { + builder.state(DetectorState.RUNNING).build(); + } + + if (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { + InitProgressProfile initProgress = new InitProgressProfile("100%", 0, 0); + builder.initProgress(initProgress); + } + } + + private void processInitResponse( + AnomalyDetector detector, + Set profilesToCollect, + long totalUpdates, + boolean hideMinutesLeft, + DetectorProfile.Builder builder, + MultiResponsesDelegateActionListener listener + ) { + if (profilesToCollect.contains(DetectorProfileName.STATE)) { + builder.state(DetectorState.INIT); + } + + if (profilesToCollect.contains(DetectorProfileName.INIT_PROGRESS)) { + if (hideMinutesLeft) { + InitProgressProfile initProgress = computeInitProgressProfile(totalUpdates, 0); + builder.initProgress(initProgress); + } else { + long intervalMins = ((IntervalTimeConfiguration) detector.getDetectionInterval()).toDuration().toMinutes(); + InitProgressProfile initProgress = computeInitProgressProfile(totalUpdates, intervalMins); + builder.initProgress(initProgress); + } + } + + listener.onResponse(builder.build()); + } + + /** + * Create search request to check if we have at least 1 anomaly score larger than 0 after AD job enabled time + * @param detectorId detector id + * @param enabledTime the time when AD job is enabled in milliseconds + * @return the search request + */ + private SearchRequest createInittedEverRequest(String detectorId, long enabledTime, String resultIndex) { + BoolQueryBuilder filterQuery = new BoolQueryBuilder(); + filterQuery.filter(QueryBuilders.termQuery(AnomalyResult.DETECTOR_ID_FIELD, detectorId)); + filterQuery.filter(QueryBuilders.rangeQuery(AnomalyResult.EXECUTION_END_TIME_FIELD).gte(enabledTime)); + filterQuery.filter(QueryBuilders.rangeQuery(AnomalyResult.ANOMALY_SCORE_FIELD).gt(0)); + + SearchSourceBuilder source = new SearchSourceBuilder().query(filterQuery).size(1); + + SearchRequest request = new SearchRequest(CommonName.ANOMALY_RESULT_INDEX_ALIAS); + request.source(source); + if (resultIndex != null) { + request.indices(resultIndex); + } + return request; + } +} diff --git a/src/main/java/org/opensearch/ad/EntityProfileRunner.java b/src/main/java/org/opensearch/ad/EntityProfileRunner.java index 1438eb013..b194c42f9 100644 --- a/src/main/java/org/opensearch/ad/EntityProfileRunner.java +++ b/src/main/java/org/opensearch/ad/EntityProfileRunner.java @@ -1,463 +1,462 @@ -// @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility -/// * -// * SPDX-License-Identifier: Apache-2.0 -// * -// * The OpenSearch Contributors require contributions made to -// * this file be licensed under the Apache-2.0 license or a -// * compatible open source license. -// * -// * Modifications Copyright OpenSearch Contributors. See -// * GitHub history for details. -// */ -// -// package org.opensearch.ad; -// -// import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; -// import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; -// import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -// -// import java.util.List; -// import java.util.Map; -// import java.util.Optional; -// import java.util.Set; -// -// import org.apache.logging.log4j.LogManager; -// import org.apache.logging.log4j.Logger; -// import org.apache.lucene.search.join.ScoreMode; -// import org.opensearch.action.ActionListener; -// import org.opensearch.action.get.GetRequest; -// import org.opensearch.action.search.SearchRequest; -// import org.opensearch.ad.constant.CommonErrorMessages; -// import org.opensearch.ad.constant.CommonName; -// import org.opensearch.ad.model.AnomalyDetector; -// import org.opensearch.ad.model.AnomalyDetectorJob; -// import org.opensearch.ad.model.AnomalyResult; -// import org.opensearch.ad.model.Entity; -// import org.opensearch.ad.model.EntityProfile; -// import org.opensearch.ad.model.EntityProfileName; -// import org.opensearch.ad.model.EntityState; -// import org.opensearch.ad.model.InitProgressProfile; -// import org.opensearch.ad.model.IntervalTimeConfiguration; -// import org.opensearch.ad.settings.NumericSetting; -// import org.opensearch.ad.transport.EntityProfileAction; -// import org.opensearch.ad.transport.EntityProfileRequest; -// import org.opensearch.ad.transport.EntityProfileResponse; -// import org.opensearch.ad.util.MultiResponsesDelegateActionListener; -// import org.opensearch.ad.util.ParseUtils; -// import org.opensearch.client.Client; -// import org.opensearch.cluster.routing.Preference; -// import org.opensearch.common.xcontent.LoggingDeprecationHandler; -// import org.opensearch.common.xcontent.NamedXContentRegistry; -// import org.opensearch.common.xcontent.XContentParser; -// import org.opensearch.common.xcontent.XContentType; -// import org.opensearch.index.IndexNotFoundException; -// import org.opensearch.index.query.BoolQueryBuilder; -// import org.opensearch.index.query.NestedQueryBuilder; -// import org.opensearch.index.query.QueryBuilders; -// import org.opensearch.index.query.TermQueryBuilder; -// import org.opensearch.search.aggregations.AggregationBuilders; -// import org.opensearch.search.builder.SearchSourceBuilder; -// -// public class EntityProfileRunner extends AbstractProfileRunner { -// private final Logger logger = LogManager.getLogger(EntityProfileRunner.class); -// -// static final String NOT_HC_DETECTOR_ERR_MSG = "This is not a high cardinality detector"; -// static final String EMPTY_ENTITY_ATTRIBUTES = "Empty entity attributes"; -// static final String NO_ENTITY = "Cannot find entity"; -// private Client client; -// private NamedXContentRegistry xContentRegistry; -// -// public EntityProfileRunner(Client client, NamedXContentRegistry xContentRegistry, long requiredSamples) { -// super(requiredSamples); -// this.client = client; -// this.xContentRegistry = xContentRegistry; -// } -// -// /** -// * Get profile info of specific entity. -// * -// * @param detectorId detector identifier -// * @param entityValue entity value -// * @param profilesToCollect profiles to collect -// * @param listener action listener to handle exception and process entity profile response -// */ -// public void profile( -// String detectorId, -// Entity entityValue, -// Set profilesToCollect, -// ActionListener listener -// ) { -// if (profilesToCollect == null || profilesToCollect.size() == 0) { -// listener.onFailure(new IllegalArgumentException(CommonErrorMessages.EMPTY_PROFILES_COLLECT)); -// return; -// } -// GetRequest getDetectorRequest = new GetRequest(ANOMALY_DETECTORS_INDEX, detectorId); -// -// client.get(getDetectorRequest, ActionListener.wrap(getResponse -> { -// if (getResponse != null && getResponse.isExists()) { -// try ( -// XContentParser parser = XContentType.JSON -// .xContent() -// .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); -// AnomalyDetector detector = AnomalyDetector.parse(parser, detectorId); -// List categoryFields = detector.getCategoryField(); -// int maxCategoryFields = NumericSetting.maxCategoricalFields(); -// if (categoryFields == null || categoryFields.size() == 0) { -// listener.onFailure(new IllegalArgumentException(NOT_HC_DETECTOR_ERR_MSG)); -// } else if (categoryFields.size() > maxCategoryFields) { -// listener -// .onFailure(new IllegalArgumentException(CommonErrorMessages.getTooManyCategoricalFieldErr(maxCategoryFields))); -// } else { -// validateEntity(entityValue, categoryFields, detectorId, profilesToCollect, detector, listener); -// } -// } catch (Exception t) { -// listener.onFailure(t); -// } -// } else { -// listener.onFailure(new IllegalArgumentException(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG + detectorId)); -// } -// }, listener::onFailure)); -// } -// -// /** -// * Verify if the input entity exists or not in case of typos. -// * -// * If a user deletes the entity after job start, then we will not be able to -// * get this entity in the index. For this case, we will not return a profile -// * for this entity even if it's running on some data node. the entity's model -// * will be deleted by another entity or by maintenance due to long inactivity. -// * -// * @param entity Entity accessor -// * @param categoryFields category fields defined for a detector -// * @param detectorId Detector Id -// * @param profilesToCollect Profile to collect from the input -// * @param detector Detector config accessor -// * @param listener Callback to send responses. -// */ -// private void validateEntity( -// Entity entity, -// List categoryFields, -// String detectorId, -// Set profilesToCollect, -// AnomalyDetector detector, -// ActionListener listener -// ) { -// Map attributes = entity.getAttributes(); -// if (attributes == null || attributes.size() != categoryFields.size()) { -// listener.onFailure(new IllegalArgumentException(EMPTY_ENTITY_ATTRIBUTES)); -// return; -// } -// for (String field : categoryFields) { -// if (false == attributes.containsKey(field)) { -// listener.onFailure(new IllegalArgumentException("Cannot find " + field)); -// return; -// } -// } -// -// BoolQueryBuilder internalFilterQuery = QueryBuilders.boolQuery().filter(detector.getFilterQuery()); -// -// for (TermQueryBuilder term : entity.getTermQueryBuilders()) { -// internalFilterQuery.filter(term); -// } -// -// SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(internalFilterQuery).size(1); -// -// SearchRequest searchRequest = new SearchRequest(detector.getIndices().toArray(new String[0]), searchSourceBuilder) -// .preference(Preference.LOCAL.toString()); -// -// client.search(searchRequest, ActionListener.wrap(searchResponse -> { -// try { -// if (searchResponse.getHits().getHits().length == 0) { -// listener.onFailure(new IllegalArgumentException(NO_ENTITY)); -// return; -// } -// prepareEntityProfile(listener, detectorId, entity, profilesToCollect, detector, categoryFields.get(0)); -// } catch (Exception e) { -// listener.onFailure(new IllegalArgumentException(NO_ENTITY)); -// return; -// } -// }, e -> listener.onFailure(new IllegalArgumentException(NO_ENTITY)))); -// -// } -// -// private void prepareEntityProfile( -// ActionListener listener, -// String detectorId, -// Entity entityValue, -// Set profilesToCollect, -// AnomalyDetector detector, -// String categoryField -// ) { -// EntityProfileRequest request = new EntityProfileRequest(detectorId, entityValue, profilesToCollect); -// -// client -// .execute( -// EntityProfileAction.INSTANCE, -// request, -// ActionListener.wrap(r -> getJob(detectorId, entityValue, profilesToCollect, detector, r, listener), listener::onFailure) -// ); -// } -// -// private void getJob( -// String detectorId, -// Entity entityValue, -// Set profilesToCollect, -// AnomalyDetector detector, -// EntityProfileResponse entityProfileResponse, -// ActionListener listener -// ) { -// GetRequest getRequest = new GetRequest(ANOMALY_DETECTOR_JOB_INDEX, detectorId); -// client.get(getRequest, ActionListener.wrap(getResponse -> { -// if (getResponse != null && getResponse.isExists()) { -// try ( -// XContentParser parser = XContentType.JSON -// .xContent() -// .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); -// AnomalyDetectorJob job = AnomalyDetectorJob.parse(parser); -// -// int totalResponsesToWait = 0; -// if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS) -// || profilesToCollect.contains(EntityProfileName.STATE)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(EntityProfileName.ENTITY_INFO)) { -// totalResponsesToWait++; -// } -// if (profilesToCollect.contains(EntityProfileName.MODELS)) { -// totalResponsesToWait++; -// } -// MultiResponsesDelegateActionListener delegateListener = -// new MultiResponsesDelegateActionListener( -// listener, -// totalResponsesToWait, -// CommonErrorMessages.FAIL_FETCH_ERR_MSG + entityValue + " of detector " + detectorId, -// false -// ); -// -// if (profilesToCollect.contains(EntityProfileName.MODELS)) { -// EntityProfile.Builder builder = new EntityProfile.Builder(); -// if (false == job.isEnabled()) { -// delegateListener.onResponse(builder.build()); -// } else { -// delegateListener.onResponse(builder.modelProfile(entityProfileResponse.getModelProfile()).build()); -// } -// } -// -// if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS) -// || profilesToCollect.contains(EntityProfileName.STATE)) { -// profileStateRelated( -// entityProfileResponse.getTotalUpdates(), -// detectorId, -// entityValue, -// profilesToCollect, -// detector, -// job, -// delegateListener -// ); -// } -// -// if (profilesToCollect.contains(EntityProfileName.ENTITY_INFO)) { -// long enabledTimeMs = job.getEnabledTime().toEpochMilli(); -// SearchRequest lastSampleTimeRequest = createLastSampleTimeRequest( -// detectorId, -// enabledTimeMs, -// entityValue, -// detector.getResultIndex() -// ); -// -// EntityProfile.Builder builder = new EntityProfile.Builder(); -// -// Optional isActiveOp = entityProfileResponse.isActive(); -// if (isActiveOp.isPresent()) { -// builder.isActive(isActiveOp.get()); -// } -// builder.lastActiveTimestampMs(entityProfileResponse.getLastActiveMs()); -// -// client.search(lastSampleTimeRequest, ActionListener.wrap(searchResponse -> { -// Optional latestSampleTimeMs = ParseUtils.getLatestDataTime(searchResponse); -// -// if (latestSampleTimeMs.isPresent()) { -// builder.lastSampleTimestampMs(latestSampleTimeMs.get()); -// } -// -// delegateListener.onResponse(builder.build()); -// }, exception -> { -// // sth wrong like result index not created. Return what we have -// if (exception instanceof IndexNotFoundException) { -// // don't print out stack trace since it is not helpful -// logger.info("Result index hasn't been created", exception.getMessage()); -// } else { -// logger.warn("fail to get last sample time", exception); -// } -// delegateListener.onResponse(builder.build()); -// })); -// } -// } catch (Exception e) { -// logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG, e); -// listener.onFailure(e); -// } -// } else { -// sendUnknownState(profilesToCollect, entityValue, true, listener); -// } -// }, exception -> { -// if (exception instanceof IndexNotFoundException) { -// logger.info(exception.getMessage()); -// sendUnknownState(profilesToCollect, entityValue, true, listener); -// } else { -// logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG + detectorId, exception); -// listener.onFailure(exception); -// } -// })); -// } -// -// private void profileStateRelated( -// long totalUpdates, -// String detectorId, -// Entity entityValue, -// Set profilesToCollect, -// AnomalyDetector detector, -// AnomalyDetectorJob job, -// MultiResponsesDelegateActionListener delegateListener -// ) { -// if (totalUpdates == 0) { -// sendUnknownState(profilesToCollect, entityValue, false, delegateListener); -// } else if (false == job.isEnabled()) { -// sendUnknownState(profilesToCollect, entityValue, false, delegateListener); -// } else if (totalUpdates >= requiredSamples) { -// sendRunningState(profilesToCollect, entityValue, delegateListener); -// } else { -// sendInitState(profilesToCollect, entityValue, detector, totalUpdates, delegateListener); -// } -// } -// -// /** -// * Send unknown state back -// * @param profilesToCollect Profiles to Collect -// * @param entityValue Entity value -// * @param immediate whether we should terminate workflow and respond immediately -// * @param delegateListener Delegate listener -// */ -// private void sendUnknownState( -// Set profilesToCollect, -// Entity entityValue, -// boolean immediate, -// ActionListener delegateListener -// ) { -// EntityProfile.Builder builder = new EntityProfile.Builder(); -// if (profilesToCollect.contains(EntityProfileName.STATE)) { -// builder.state(EntityState.UNKNOWN); -// } -// if (immediate) { -// delegateListener.onResponse(builder.build()); -// } else { -// delegateListener.onResponse(builder.build()); -// } -// } -// -// private void sendRunningState( -// Set profilesToCollect, -// Entity entityValue, -// MultiResponsesDelegateActionListener delegateListener -// ) { -// EntityProfile.Builder builder = new EntityProfile.Builder(); -// if (profilesToCollect.contains(EntityProfileName.STATE)) { -// builder.state(EntityState.RUNNING); -// } -// if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS)) { -// InitProgressProfile initProgress = new InitProgressProfile("100%", 0, 0); -// builder.initProgress(initProgress); -// } -// delegateListener.onResponse(builder.build()); -// } -// -// private void sendInitState( -// Set profilesToCollect, -// Entity entityValue, -// AnomalyDetector detector, -// long updates, -// MultiResponsesDelegateActionListener delegateListener -// ) { -// EntityProfile.Builder builder = new EntityProfile.Builder(); -// if (profilesToCollect.contains(EntityProfileName.STATE)) { -// builder.state(EntityState.INIT); -// } -// if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS)) { -// long intervalMins = ((IntervalTimeConfiguration) detector.getDetectionInterval()).toDuration().toMinutes(); -// InitProgressProfile initProgress = computeInitProgressProfile(updates, intervalMins); -// builder.initProgress(initProgress); -// } -// delegateListener.onResponse(builder.build()); -// } -// -// private SearchRequest createLastSampleTimeRequest(String detectorId, long enabledTime, Entity entity, String resultIndex) { -// BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); -// -// String path = "entity"; -// String entityName = path + ".name"; -// String entityValue = path + ".value"; -// -// for (Map.Entry attribute : entity.getAttributes().entrySet()) { -// /* -// * each attribute pair corresponds to a nested query like -// "nested": { -// "query": { -// "bool": { -// "filter": [ -// { -// "term": { -// "entity.name": { -// "value": "turkey4", -// "boost": 1 -// } -// } -// }, -// { -// "term": { -// "entity.value": { -// "value": "Turkey", -// "boost": 1 -// } -// } -// } -// ] -// } -// }, -// "path": "entity", -// "ignore_unmapped": false, -// "score_mode": "none", -// "boost": 1 -// } -// },*/ -// BoolQueryBuilder nestedBoolQueryBuilder = new BoolQueryBuilder(); -// -// TermQueryBuilder entityNameFilterQuery = QueryBuilders.termQuery(entityName, attribute.getKey()); -// nestedBoolQueryBuilder.filter(entityNameFilterQuery); -// TermQueryBuilder entityValueFilterQuery = QueryBuilders.termQuery(entityValue, attribute.getValue()); -// nestedBoolQueryBuilder.filter(entityValueFilterQuery); -// -// NestedQueryBuilder nestedNameQueryBuilder = new NestedQueryBuilder(path, nestedBoolQueryBuilder, ScoreMode.None); -// boolQueryBuilder.filter(nestedNameQueryBuilder); -// } -// -// boolQueryBuilder.filter(QueryBuilders.termQuery(AnomalyResult.DETECTOR_ID_FIELD, detectorId)); -// -// boolQueryBuilder.filter(QueryBuilders.rangeQuery(AnomalyResult.EXECUTION_END_TIME_FIELD).gte(enabledTime)); -// -// SearchSourceBuilder source = new SearchSourceBuilder() -// .query(boolQueryBuilder) -// .aggregation(AggregationBuilders.max(CommonName.AGG_NAME_MAX_TIME).field(AnomalyResult.EXECUTION_END_TIME_FIELD)) -// .trackTotalHits(false) -// .size(0); -// -// SearchRequest request = new SearchRequest(CommonName.ANOMALY_RESULT_INDEX_ALIAS); -// request.source(source); -// if (resultIndex != null) { -// request.indices(resultIndex); -// } -// return request; -// } -// } + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.ad; + +import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; +import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; +import org.opensearch.ad.model.AnomalyResult; +import org.opensearch.ad.model.Entity; +import org.opensearch.ad.model.EntityProfile; +import org.opensearch.ad.model.EntityProfileName; +import org.opensearch.ad.model.EntityState; +import org.opensearch.ad.model.InitProgressProfile; +import org.opensearch.ad.model.IntervalTimeConfiguration; +import org.opensearch.ad.settings.NumericSetting; +import org.opensearch.ad.transport.EntityProfileRequest; +import org.opensearch.ad.transport.EntityProfileResponse; +import org.opensearch.ad.util.MultiResponsesDelegateActionListener; +import org.opensearch.ad.util.ParseUtils; +import org.opensearch.cluster.routing.Preference; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.NestedQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.search.aggregations.AggregationBuilders; +import org.opensearch.search.builder.SearchSourceBuilder; + +public class EntityProfileRunner extends AbstractProfileRunner { + private final Logger logger = LogManager.getLogger(EntityProfileRunner.class); + + static final String NOT_HC_DETECTOR_ERR_MSG = "This is not a high cardinality detector"; + static final String EMPTY_ENTITY_ATTRIBUTES = "Empty entity attributes"; + static final String NO_ENTITY = "Cannot find entity"; + private SDKRestClient client; + private NamedXContentRegistry xContentRegistry; + + public EntityProfileRunner(SDKRestClient client, NamedXContentRegistry xContentRegistry, long requiredSamples) { + super(requiredSamples); + this.client = client; + this.xContentRegistry = xContentRegistry; + } + + /** + * Get profile info of specific entity. + * + * @param detectorId detector identifier + * @param entityValue entity value + * @param profilesToCollect profiles to collect + * @param listener action listener to handle exception and process entity profile response + */ + public void profile( + String detectorId, + Entity entityValue, + Set profilesToCollect, + ActionListener listener + ) { + if (profilesToCollect == null || profilesToCollect.size() == 0) { + listener.onFailure(new IllegalArgumentException(CommonErrorMessages.EMPTY_PROFILES_COLLECT)); + return; + } + GetRequest getDetectorRequest = new GetRequest(ANOMALY_DETECTORS_INDEX, detectorId); + + client.get(getDetectorRequest, ActionListener.wrap(getResponse -> { + if (getResponse != null && getResponse.isExists()) { + try ( + XContentParser parser = XContentType.JSON + .xContent() + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + AnomalyDetector detector = AnomalyDetector.parse(parser, detectorId); + List categoryFields = detector.getCategoryField(); + int maxCategoryFields = NumericSetting.maxCategoricalFields(); + if (categoryFields == null || categoryFields.size() == 0) { + listener.onFailure(new IllegalArgumentException(NOT_HC_DETECTOR_ERR_MSG)); + } else if (categoryFields.size() > maxCategoryFields) { + listener + .onFailure(new IllegalArgumentException(CommonErrorMessages.getTooManyCategoricalFieldErr(maxCategoryFields))); + } else { + validateEntity(entityValue, categoryFields, detectorId, profilesToCollect, detector, listener); + } + } catch (Exception t) { + listener.onFailure(t); + } + } else { + listener.onFailure(new IllegalArgumentException(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG + detectorId)); + } + }, listener::onFailure)); + } + + /** + * Verify if the input entity exists or not in case of typos. + * + * If a user deletes the entity after job start, then we will not be able to + * get this entity in the index. For this case, we will not return a profile + * for this entity even if it's running on some data node. the entity's model + * will be deleted by another entity or by maintenance due to long inactivity. + * + * @param entity Entity accessor + * @param categoryFields category fields defined for a detector + * @param detectorId Detector Id + * @param profilesToCollect Profile to collect from the input + * @param detector Detector config accessor + * @param listener Callback to send responses. + */ + private void validateEntity( + Entity entity, + List categoryFields, + String detectorId, + Set profilesToCollect, + AnomalyDetector detector, + ActionListener listener + ) { + Map attributes = entity.getAttributes(); + if (attributes == null || attributes.size() != categoryFields.size()) { + listener.onFailure(new IllegalArgumentException(EMPTY_ENTITY_ATTRIBUTES)); + return; + } + for (String field : categoryFields) { + if (false == attributes.containsKey(field)) { + listener.onFailure(new IllegalArgumentException("Cannot find " + field)); + return; + } + } + + BoolQueryBuilder internalFilterQuery = QueryBuilders.boolQuery().filter(detector.getFilterQuery()); + + for (TermQueryBuilder term : entity.getTermQueryBuilders()) { + internalFilterQuery.filter(term); + } + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(internalFilterQuery).size(1); + + SearchRequest searchRequest = new SearchRequest(detector.getIndices().toArray(new String[0]), searchSourceBuilder) + .preference(Preference.LOCAL.toString()); + + client.search(searchRequest, ActionListener.wrap(searchResponse -> { + try { + if (searchResponse.getHits().getHits().length == 0) { + listener.onFailure(new IllegalArgumentException(NO_ENTITY)); + return; + } + prepareEntityProfile(listener, detectorId, entity, profilesToCollect, detector, categoryFields.get(0)); + } catch (Exception e) { + listener.onFailure(new IllegalArgumentException(NO_ENTITY)); + return; + } + }, e -> listener.onFailure(new IllegalArgumentException(NO_ENTITY)))); + + } + + private void prepareEntityProfile( + ActionListener listener, + String detectorId, + Entity entityValue, + Set profilesToCollect, + AnomalyDetector detector, + String categoryField + ) { + EntityProfileRequest request = new EntityProfileRequest(detectorId, entityValue, profilesToCollect); + + /*client + .execute( + EntityProfileAction.INSTANCE, + request, + ActionListener.wrap(r -> getJob(detectorId, entityValue, profilesToCollect, detector, r, listener), listener::onFailure) + );*/ + } + + private void getJob( + String detectorId, + Entity entityValue, + Set profilesToCollect, + AnomalyDetector detector, + EntityProfileResponse entityProfileResponse, + ActionListener listener + ) { + GetRequest getRequest = new GetRequest(ANOMALY_DETECTOR_JOB_INDEX, detectorId); + client.get(getRequest, ActionListener.wrap(getResponse -> { + if (getResponse != null && getResponse.isExists()) { + try ( + XContentParser parser = XContentType.JSON + .xContent() + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + AnomalyDetectorJob job = AnomalyDetectorJob.parse(parser); + + int totalResponsesToWait = 0; + if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS) + || profilesToCollect.contains(EntityProfileName.STATE)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(EntityProfileName.ENTITY_INFO)) { + totalResponsesToWait++; + } + if (profilesToCollect.contains(EntityProfileName.MODELS)) { + totalResponsesToWait++; + } + MultiResponsesDelegateActionListener delegateListener = + new MultiResponsesDelegateActionListener( + listener, + totalResponsesToWait, + CommonErrorMessages.FAIL_FETCH_ERR_MSG + entityValue + " of detector " + detectorId, + false + ); + + if (profilesToCollect.contains(EntityProfileName.MODELS)) { + EntityProfile.Builder builder = new EntityProfile.Builder(); + if (false == job.isEnabled()) { + delegateListener.onResponse(builder.build()); + } else { + delegateListener.onResponse(builder.modelProfile(entityProfileResponse.getModelProfile()).build()); + } + } + + if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS) + || profilesToCollect.contains(EntityProfileName.STATE)) { + profileStateRelated( + entityProfileResponse.getTotalUpdates(), + detectorId, + entityValue, + profilesToCollect, + detector, + job, + delegateListener + ); + } + + if (profilesToCollect.contains(EntityProfileName.ENTITY_INFO)) { + long enabledTimeMs = job.getEnabledTime().toEpochMilli(); + SearchRequest lastSampleTimeRequest = createLastSampleTimeRequest( + detectorId, + enabledTimeMs, + entityValue, + detector.getResultIndex() + ); + + EntityProfile.Builder builder = new EntityProfile.Builder(); + + Optional isActiveOp = entityProfileResponse.isActive(); + if (isActiveOp.isPresent()) { + builder.isActive(isActiveOp.get()); + } + builder.lastActiveTimestampMs(entityProfileResponse.getLastActiveMs()); + + client.search(lastSampleTimeRequest, ActionListener.wrap(searchResponse -> { + Optional latestSampleTimeMs = ParseUtils.getLatestDataTime(searchResponse); + + if (latestSampleTimeMs.isPresent()) { + builder.lastSampleTimestampMs(latestSampleTimeMs.get()); + } + + delegateListener.onResponse(builder.build()); + }, exception -> { + // sth wrong like result index not created. Return what we have + if (exception instanceof IndexNotFoundException) { + // don't print out stack trace since it is not helpful + logger.info("Result index hasn't been created", exception.getMessage()); + } else { + logger.warn("fail to get last sample time", exception); + } + delegateListener.onResponse(builder.build()); + })); + } + } catch (Exception e) { + logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG, e); + listener.onFailure(e); + } + } else { + sendUnknownState(profilesToCollect, entityValue, true, listener); + } + }, exception -> { + if (exception instanceof IndexNotFoundException) { + logger.info(exception.getMessage()); + sendUnknownState(profilesToCollect, entityValue, true, listener); + } else { + logger.error(CommonErrorMessages.FAIL_TO_GET_PROFILE_MSG + detectorId, exception); + listener.onFailure(exception); + } + })); + } + + private void profileStateRelated( + long totalUpdates, + String detectorId, + Entity entityValue, + Set profilesToCollect, + AnomalyDetector detector, + AnomalyDetectorJob job, + MultiResponsesDelegateActionListener delegateListener + ) { + if (totalUpdates == 0) { + sendUnknownState(profilesToCollect, entityValue, false, delegateListener); + } else if (false == job.isEnabled()) { + sendUnknownState(profilesToCollect, entityValue, false, delegateListener); + } else if (totalUpdates >= requiredSamples) { + sendRunningState(profilesToCollect, entityValue, delegateListener); + } else { + sendInitState(profilesToCollect, entityValue, detector, totalUpdates, delegateListener); + } + } + + /** + * Send unknown state back + * @param profilesToCollect Profiles to Collect + * @param entityValue Entity value + * @param immediate whether we should terminate workflow and respond immediately + * @param delegateListener Delegate listener + */ + private void sendUnknownState( + Set profilesToCollect, + Entity entityValue, + boolean immediate, + ActionListener delegateListener + ) { + EntityProfile.Builder builder = new EntityProfile.Builder(); + if (profilesToCollect.contains(EntityProfileName.STATE)) { + builder.state(EntityState.UNKNOWN); + } + if (immediate) { + delegateListener.onResponse(builder.build()); + } else { + delegateListener.onResponse(builder.build()); + } + } + + private void sendRunningState( + Set profilesToCollect, + Entity entityValue, + MultiResponsesDelegateActionListener delegateListener + ) { + EntityProfile.Builder builder = new EntityProfile.Builder(); + if (profilesToCollect.contains(EntityProfileName.STATE)) { + builder.state(EntityState.RUNNING); + } + if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS)) { + InitProgressProfile initProgress = new InitProgressProfile("100%", 0, 0); + builder.initProgress(initProgress); + } + delegateListener.onResponse(builder.build()); + } + + private void sendInitState( + Set profilesToCollect, + Entity entityValue, + AnomalyDetector detector, + long updates, + MultiResponsesDelegateActionListener delegateListener + ) { + EntityProfile.Builder builder = new EntityProfile.Builder(); + if (profilesToCollect.contains(EntityProfileName.STATE)) { + builder.state(EntityState.INIT); + } + if (profilesToCollect.contains(EntityProfileName.INIT_PROGRESS)) { + long intervalMins = ((IntervalTimeConfiguration) detector.getDetectionInterval()).toDuration().toMinutes(); + InitProgressProfile initProgress = computeInitProgressProfile(updates, intervalMins); + builder.initProgress(initProgress); + } + delegateListener.onResponse(builder.build()); + } + + private SearchRequest createLastSampleTimeRequest(String detectorId, long enabledTime, Entity entity, String resultIndex) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + + String path = "entity"; + String entityName = path + ".name"; + String entityValue = path + ".value"; + + for (Map.Entry attribute : entity.getAttributes().entrySet()) { + /* + * each attribute pair corresponds to a nested query like + "nested": { + "query": { + "bool": { + "filter": [ + { + "term": { + "entity.name": { + "value": "turkey4", + "boost": 1 + } + } + }, + { + "term": { + "entity.value": { + "value": "Turkey", + "boost": 1 + } + } + } + ] + } + }, + "path": "entity", + "ignore_unmapped": false, + "score_mode": "none", + "boost": 1 + } + },*/ + BoolQueryBuilder nestedBoolQueryBuilder = new BoolQueryBuilder(); + + TermQueryBuilder entityNameFilterQuery = QueryBuilders.termQuery(entityName, attribute.getKey()); + nestedBoolQueryBuilder.filter(entityNameFilterQuery); + TermQueryBuilder entityValueFilterQuery = QueryBuilders.termQuery(entityValue, attribute.getValue()); + nestedBoolQueryBuilder.filter(entityValueFilterQuery); + + NestedQueryBuilder nestedNameQueryBuilder = new NestedQueryBuilder(path, nestedBoolQueryBuilder, ScoreMode.None); + boolQueryBuilder.filter(nestedNameQueryBuilder); + } + + boolQueryBuilder.filter(QueryBuilders.termQuery(AnomalyResult.DETECTOR_ID_FIELD, detectorId)); + + boolQueryBuilder.filter(QueryBuilders.rangeQuery(AnomalyResult.EXECUTION_END_TIME_FIELD).gte(enabledTime)); + + SearchSourceBuilder source = new SearchSourceBuilder() + .query(boolQueryBuilder) + .aggregation(AggregationBuilders.max(CommonName.AGG_NAME_MAX_TIME).field(AnomalyResult.EXECUTION_END_TIME_FIELD)) + .trackTotalHits(false) + .size(0); + + SearchRequest request = new SearchRequest(CommonName.ANOMALY_RESULT_INDEX_ALIAS); + request.source(source); + if (resultIndex != null) { + request.indices(resultIndex); + } + return request; + } +} diff --git a/src/main/java/org/opensearch/ad/model/AnomalyDetector.java b/src/main/java/org/opensearch/ad/model/AnomalyDetector.java index 252eb662d..6a43ed548 100644 --- a/src/main/java/org/opensearch/ad/model/AnomalyDetector.java +++ b/src/main/java/org/opensearch/ad/model/AnomalyDetector.java @@ -72,7 +72,7 @@ public class AnomalyDetector implements Writeable, ToXContentObject { it -> parse(it) ); public static final String NO_ID = ""; - public static final String ANOMALY_DETECTORS_INDEX = "opendistro-anomaly-detectors"; + public static final String ANOMALY_DETECTORS_INDEX = ".opendistro-anomaly-detectors"; public static final String TYPE = "_doc"; public static final String QUERY_PARAM_PERIOD_START = "period_start"; public static final String QUERY_PARAM_PERIOD_END = "period_end"; diff --git a/src/main/java/org/opensearch/ad/rest/RestCreateDetectorAction.java b/src/main/java/org/opensearch/ad/rest/RestCreateDetectorAction.java deleted file mode 100644 index d53194ae4..000000000 --- a/src/main/java/org/opensearch/ad/rest/RestCreateDetectorAction.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.ad.rest; - -import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; -import static org.opensearch.ad.settings.AnomalyDetectorSettings.ANOMALY_DETECTORS_INDEX_MAPPING_FILE; -import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.rest.RestStatus.BAD_REQUEST; -import static org.opensearch.rest.RestStatus.CREATED; -import static org.opensearch.rest.RestStatus.OK; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.List; - -import org.opensearch.ad.AnomalyDetectorExtension; -import org.opensearch.ad.constant.CommonErrorMessages; -import org.opensearch.ad.indices.AnomalyDetectionIndices; -import org.opensearch.ad.model.AnomalyDetector; -import org.opensearch.ad.settings.EnabledSetting; -import org.opensearch.client.json.JsonpMapper; -import org.opensearch.client.opensearch.OpenSearchClient; -import org.opensearch.client.opensearch._types.mapping.TypeMapping; -import org.opensearch.client.opensearch.core.IndexRequest; -import org.opensearch.client.opensearch.core.IndexResponse; -import org.opensearch.client.opensearch.indices.CreateIndexRequest; -import org.opensearch.client.opensearch.indices.CreateIndexResponse; -import org.opensearch.common.xcontent.NamedXContentRegistry; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.common.xcontent.XContentParser; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.extensions.rest.ExtensionRestRequest; -import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.sdk.BaseExtensionRestHandler; -import org.opensearch.sdk.ExtensionsRunner; -import org.opensearch.sdk.RouteHandler; - -import com.google.common.base.Charsets; -import com.google.common.io.Resources; -import jakarta.json.stream.JsonParser; - -public class RestCreateDetectorAction extends BaseExtensionRestHandler { - - private final OpenSearchClient sdkClient; - private final NamedXContentRegistry xContentRegistry; - - public RestCreateDetectorAction(ExtensionsRunner runner, AnomalyDetectorExtension extension) { - this.xContentRegistry = runner.getNamedXContentRegistry().getRegistry(); - this.sdkClient = extension.getClient(); - } - - @Override - protected List routeHandlers() { - return List.of(new RouteHandler(POST, "/detectors", (r) -> handlePostRequest(r))); - } - - private String getAnomalyDetectorMappings() throws IOException { - URL url = AnomalyDetectionIndices.class.getClassLoader().getResource(ANOMALY_DETECTORS_INDEX_MAPPING_FILE); - return Resources.toString(url, Charsets.UTF_8); - } - - private IndexResponse indexAnomalyDetector(AnomalyDetector anomalyDetector) throws IOException { - AnomalyDetector detector = new AnomalyDetector( - anomalyDetector.getName(), - anomalyDetector.getVersion(), - anomalyDetector.getName(), - anomalyDetector.getDescription(), - anomalyDetector.getTimeField(), - anomalyDetector.getIndices(), - anomalyDetector.getFeatureAttributes(), - anomalyDetector.getFilterQuery(), - anomalyDetector.getDetectionInterval(), - anomalyDetector.getWindowDelay(), - anomalyDetector.getShingleSize(), - anomalyDetector.getUiMetadata(), - anomalyDetector.getSchemaVersion(), - Instant.now(), - anomalyDetector.getCategoryField(), - null, - anomalyDetector.getResultIndex() - ); - - IndexRequest indexRequest = new IndexRequest.Builder() - .index(ANOMALY_DETECTORS_INDEX) - .document(detector) - .build(); - IndexResponse indexResponse = sdkClient.index(indexRequest); - return indexResponse; - - } - - private CreateIndexRequest initAnomalyDetectorIndex() { - JsonpMapper mapper = sdkClient._transport().jsonpMapper(); - JsonParser parser = null; - try { - parser = mapper - .jsonProvider() - .createParser(new ByteArrayInputStream(getAnomalyDetectorMappings().getBytes(StandardCharsets.UTF_8))); - } catch (Exception e) { - e.printStackTrace(); - } - - CreateIndexRequest request = null; - try { - request = new CreateIndexRequest.Builder() - .index(ANOMALY_DETECTORS_INDEX) - .mappings(TypeMapping._DESERIALIZER.deserialize(parser, mapper)) - .build(); - } catch (Exception e) { - e.printStackTrace(); - } - - return request; - } - - private ExtensionRestResponse handlePostRequest(ExtensionRestRequest request) { - if (!EnabledSetting.isADPluginEnabled()) { - throw new IllegalStateException(CommonErrorMessages.DISABLED_ERR_MSG); - } - - XContentParser parser; - AnomalyDetector detector; - XContentBuilder builder = null; - CreateIndexRequest createIndexRequest; - try { - parser = request.contentParser(this.xContentRegistry); - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - detector = AnomalyDetector.parse(parser); - createIndexRequest = initAnomalyDetectorIndex(); - CreateIndexResponse createIndexResponse = sdkClient.indices().create(createIndexRequest); - if (createIndexResponse.acknowledged()) { - IndexResponse indexResponse = indexAnomalyDetector(detector); - try { - builder = XContentBuilder.builder(XContentType.JSON.xContent()); - builder.startObject(); - builder.field("id", indexResponse.id()); - builder.field("version", indexResponse.version()); - builder.field("seqNo", indexResponse.seqNo()); - builder.field("primaryTerm", indexResponse.primaryTerm()); - builder.field("detector", detector); - builder.field("status", CREATED); - builder.endObject(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } catch (Exception e) { - return new ExtensionRestResponse(request, BAD_REQUEST, builder); - } - return new ExtensionRestResponse(request, OK, builder); - } -} diff --git a/src/main/java/org/opensearch/ad/rest/RestGetAnomalyDetectorAction.java b/src/main/java/org/opensearch/ad/rest/RestGetAnomalyDetectorAction.java index 216cfaf94..de35b43f6 100644 --- a/src/main/java/org/opensearch/ad/rest/RestGetAnomalyDetectorAction.java +++ b/src/main/java/org/opensearch/ad/rest/RestGetAnomalyDetectorAction.java @@ -12,62 +12,112 @@ package org.opensearch.ad.rest; import static org.opensearch.ad.util.RestHandlerUtils.DETECTOR_ID; -import static org.opensearch.ad.util.RestHandlerUtils.PROFILE; import static org.opensearch.ad.util.RestHandlerUtils.TYPE; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.ad.AnomalyDetectorPlugin; +import org.opensearch.action.ActionListener; +import org.opensearch.ad.AnomalyDetectorExtension; import org.opensearch.ad.constant.CommonErrorMessages; import org.opensearch.ad.constant.CommonName; import org.opensearch.ad.model.Entity; +import org.opensearch.ad.settings.AnomalyDetectorSettings; import org.opensearch.ad.settings.EnabledSetting; -import org.opensearch.ad.transport.GetAnomalyDetectorAction; import org.opensearch.ad.transport.GetAnomalyDetectorRequest; -import org.opensearch.client.node.NodeClient; +import org.opensearch.ad.transport.GetAnomalyDetectorResponse; +import org.opensearch.ad.transport.GetAnomalyDetectorTransportAction; import org.opensearch.common.Strings; -import org.opensearch.rest.BaseRestHandler; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.common.xcontent.ToXContent; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.extensions.rest.ExtensionRestRequest; +import org.opensearch.extensions.rest.ExtensionRestResponse; import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestActions; -import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.rest.RestStatus; +import org.opensearch.sdk.BaseExtensionRestHandler; +import org.opensearch.sdk.ExtensionsRunner; +import org.opensearch.sdk.RouteHandler; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.sdk.SDKClusterService; +import org.opensearch.transport.TransportService; import com.google.common.collect.ImmutableList; /** * This class consists of the REST handler to retrieve an anomaly detector. */ -public class RestGetAnomalyDetectorAction extends BaseRestHandler { +public class RestGetAnomalyDetectorAction extends BaseExtensionRestHandler { private static final String GET_ANOMALY_DETECTOR_ACTION = "get_anomaly_detector"; private static final Logger logger = LogManager.getLogger(RestGetAnomalyDetectorAction.class); + private NamedXContentRegistry namedXContentRegistry; + private Settings settings; + private TransportService transportService; + private SDKRestClient client; + private SDKClusterService clusterService; + private ExtensionsRunner extensionsRunner; + + public RestGetAnomalyDetectorAction(ExtensionsRunner extensionsRunner, AnomalyDetectorExtension anomalyDetectorExtension) { + this.extensionsRunner = extensionsRunner; + this.namedXContentRegistry = extensionsRunner.getNamedXContentRegistry().getRegistry(); + this.settings = extensionsRunner.getEnvironmentSettings(); + this.transportService = extensionsRunner.getExtensionTransportService(); + this.client = anomalyDetectorExtension.getRestClient(); + this.clusterService = new SDKClusterService(extensionsRunner); + } - public RestGetAnomalyDetectorAction() {} - - @Override + // @Override public String getName() { return GET_ANOMALY_DETECTOR_ACTION; } @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + public List routeHandlers() { + return ImmutableList + .of( + // GET + new RouteHandler( + RestRequest.Method.GET, + String.format(Locale.ROOT, "%s/{%s}", AnomalyDetectorExtension.AD_BASE_DETECTORS_URI, DETECTOR_ID), + handleRequest + ) + ); + } + + private Function handleRequest = (request) -> { + try { + return prepareRequest(request); + } catch (Exception e) { + // TODO: handle the AD-specific exceptions separately + return exceptionalRequest(request, e); + } + }; + + protected ExtensionRestResponse prepareRequest(ExtensionRestRequest request) throws IOException { if (!EnabledSetting.isADPluginEnabled()) { throw new IllegalStateException(CommonErrorMessages.DISABLED_ERR_MSG); } String detectorId = request.param(DETECTOR_ID); String typesStr = request.param(TYPE); - String rawPath = request.rawPath(); - boolean returnJob = request.paramAsBoolean("job", false); - boolean returnTask = request.paramAsBoolean("task", false); - boolean all = request.paramAsBoolean("_all", false); + String rawPath = request.path(); + // FIXME handle this + // Passed false until job scheduler is integrated + boolean returnJob = false; + boolean returnTask = false; + boolean all = false; GetAnomalyDetectorRequest getAnomalyDetectorRequest = new GetAnomalyDetectorRequest( detectorId, - RestActions.parseVersion(request), + 1, // version. RestActions.parseVersion(request). TODO: https://github.com/opensearch-project/opensearch-sdk-java/issues/431 returnJob, returnTask, typesStr, @@ -76,27 +126,52 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli buildEntity(request, detectorId) ); - return channel -> client - .execute(GetAnomalyDetectorAction.INSTANCE, getAnomalyDetectorRequest, new RestToXContentListener<>(channel)); + GetAnomalyDetectorTransportAction getTransportAction = new GetAnomalyDetectorTransportAction( + transportService, + null, // nodeFilter + null, // ActionFilters actionFilters + clusterService, + client, + settings, + extensionsRunner.getNamedXContentRegistry().getRegistry(), // TODO: + // https://github.com/opensearch-project/opensearch-sdk-java/issues/447 + null // ADTaskManager adTaskManager + ); + + CompletableFuture futureResponse = new CompletableFuture<>(); + getTransportAction + .doExecute( + null, // task + getAnomalyDetectorRequest, + ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) + ); + + GetAnomalyDetectorResponse response = futureResponse + .orTimeout(AnomalyDetectorSettings.REQUEST_TIMEOUT.get(settings).getMillis(), TimeUnit.MILLISECONDS) + .join(); + + // TODO handle exceptional response + return getAnomalyDetectorResponse(request, response); + } - @Override + /*@Override public List routes() { return ImmutableList - .of( - // Opensearch-only API. Considering users may provide entity in the search body, support POST as well. - new Route( - RestRequest.Method.POST, - String.format(Locale.ROOT, "%s/{%s}/%s", AnomalyDetectorPlugin.AD_BASE_DETECTORS_URI, DETECTOR_ID, PROFILE) - ), - new Route( - RestRequest.Method.POST, - String.format(Locale.ROOT, "%s/{%s}/%s/{%s}", AnomalyDetectorPlugin.AD_BASE_DETECTORS_URI, DETECTOR_ID, PROFILE, TYPE) - ) - ); - } + .of( + // Opensearch-only API. Considering users may provide entity in the search body, support POST as well. + new Route( + RestRequest.Method.POST, + String.format(Locale.ROOT, "%s/{%s}/%s", AnomalyDetectorPlugin.AD_BASE_DETECTORS_URI, DETECTOR_ID, PROFILE) + ), + new Route( + RestRequest.Method.POST, + String.format(Locale.ROOT, "%s/{%s}/%s/{%s}", AnomalyDetectorPlugin.AD_BASE_DETECTORS_URI, DETECTOR_ID, PROFILE, TYPE) + ) + ); + }*/ - @Override + /* @Override public List replacedRoutes() { String path = String.format(Locale.ROOT, "%s/{%s}", AnomalyDetectorPlugin.LEGACY_OPENDISTRO_AD_BASE_URI, DETECTOR_ID); String newPath = String.format(Locale.ROOT, "%s/{%s}", AnomalyDetectorPlugin.AD_BASE_DETECTORS_URI, DETECTOR_ID); @@ -127,9 +202,9 @@ public List replacedRoutes() { ) ) ); - } + }*/ - private Entity buildEntity(RestRequest request, String detectorId) throws IOException { + private Entity buildEntity(ExtensionRestRequest request, String detectorId) throws IOException { if (Strings.isEmpty(detectorId)) { throw new IllegalStateException(CommonErrorMessages.AD_ID_MISSING_MSG); } @@ -151,7 +226,7 @@ private Entity buildEntity(RestRequest request, String detectorId) throws IOExce * }] * } */ - Optional entity = Entity.fromJsonObject(request.contentParser()); + Optional entity = Entity.fromJsonObject(request.contentParser(namedXContentRegistry)); if (entity.isPresent()) { return entity.get(); } @@ -159,4 +234,15 @@ private Entity buildEntity(RestRequest request, String detectorId) throws IOExce // not a valid profile request with correct entity information return null; } + + private ExtensionRestResponse getAnomalyDetectorResponse(ExtensionRestRequest request, GetAnomalyDetectorResponse response) + throws IOException { + RestStatus restStatus = RestStatus.OK; + ExtensionRestResponse extensionRestResponse = new ExtensionRestResponse( + request, + restStatus, + response.toXContent(JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS) + ); + return extensionRestResponse; + } } diff --git a/src/main/java/org/opensearch/ad/rest/RestGetDetectorAction.java b/src/main/java/org/opensearch/ad/rest/RestGetDetectorAction.java deleted file mode 100644 index 04e80062c..000000000 --- a/src/main/java/org/opensearch/ad/rest/RestGetDetectorAction.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.opensearch.ad.rest; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestStatus.NOT_FOUND; -import static org.opensearch.rest.RestStatus.OK; - -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.ad.constant.CommonErrorMessages; -import org.opensearch.ad.settings.EnabledSetting; -import org.opensearch.extensions.rest.ExtensionRestRequest; -import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.rest.RestHandler.Route; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.sdk.ExtensionRestHandler; - -public class RestGetDetectorAction implements ExtensionRestHandler { - private final Logger logger = LogManager.getLogger(RestGetDetectorAction.class); - - @Override - public List routes() { - return List.of(new Route(GET, "/detectors")); - } - - @Override - public ExtensionRestResponse handleRequest(ExtensionRestRequest request) { - if (!EnabledSetting.isADPluginEnabled()) { - throw new IllegalStateException(CommonErrorMessages.DISABLED_ERR_MSG); - } - Method method = request.method(); - - if (!Method.GET.equals(method)) { - return new ExtensionRestResponse( - request, - NOT_FOUND, - "Extension REST action improperly configured to handle " + request.toString() - ); - } - // do things with request - return new ExtensionRestResponse(request, OK, "placeholder"); - } - -} diff --git a/src/main/java/org/opensearch/ad/rest/RestValidateDetectorAction.java b/src/main/java/org/opensearch/ad/rest/RestValidateDetectorAction.java deleted file mode 100644 index 4baa74f45..000000000 --- a/src/main/java/org/opensearch/ad/rest/RestValidateDetectorAction.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.opensearch.ad.rest; - -import static org.opensearch.ad.util.RestHandlerUtils.TYPE; -import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -import static org.opensearch.rest.RestRequest.Method.POST; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.ad.AnomalyDetectorExtension; -import org.opensearch.ad.common.exception.ADValidationException; -import org.opensearch.ad.constant.CommonErrorMessages; -import org.opensearch.ad.model.AnomalyDetector; -import org.opensearch.ad.model.DetectorValidationIssue; -import org.opensearch.ad.model.ValidationAspect; -import org.opensearch.ad.settings.EnabledSetting; -import org.opensearch.ad.transport.ValidateAnomalyDetectorRequest; -import org.opensearch.client.opensearch.OpenSearchClient; -import org.opensearch.common.xcontent.NamedXContentRegistry; -import org.opensearch.common.xcontent.XContentBuilder; -import org.opensearch.common.xcontent.XContentParser; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.extensions.rest.ExtensionRestRequest; -import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.rest.RestStatus; -import org.opensearch.sdk.BaseExtensionRestHandler; -import org.opensearch.sdk.ExtensionsRunner; -import org.opensearch.sdk.RouteHandler; - -public class RestValidateDetectorAction extends BaseExtensionRestHandler { - private final Logger logger = LogManager.getLogger(RestValidateDetectorAction.class); - private final OpenSearchClient sdkClient; - private final NamedXContentRegistry xContentRegistry; - - public static final Set ALL_VALIDATION_ASPECTS_STRS = Arrays - .asList(ValidationAspect.values()) - .stream() - .map(aspect -> aspect.getName()) - .collect(Collectors.toSet()); - - public RestValidateDetectorAction(ExtensionsRunner runner, AnomalyDetectorExtension extension) { - this.xContentRegistry = runner.getNamedXContentRegistry().getRegistry(); - this.sdkClient = extension.getClient(); - } - - @Override - protected List routeHandlers() { - return List.of(new RouteHandler(POST, "/detectors/_validate", (r) -> handleValidateDetectorRequest(r))); - } - - private ExtensionRestResponse handleValidateDetectorRequest(ExtensionRestRequest request) { - if (!EnabledSetting.isADPluginEnabled()) { - throw new IllegalStateException(CommonErrorMessages.DISABLED_ERR_MSG); - } - AnomalyDetector detector; - XContentParser parser; - XContentBuilder builder = null; - ValidateAnomalyDetectorRequest validateAnomalyDetectorRequest; - try { - parser = request.contentParser(this.xContentRegistry); - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - String typesStr = validateTypeString(request); - DetectorValidationIssue issue = null; - try { - detector = AnomalyDetector.parse(parser); - // validateAnomalyDetectorRequest= new ValidateAnomalyDetectorRequest( - // detector, - // typesStr, - // extension, - // maxMultiEntityDetectors, - // maxAnomalyFeatures, - // requestTimeout - // ); - } catch (Exception e) { - if (e instanceof ADValidationException) { - ADValidationException ADException = (ADValidationException) e; - issue = new DetectorValidationIssue(ADException.getAspect(), ADException.getType(), ADException.getMessage()); - } - } - - try { - builder = XContentBuilder.builder(XContentType.JSON.xContent()); - builder.startObject(); - builder.field("issue", issue); - builder.endObject(); - } catch (IOException e) { - e.printStackTrace(); - } - - } catch (Exception e) { - return new ExtensionRestResponse(request, RestStatus.BAD_REQUEST, builder); - } - return new ExtensionRestResponse(request, RestStatus.OK, "placeholder"); - } - - private Boolean validationTypesAreAccepted(String validationType) { - Set typesInRequest = new HashSet<>(Arrays.asList(validationType.split(","))); - return (!Collections.disjoint(typesInRequest, ALL_VALIDATION_ASPECTS_STRS)); - } - - private String validateTypeString(ExtensionRestRequest request) { - String typesStr = request.param(TYPE); - - // if type param isn't blank and isn't a part of possible validation types throws exception - if (!StringUtils.isBlank(typesStr)) { - if (!validationTypesAreAccepted(typesStr)) { - throw new IllegalStateException(CommonErrorMessages.NOT_EXISTENT_VALIDATION_TYPE); - } - } - return typesStr; - } - -} diff --git a/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorResponse.java b/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorResponse.java index 0674a5c0b..a58c9cddb 100644 --- a/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorResponse.java +++ b/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorResponse.java @@ -16,6 +16,7 @@ import org.opensearch.action.ActionResponse; import org.opensearch.ad.model.ADTask; import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; import org.opensearch.ad.model.DetectorProfile; import org.opensearch.ad.model.EntityProfile; import org.opensearch.ad.util.RestHandlerUtils; @@ -33,7 +34,7 @@ public class GetAnomalyDetectorResponse extends ActionResponse implements ToXCon private long primaryTerm; private long seqNo; private AnomalyDetector detector; - // private AnomalyDetectorJob adJob; + private AnomalyDetectorJob adJob; private ADTask realtimeAdTask; private ADTask historicalAdTask; private RestStatus restStatus; @@ -63,12 +64,11 @@ public GetAnomalyDetectorResponse(StreamInput in) throws IOException { restStatus = in.readEnum(RestStatus.class); detector = new AnomalyDetector(in); returnJob = in.readBoolean(); - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility - // if (returnJob) { - // adJob = new AnomalyDetectorJob(in); - // } else { - // adJob = null; - // } + if (returnJob) { + adJob = new AnomalyDetectorJob(in); + } else { + adJob = null; + } returnTask = in.readBoolean(); if (in.readBoolean()) { realtimeAdTask = new ADTask(in); @@ -89,7 +89,7 @@ public GetAnomalyDetectorResponse( long primaryTerm, long seqNo, AnomalyDetector detector, - // AnomalyDetectorJob adJob, + AnomalyDetectorJob adJob, boolean returnJob, ADTask realtimeAdTask, ADTask historicalAdTask, @@ -106,12 +106,11 @@ public GetAnomalyDetectorResponse( this.detector = detector; this.restStatus = restStatus; this.returnJob = returnJob; - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility - // if (this.returnJob) { - // this.adJob = adJob; - // } else { - // this.adJob = null; - // } + if (this.returnJob) { + this.adJob = adJob; + } else { + this.adJob = null; + } this.returnTask = returnTask; if (this.returnTask) { this.realtimeAdTask = realtimeAdTask; @@ -144,13 +143,12 @@ public void writeTo(StreamOutput out) throws IOException { out.writeLong(seqNo); out.writeEnum(restStatus); detector.writeTo(out); - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility - // if (returnJob) { - // out.writeBoolean(true); // returnJob is true - // adJob.writeTo(out); - // } else { - // out.writeBoolean(false); // returnJob is false - // } + if (returnJob) { + out.writeBoolean(true); // returnJob is true + adJob.writeTo(out); + } else { + out.writeBoolean(false); // returnJob is false + } out.writeBoolean(returnTask); if (realtimeAdTask != null) { out.writeBoolean(true); @@ -182,10 +180,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(RestHandlerUtils._PRIMARY_TERM, primaryTerm); builder.field(RestHandlerUtils._SEQ_NO, seqNo); builder.field(RestHandlerUtils.ANOMALY_DETECTOR, detector); - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility - // if (returnJob) { - // builder.field(RestHandlerUtils.ANOMALY_DETECTOR_JOB, adJob); - // } + if (returnJob) { + builder.field(RestHandlerUtils.ANOMALY_DETECTOR_JOB, adJob); + } if (returnTask) { builder.field(RestHandlerUtils.REALTIME_TASK, realtimeAdTask); builder.field(RestHandlerUtils.HISTORICAL_ANALYSIS_TASK, historicalAdTask); @@ -198,10 +195,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public DetectorProfile getDetectorProfile() { return detectorProfile; } - // @anomaly-detection.create-detector Commented this code until we have support of Get Detector for extensibility - // public AnomalyDetectorJob getAdJob() { - // return adJob; - // } + + public AnomalyDetectorJob getAdJob() { + return adJob; + } public ADTask getRealtimeAdTask() { return realtimeAdTask; diff --git a/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportAction.java b/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportAction.java index 4ba7f0ce1..b7719ab35 100644 --- a/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportAction.java +++ b/src/main/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportAction.java @@ -1,430 +1,432 @@ -// @anomaly-detection.create-detector Commented this code until we have support of Get Detector for extensibility -/// * -// * SPDX-License-Identifier: Apache-2.0 -// * -// * The OpenSearch Contributors require contributions made to -// * this file be licensed under the Apache-2.0 license or a -// * compatible open source license. -// * -// * Modifications Copyright OpenSearch Contributors. See -// * GitHub history for details. -// */ -// -// package org.opensearch.ad.transport; -// -// import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG; -// import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_GET_DETECTOR; -// import static org.opensearch.ad.model.ADTaskType.ALL_DETECTOR_TASK_TYPES; -// import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; -// import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; -// import static org.opensearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; -// import static org.opensearch.ad.util.ParseUtils.getNullUser; -// import static org.opensearch.ad.util.ParseUtils.resolveUserAndExecute; -// import static org.opensearch.ad.util.RestHandlerUtils.PROFILE; -// import static org.opensearch.ad.util.RestHandlerUtils.wrapRestActionListener; -// import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -// -// import java.util.ArrayList; -// import java.util.Arrays; -// import java.util.EnumSet; -// import java.util.HashMap; -// import java.util.HashSet; -// import java.util.List; -// import java.util.Map; -// import java.util.Optional; -// import java.util.Set; -// import java.util.stream.Collectors; -// -// import org.apache.logging.log4j.LogManager; -// import org.apache.logging.log4j.Logger; -// import org.opensearch.OpenSearchStatusException; -// import org.opensearch.action.ActionListener; -// import org.opensearch.action.get.MultiGetItemResponse; -// import org.opensearch.action.get.MultiGetRequest; -// import org.opensearch.action.get.MultiGetResponse; -// import org.opensearch.action.support.ActionFilters; -// import org.opensearch.action.support.HandledTransportAction; -// import org.opensearch.ad.AnomalyDetectorProfileRunner; -// import org.opensearch.ad.EntityProfileRunner; -// import org.opensearch.ad.Name; -// import org.opensearch.ad.auth.UserIdentity; -// import org.opensearch.ad.model.ADTask; -// import org.opensearch.ad.model.ADTaskType; -// import org.opensearch.ad.model.AnomalyDetector; -// import org.opensearch.ad.model.AnomalyDetectorJob; -// import org.opensearch.ad.model.DetectorProfile; -// import org.opensearch.ad.model.DetectorProfileName; -// import org.opensearch.ad.model.Entity; -// import org.opensearch.ad.model.EntityProfileName; -// import org.opensearch.ad.settings.AnomalyDetectorSettings; -// import org.opensearch.ad.task.ADTaskManager; -// import org.opensearch.ad.util.DiscoveryNodeFilterer; -// import org.opensearch.ad.util.RestHandlerUtils; -// import org.opensearch.client.Client; -// import org.opensearch.cluster.service.ClusterService; -// import org.opensearch.common.CheckedConsumer; -// import org.opensearch.common.Strings; -// import org.opensearch.common.inject.Inject; -// import org.opensearch.common.settings.Settings; -// import org.opensearch.common.xcontent.NamedXContentRegistry; -// import org.opensearch.common.xcontent.XContentParser; -// import org.opensearch.rest.RestStatus; -// import org.opensearch.tasks.Task; -// import org.opensearch.transport.TransportService; -// -// import com.google.common.collect.Sets; -// -// public class GetAnomalyDetectorTransportAction extends HandledTransportAction { -// -// private static final Logger LOG = LogManager.getLogger(GetAnomalyDetectorTransportAction.class); -// -// private final ClusterService clusterService; -// private final Client client; -// -// private final Set allProfileTypeStrs; -// private final Set allProfileTypes; -// private final Set defaultDetectorProfileTypes; -// private final Set allEntityProfileTypeStrs; -// private final Set allEntityProfileTypes; -// private final Set defaultEntityProfileTypes; -// private final NamedXContentRegistry xContentRegistry; -// private final DiscoveryNodeFilterer nodeFilter; -// private final TransportService transportService; -// private volatile Boolean filterByEnabled; -// private final ADTaskManager adTaskManager; -// -// @Inject -// public GetAnomalyDetectorTransportAction( -// TransportService transportService, -// DiscoveryNodeFilterer nodeFilter, -// ActionFilters actionFilters, -// ClusterService clusterService, -// Client client, -// Settings settings, -// NamedXContentRegistry xContentRegistry, -// ADTaskManager adTaskManager -// ) { -// super(GetAnomalyDetectorAction.NAME, transportService, actionFilters, GetAnomalyDetectorRequest::new); -// this.clusterService = clusterService; -// this.client = client; -// -// List allProfiles = Arrays.asList(DetectorProfileName.values()); -// this.allProfileTypes = EnumSet.copyOf(allProfiles); -// this.allProfileTypeStrs = getProfileListStrs(allProfiles); -// List defaultProfiles = Arrays.asList(DetectorProfileName.ERROR, DetectorProfileName.STATE); -// this.defaultDetectorProfileTypes = new HashSet(defaultProfiles); -// -// List allEntityProfiles = Arrays.asList(EntityProfileName.values()); -// this.allEntityProfileTypes = EnumSet.copyOf(allEntityProfiles); -// this.allEntityProfileTypeStrs = getProfileListStrs(allEntityProfiles); -// List defaultEntityProfiles = Arrays.asList(EntityProfileName.STATE); -// this.defaultEntityProfileTypes = new HashSet(defaultEntityProfiles); -// -// this.xContentRegistry = xContentRegistry; -// this.nodeFilter = nodeFilter; -// filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); -// clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); -// this.transportService = transportService; -// this.adTaskManager = adTaskManager; -// } -// -// @Override -// protected void doExecute(Task task, GetAnomalyDetectorRequest request, ActionListener actionListener) { -// String detectorID = request.getDetectorID(); -// // Temporary null user for AD extension without security. Will always execute detector. -// UserIdentity user = getNullUser(); -// ActionListener listener = wrapRestActionListener(actionListener, FAIL_TO_GET_DETECTOR); -// try { -// resolveUserAndExecute( -// user, -// detectorID, -// filterByEnabled, -// listener, -// (anomalyDetector) -> getExecute(request, listener), -// client, -// clusterService, -// xContentRegistry -// ); -// } catch (Exception e) { -// LOG.error(e); -// listener.onFailure(e); -// } -// } -// -// protected void getExecute(GetAnomalyDetectorRequest request, ActionListener listener) { -// String detectorID = request.getDetectorID(); -// String typesStr = request.getTypeStr(); -// String rawPath = request.getRawPath(); -// Entity entity = request.getEntity(); -// boolean all = request.isAll(); -// boolean returnJob = request.isReturnJob(); -// boolean returnTask = request.isReturnTask(); -// -// try { -// if (!Strings.isEmpty(typesStr) || rawPath.endsWith(PROFILE) || rawPath.endsWith(PROFILE + "/")) { -// if (entity != null) { -// Set entityProfilesToCollect = getEntityProfilesToCollect(typesStr, all); -// EntityProfileRunner profileRunner = new EntityProfileRunner( -// client, -// xContentRegistry, -// AnomalyDetectorSettings.NUM_MIN_SAMPLES -// ); -// profileRunner -// .profile( -// detectorID, -// entity, -// entityProfilesToCollect, -// ActionListener -// .wrap( -// profile -> { -// listener -// .onResponse( -// new GetAnomalyDetectorResponse( -// 0, -// null, -// 0, -// 0, -// null, -// null, -// false, -// null, -// null, -// false, -// null, -// null, -// profile, -// true -// ) -// ); -// }, -// e -> listener.onFailure(e) -// ) -// ); -// } else { -// Set profilesToCollect = getProfilesToCollect(typesStr, all); -// AnomalyDetectorProfileRunner profileRunner = new AnomalyDetectorProfileRunner( -// client, -// xContentRegistry, -// nodeFilter, -// AnomalyDetectorSettings.NUM_MIN_SAMPLES, -// transportService, -// adTaskManager -// ); -// profileRunner.profile(detectorID, getProfileActionListener(listener), profilesToCollect); -// } -// } else { -// if (returnTask) { -// adTaskManager.getAndExecuteOnLatestADTasks(detectorID, null, null, ALL_DETECTOR_TASK_TYPES, (taskList) -> { -// Optional realtimeAdTask = Optional.empty(); -// Optional historicalAdTask = Optional.empty(); -// -// if (taskList != null && taskList.size() > 0) { -// Map adTasks = new HashMap<>(); -// List duplicateAdTasks = new ArrayList<>(); -// for (ADTask task : taskList) { -// if (adTasks.containsKey(task.getTaskType())) { -// LOG -// .info( -// "Found duplicate latest task of detector {}, task id: {}, task type: {}", -// detectorID, -// task.getTaskType(), -// task.getTaskId() -// ); -// duplicateAdTasks.add(task); -// continue; -// } -// adTasks.put(task.getTaskType(), task); -// } -// if (duplicateAdTasks.size() > 0) { -// adTaskManager.resetLatestFlagAsFalse(duplicateAdTasks); -// } -// -// if (adTasks.containsKey(ADTaskType.REALTIME_HC_DETECTOR.name())) { -// realtimeAdTask = Optional.ofNullable(adTasks.get(ADTaskType.REALTIME_HC_DETECTOR.name())); -// } else if (adTasks.containsKey(ADTaskType.REALTIME_SINGLE_ENTITY.name())) { -// realtimeAdTask = Optional.ofNullable(adTasks.get(ADTaskType.REALTIME_SINGLE_ENTITY.name())); -// } -// if (adTasks.containsKey(ADTaskType.HISTORICAL_HC_DETECTOR.name())) { -// historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL_HC_DETECTOR.name())); -// } else if (adTasks.containsKey(ADTaskType.HISTORICAL_SINGLE_ENTITY.name())) { -// historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL_SINGLE_ENTITY.name())); -// } else if (adTasks.containsKey(ADTaskType.HISTORICAL.name())) { -// historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL.name())); -// } -// } -// getDetectorAndJob(detectorID, returnJob, returnTask, realtimeAdTask, historicalAdTask, listener); -// }, transportService, true, 2, listener); -// } else { -// getDetectorAndJob(detectorID, returnJob, returnTask, Optional.empty(), Optional.empty(), listener); -// } -// } -// } catch (Exception e) { -// LOG.error(e); -// listener.onFailure(e); -// } -// } -// -// private void getDetectorAndJob( -// String detectorID, -// boolean returnJob, -// boolean returnTask, -// Optional realtimeAdTask, -// Optional historicalAdTask, -// ActionListener listener -// ) { -// MultiGetRequest.Item adItem = new MultiGetRequest.Item(ANOMALY_DETECTORS_INDEX, detectorID); -// MultiGetRequest multiGetRequest = new MultiGetRequest().add(adItem); -// if (returnJob) { -// MultiGetRequest.Item adJobItem = new MultiGetRequest.Item(ANOMALY_DETECTOR_JOB_INDEX, detectorID); -// multiGetRequest.add(adJobItem); -// } -// client.multiGet(multiGetRequest, onMultiGetResponse(listener, returnJob, returnTask, realtimeAdTask, historicalAdTask, detectorID)); -// } -// -// private ActionListener onMultiGetResponse( -// ActionListener listener, -// boolean returnJob, -// boolean returnTask, -// Optional realtimeAdTask, -// Optional historicalAdTask, -// String detectorId -// ) { -// return new ActionListener() { -// @Override -// public void onResponse(MultiGetResponse multiGetResponse) { -// MultiGetItemResponse[] responses = multiGetResponse.getResponses(); -// AnomalyDetector detector = null; -// AnomalyDetectorJob adJob = null; -// String id = null; -// long version = 0; -// long seqNo = 0; -// long primaryTerm = 0; -// -// for (MultiGetItemResponse response : responses) { -// if (ANOMALY_DETECTORS_INDEX.equals(response.getIndex())) { -// if (response.getResponse() == null || !response.getResponse().isExists()) { -// listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, RestStatus.NOT_FOUND)); -// return; -// } -// id = response.getId(); -// version = response.getResponse().getVersion(); -// primaryTerm = response.getResponse().getPrimaryTerm(); -// seqNo = response.getResponse().getSeqNo(); -// if (!response.getResponse().isSourceEmpty()) { -// try ( -// XContentParser parser = RestHandlerUtils -// .createXContentParserFromRegistry(xContentRegistry, response.getResponse().getSourceAsBytesRef()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); -// detector = parser.namedObject(AnomalyDetector.class, AnomalyDetector.PARSE_FIELD_NAME, null); -// } catch (Exception e) { -// String message = "Failed to parse detector job " + detectorId; -// listener.onFailure(buildInternalServerErrorResponse(e, message)); -// return; -// } -// } -// } -// -// if (ANOMALY_DETECTOR_JOB_INDEX.equals(response.getIndex())) { -// if (response.getResponse() != null -// && response.getResponse().isExists() -// && !response.getResponse().isSourceEmpty()) { -// try ( -// XContentParser parser = RestHandlerUtils -// .createXContentParserFromRegistry(xContentRegistry, response.getResponse().getSourceAsBytesRef()) -// ) { -// ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); -// adJob = AnomalyDetectorJob.parse(parser); -// } catch (Exception e) { -// String message = "Failed to parse detector job " + detectorId; -// listener.onFailure(buildInternalServerErrorResponse(e, message)); -// return; -// } -// } -// } -// } -// listener -// .onResponse( -// new GetAnomalyDetectorResponse( -// version, -// id, -// primaryTerm, -// seqNo, -// detector, -// adJob, -// returnJob, -// realtimeAdTask.orElse(null), -// historicalAdTask.orElse(null), -// returnTask, -// RestStatus.OK, -// null, -// null, -// false -// ) -// ); -// } -// -// @Override -// public void onFailure(Exception e) { -// listener.onFailure(e); -// } -// }; -// } -// -// private ActionListener getProfileActionListener(ActionListener listener) { -// return ActionListener.wrap(new CheckedConsumer() { -// @Override -// public void accept(DetectorProfile profile) throws Exception { -// listener -// .onResponse( -// new GetAnomalyDetectorResponse(0, null, 0, 0, null, null, false, null, null, false, null, profile, null, true) -// ); -// } -// }, exception -> { listener.onFailure(exception); }); -// } -// -// private OpenSearchStatusException buildInternalServerErrorResponse(Exception e, String errorMsg) { -// LOG.error(errorMsg, e); -// return new OpenSearchStatusException(errorMsg, RestStatus.INTERNAL_SERVER_ERROR); -// } -// -// /** -// * -// * @param typesStr a list of input profile types separated by comma -// * @param all whether we should return all profile in the response -// * @return profiles to collect for a detector -// */ -// private Set getProfilesToCollect(String typesStr, boolean all) { -// if (all) { -// return this.allProfileTypes; -// } else if (Strings.isEmpty(typesStr)) { -// return this.defaultDetectorProfileTypes; -// } else { -// // Filter out unsupported types -// Set typesInRequest = new HashSet<>(Arrays.asList(typesStr.split(","))); -// return DetectorProfileName.getNames(Sets.intersection(allProfileTypeStrs, typesInRequest)); -// } -// } -// -// /** -// * -// * @param typesStr a list of input profile types separated by comma -// * @param all whether we should return all profile in the response -// * @return profiles to collect for an entity -// */ -// private Set getEntityProfilesToCollect(String typesStr, boolean all) { -// if (all) { -// return this.allEntityProfileTypes; -// } else if (Strings.isEmpty(typesStr)) { -// return this.defaultEntityProfileTypes; -// } else { -// // Filter out unsupported types -// Set typesInRequest = new HashSet<>(Arrays.asList(typesStr.split(","))); -// return EntityProfileName.getNames(Sets.intersection(allEntityProfileTypeStrs, typesInRequest)); -// } -// } -// -// private Set getProfileListStrs(List profileList) { -// return profileList.stream().map(profile -> profile.getName()).collect(Collectors.toSet()); -// } -// } +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.ad.transport; + +import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG; +import static org.opensearch.ad.constant.CommonErrorMessages.FAIL_TO_GET_DETECTOR; +import static org.opensearch.ad.model.ADTaskType.ALL_DETECTOR_TASK_TYPES; +import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; +import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; +import static org.opensearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; +import static org.opensearch.ad.util.ParseUtils.getNullUser; +import static org.opensearch.ad.util.ParseUtils.resolveUserAndExecute; +import static org.opensearch.ad.util.RestHandlerUtils.PROFILE; +import static org.opensearch.ad.util.RestHandlerUtils.wrapRestActionListener; +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchStatusException; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.ad.AnomalyDetectorProfileRunner; +import org.opensearch.ad.EntityProfileRunner; +import org.opensearch.ad.Name; +import org.opensearch.ad.auth.UserIdentity; +import org.opensearch.ad.model.ADTask; +import org.opensearch.ad.model.ADTaskType; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; +import org.opensearch.ad.model.DetectorProfile; +import org.opensearch.ad.model.DetectorProfileName; +import org.opensearch.ad.model.Entity; +import org.opensearch.ad.model.EntityProfileName; +import org.opensearch.ad.settings.AnomalyDetectorSettings; +import org.opensearch.ad.task.ADTaskManager; +import org.opensearch.ad.util.DiscoveryNodeFilterer; +import org.opensearch.ad.util.RestHandlerUtils; +import org.opensearch.common.CheckedConsumer; +import org.opensearch.common.Strings; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.rest.RestStatus; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.sdk.SDKClusterService; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import com.google.common.collect.Sets; + +public class GetAnomalyDetectorTransportAction { + // extends HandledTransportAction { + + private static final Logger LOG = LogManager.getLogger(GetAnomalyDetectorTransportAction.class); + + private final SDKClusterService clusterService; + private final SDKRestClient client; + + private final Set allProfileTypeStrs; + private final Set allProfileTypes; + private final Set defaultDetectorProfileTypes; + private final Set allEntityProfileTypeStrs; + private final Set allEntityProfileTypes; + private final Set defaultEntityProfileTypes; + private final NamedXContentRegistry xContentRegistry; + private final DiscoveryNodeFilterer nodeFilter; + private final TransportService transportService; + private volatile Boolean filterByEnabled; + private final ADTaskManager adTaskManager; + + @Inject + public GetAnomalyDetectorTransportAction( + TransportService transportService, + DiscoveryNodeFilterer nodeFilter, + ActionFilters actionFilters, + SDKClusterService clusterService, + SDKRestClient client, + Settings settings, + NamedXContentRegistry xContentRegistry, + ADTaskManager adTaskManager + ) { + // super(GetAnomalyDetectorAction.NAME, transportService, actionFilters, GetAnomalyDetectorRequest::new); + this.clusterService = clusterService; + this.client = client; + + List allProfiles = Arrays.asList(DetectorProfileName.values()); + this.allProfileTypes = EnumSet.copyOf(allProfiles); + this.allProfileTypeStrs = getProfileListStrs(allProfiles); + List defaultProfiles = Arrays.asList(DetectorProfileName.ERROR, DetectorProfileName.STATE); + this.defaultDetectorProfileTypes = new HashSet(defaultProfiles); + + List allEntityProfiles = Arrays.asList(EntityProfileName.values()); + this.allEntityProfileTypes = EnumSet.copyOf(allEntityProfiles); + this.allEntityProfileTypeStrs = getProfileListStrs(allEntityProfiles); + List defaultEntityProfiles = Arrays.asList(EntityProfileName.STATE); + this.defaultEntityProfileTypes = new HashSet(defaultEntityProfiles); + + this.xContentRegistry = xContentRegistry; + this.nodeFilter = nodeFilter; + filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); + this.transportService = transportService; + this.adTaskManager = adTaskManager; + } + + public void doExecute(Task task, GetAnomalyDetectorRequest request, ActionListener actionListener) { + String detectorID = request.getDetectorID(); + // Temporary null user for AD extension without security. Will always execute detector. + UserIdentity user = getNullUser(); + ActionListener listener = wrapRestActionListener(actionListener, FAIL_TO_GET_DETECTOR); + try { + resolveUserAndExecute( + user, + detectorID, + filterByEnabled, + listener, + (anomalyDetector) -> getExecute(request, listener), + client, + clusterService, + xContentRegistry + ); + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + // FIXME Handle this + // Will be covered after Job Scheduler has been integrated + protected void getExecute(GetAnomalyDetectorRequest request, ActionListener listener) { + String detectorID = request.getDetectorID(); + String typesStr = request.getTypeStr(); + String rawPath = request.getRawPath(); + Entity entity = request.getEntity(); + boolean all = request.isAll(); + boolean returnJob = request.isReturnJob(); + boolean returnTask = request.isReturnTask(); + + try { + if (!Strings.isEmpty(typesStr) || rawPath.endsWith(PROFILE) || rawPath.endsWith(PROFILE + "/")) { + if (entity != null) { + Set entityProfilesToCollect = getEntityProfilesToCollect(typesStr, all); + EntityProfileRunner profileRunner = new EntityProfileRunner( + client, + xContentRegistry, + AnomalyDetectorSettings.NUM_MIN_SAMPLES + ); + profileRunner + .profile( + detectorID, + entity, + entityProfilesToCollect, + ActionListener + .wrap( + profile -> { + listener + .onResponse( + new GetAnomalyDetectorResponse( + 0, + null, + 0, + 0, + null, + null, + false, + null, + null, + false, + null, + null, + profile, + true + ) + ); + }, + e -> listener.onFailure(e) + ) + ); + } else { + Set profilesToCollect = getProfilesToCollect(typesStr, all); + AnomalyDetectorProfileRunner profileRunner = new AnomalyDetectorProfileRunner( + client, + xContentRegistry, + nodeFilter, + AnomalyDetectorSettings.NUM_MIN_SAMPLES, + transportService, + adTaskManager + ); + profileRunner.profile(detectorID, getProfileActionListener(listener), profilesToCollect); + } + } else { + if (returnTask) { + adTaskManager.getAndExecuteOnLatestADTasks(detectorID, null, null, ALL_DETECTOR_TASK_TYPES, (taskList) -> { + Optional realtimeAdTask = Optional.empty(); + Optional historicalAdTask = Optional.empty(); + + if (taskList != null && taskList.size() > 0) { + Map adTasks = new HashMap<>(); + List duplicateAdTasks = new ArrayList<>(); + for (ADTask task : taskList) { + if (adTasks.containsKey(task.getTaskType())) { + LOG + .info( + "Found duplicate latest task of detector {}, task id: {}, task type: {}", + detectorID, + task.getTaskType(), + task.getTaskId() + ); + duplicateAdTasks.add(task); + continue; + } + adTasks.put(task.getTaskType(), task); + } + if (duplicateAdTasks.size() > 0) { + adTaskManager.resetLatestFlagAsFalse(duplicateAdTasks); + } + + if (adTasks.containsKey(ADTaskType.REALTIME_HC_DETECTOR.name())) { + realtimeAdTask = Optional.ofNullable(adTasks.get(ADTaskType.REALTIME_HC_DETECTOR.name())); + } else if (adTasks.containsKey(ADTaskType.REALTIME_SINGLE_ENTITY.name())) { + realtimeAdTask = Optional.ofNullable(adTasks.get(ADTaskType.REALTIME_SINGLE_ENTITY.name())); + } + if (adTasks.containsKey(ADTaskType.HISTORICAL_HC_DETECTOR.name())) { + historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL_HC_DETECTOR.name())); + } else if (adTasks.containsKey(ADTaskType.HISTORICAL_SINGLE_ENTITY.name())) { + historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL_SINGLE_ENTITY.name())); + } else if (adTasks.containsKey(ADTaskType.HISTORICAL.name())) { + historicalAdTask = Optional.ofNullable(adTasks.get(ADTaskType.HISTORICAL.name())); + } + } + getDetectorAndJob(detectorID, returnJob, returnTask, realtimeAdTask, historicalAdTask, listener); + }, transportService, true, 2, listener); + } else { + getDetectorAndJob(detectorID, returnJob, returnTask, Optional.empty(), Optional.empty(), listener); + } + } + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + private void getDetectorAndJob( + String detectorID, + boolean returnJob, + boolean returnTask, + Optional realtimeAdTask, + Optional historicalAdTask, + ActionListener listener + ) { + MultiGetRequest.Item adItem = new MultiGetRequest.Item(ANOMALY_DETECTORS_INDEX, detectorID); + MultiGetRequest multiGetRequest = new MultiGetRequest().add(adItem); + if (returnJob) { + MultiGetRequest.Item adJobItem = new MultiGetRequest.Item(ANOMALY_DETECTOR_JOB_INDEX, detectorID); + multiGetRequest.add(adJobItem); + } + client.multiGet(multiGetRequest, onMultiGetResponse(listener, returnJob, returnTask, realtimeAdTask, historicalAdTask, detectorID)); + } + + private ActionListener onMultiGetResponse( + ActionListener listener, + boolean returnJob, + boolean returnTask, + Optional realtimeAdTask, + Optional historicalAdTask, + String detectorId + ) { + return new ActionListener() { + @Override + public void onResponse(MultiGetResponse multiGetResponse) { + MultiGetItemResponse[] responses = multiGetResponse.getResponses(); + AnomalyDetector detector = null; + AnomalyDetectorJob adJob = null; + String id = null; + long version = 0; + long seqNo = 0; + long primaryTerm = 0; + + for (MultiGetItemResponse response : responses) { + if (ANOMALY_DETECTORS_INDEX.equals(response.getIndex())) { + if (response.getResponse() == null || !response.getResponse().isExists()) { + listener.onFailure(new OpenSearchStatusException(FAIL_TO_FIND_DETECTOR_MSG + detectorId, RestStatus.NOT_FOUND)); + return; + } + id = response.getId(); + version = response.getResponse().getVersion(); + primaryTerm = response.getResponse().getPrimaryTerm(); + seqNo = response.getResponse().getSeqNo(); + if (!response.getResponse().isSourceEmpty()) { + try ( + XContentParser parser = RestHandlerUtils + .createXContentParserFromRegistry(xContentRegistry, response.getResponse().getSourceAsBytesRef()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + detector = parser.namedObject(AnomalyDetector.class, AnomalyDetector.PARSE_FIELD_NAME, null); + } catch (Exception e) { + String message = "Failed to parse detector job " + detectorId; + listener.onFailure(buildInternalServerErrorResponse(e, message)); + return; + } + } + } + + if (ANOMALY_DETECTOR_JOB_INDEX.equals(response.getIndex())) { + if (response.getResponse() != null + && response.getResponse().isExists() + && !response.getResponse().isSourceEmpty()) { + try ( + XContentParser parser = RestHandlerUtils + .createXContentParserFromRegistry(xContentRegistry, response.getResponse().getSourceAsBytesRef()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + adJob = AnomalyDetectorJob.parse(parser); + } catch (Exception e) { + String message = "Failed to parse detector job " + detectorId; + listener.onFailure(buildInternalServerErrorResponse(e, message)); + return; + } + } + } + } + listener + .onResponse( + new GetAnomalyDetectorResponse( + version, + id, + primaryTerm, + seqNo, + detector, + adJob, + returnJob, + realtimeAdTask.orElse(null), + historicalAdTask.orElse(null), + returnTask, + RestStatus.OK, + null, + null, + false + ) + ); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }; + } + + // FIXME Handle this + // Will be covered after Job Scheduler has been integrated + private ActionListener getProfileActionListener(ActionListener listener) { + return ActionListener.wrap(new CheckedConsumer() { + @Override + public void accept(DetectorProfile profile) throws Exception { + listener + .onResponse( + new GetAnomalyDetectorResponse(0, null, 0, 0, null, null, false, null, null, false, null, profile, null, true) + ); + } + }, exception -> { listener.onFailure(exception); }); + } + + private OpenSearchStatusException buildInternalServerErrorResponse(Exception e, String errorMsg) { + LOG.error(errorMsg, e); + return new OpenSearchStatusException(errorMsg, RestStatus.INTERNAL_SERVER_ERROR); + } + + /** + * + * @param typesStr a list of input profile types separated by comma + * @param all whether we should return all profile in the response + * @return profiles to collect for a detector + */ + private Set getProfilesToCollect(String typesStr, boolean all) { + if (all) { + return this.allProfileTypes; + } else if (Strings.isEmpty(typesStr)) { + return this.defaultDetectorProfileTypes; + } else { + // Filter out unsupported types + Set typesInRequest = new HashSet<>(Arrays.asList(typesStr.split(","))); + return DetectorProfileName.getNames(Sets.intersection(allProfileTypeStrs, typesInRequest)); + } + } + + /** + * + * @param typesStr a list of input profile types separated by comma + * @param all whether we should return all profile in the response + * @return profiles to collect for an entity + */ + private Set getEntityProfilesToCollect(String typesStr, boolean all) { + if (all) { + return this.allEntityProfileTypes; + } else if (Strings.isEmpty(typesStr)) { + return this.defaultEntityProfileTypes; + } else { + // Filter out unsupported types + Set typesInRequest = new HashSet<>(Arrays.asList(typesStr.split(","))); + return EntityProfileName.getNames(Sets.intersection(allEntityProfileTypeStrs, typesInRequest)); + } + } + + private Set getProfileListStrs(List profileList) { + return profileList.stream().map(profile -> profile.getName()).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/org/opensearch/ad/transport/IndexAnomalyDetectorTransportAction.java b/src/main/java/org/opensearch/ad/transport/IndexAnomalyDetectorTransportAction.java index 00380c884..b570948dc 100644 --- a/src/main/java/org/opensearch/ad/transport/IndexAnomalyDetectorTransportAction.java +++ b/src/main/java/org/opensearch/ad/transport/IndexAnomalyDetectorTransportAction.java @@ -81,12 +81,7 @@ public IndexAnomalyDetectorTransportAction( this.adTaskManager = adTaskManager; this.searchFeatureDao = searchFeatureDao; filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); - try { - sdkClusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); - } catch (Exception e) { - // FIXME Handle this - // https://github.com/opensearch-project/opensearch-sdk-java/issues/422 - } + sdkClusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); } // FIXME Investigate whether we should inherit from TransportAction diff --git a/src/test/java/org/opensearch/ad/AbstractProfileRunnerTests.java b/src/test/java/org/opensearch/ad/AbstractProfileRunnerTests.java index 8105d25f7..b9c2af012 100644 --- a/src/test/java/org/opensearch/ad/AbstractProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/AbstractProfileRunnerTests.java @@ -10,8 +10,38 @@ * GitHub history for details. */ -/*package org.opensearch.ad; - +package org.opensearch.ad; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.opensearch.Version; +import org.opensearch.action.get.GetResponse; +import org.opensearch.ad.model.ADTask; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.DetectorProfileName; +import org.opensearch.ad.task.ADTaskManager; +import org.opensearch.ad.util.DiscoveryNodeFilterer; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.transport.TransportService; public class AbstractProfileRunnerTests extends AbstractADTest { protected enum DetectorStatus { @@ -35,7 +65,7 @@ protected enum ErrorResultStatus { } protected AnomalyDetectorProfileRunner runner; - protected Client client; + protected SDKRestClient client; protected DiscoveryNodeFilterer nodeFilter; protected AnomalyDetector detector; protected ClusterService clusterService; @@ -119,7 +149,7 @@ public static void setUpOnce() { @Before public void setUp() throws Exception { super.setUp(); - client = mock(Client.class); + client = mock(SDKRestClient.class); nodeFilter = mock(DiscoveryNodeFilterer.class); clusterService = mock(ClusterService.class); adTaskManager = mock(ADTaskManager.class); @@ -140,4 +170,4 @@ public void setUp() throws Exception { detectorGetReponse = mock(GetResponse.class); } -}*/ +} diff --git a/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java b/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java index bb5f8afc1..3fcc76a1e 100644 --- a/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java @@ -11,9 +11,39 @@ package org.opensearch.ad; -/* +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; +import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; +import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.apache.lucene.index.IndexNotFoundException; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; +import org.opensearch.ad.model.ADTask; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; +import org.opensearch.ad.model.DetectorInternalState; +import org.opensearch.ad.model.DetectorProfile; +import org.opensearch.ad.model.DetectorProfileName; +import org.opensearch.ad.model.DetectorState; +import org.opensearch.ad.model.InitProgressProfile; +import org.opensearch.ad.model.IntervalTimeConfiguration; + +// @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility public class AnomalyDetectorProfileRunnerTests extends AbstractProfileRunnerTests { enum RCFPollingStatus { INIT_NOT_EXIT, @@ -28,371 +58,377 @@ enum RCFPollingStatus { private Instant jobEnabledTime = Instant.now().minus(1, ChronoUnit.DAYS); - */ -/** + /** * Convenience methods for single-stream detector profile tests set up * @param detectorStatus Detector config status * @param jobStatus Detector job status * @param rcfPollingStatus RCF polling result status * @param errorResultStatus Error result status * @throws IOException when failing the getting request - *//* - // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility - @SuppressWarnings("unchecked") - private void setUpClientGet( - DetectorStatus detectorStatus, - JobStatus jobStatus, - RCFPollingStatus rcfPollingStatus, - ErrorResultStatus errorResultStatus - ) throws IOException { - detector = TestHelpers.randomAnomalyDetectorWithInterval(new IntervalTimeConfiguration(detectorIntervalMin, ChronoUnit.MINUTES)); - - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - GetRequest request = (GetRequest) args[0]; - ActionListener listener = (ActionListener) args[1]; - - if (request.index().equals(ANOMALY_DETECTORS_INDEX)) { - switch (detectorStatus) { - case EXIST: - listener - .onResponse( - TestHelpers.createGetResponse(detector, detector.getDetectorId(), AnomalyDetector.ANOMALY_DETECTORS_INDEX) - ); - break; - case INDEX_NOT_EXIST: - listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTORS_INDEX)); - break; - case NO_DOC: - when(detectorGetReponse.isExists()).thenReturn(false); - listener.onResponse(detectorGetReponse); - break; - default: - assertTrue("should not reach here", false); - break; - } - } else if (request.index().equals(ANOMALY_DETECTOR_JOB_INDEX)) { - AnomalyDetectorJob job = null; - switch (jobStatus) { - case INDEX_NOT_EXIT: - listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTOR_JOB_INDEX)); - break; - case DISABLED: - job = TestHelpers.randomAnomalyDetectorJob(false, jobEnabledTime, null); - listener - .onResponse( - TestHelpers.createGetResponse(job, detector.getDetectorId(), AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX) - ); - break; - case ENABLED: - job = TestHelpers.randomAnomalyDetectorJob(true, jobEnabledTime, null); - listener - .onResponse( - TestHelpers.createGetResponse(job, detector.getDetectorId(), AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX) - ); - break; - default: - assertTrue("should not reach here", false); - break; - } - } else { - if (errorResultStatus == ErrorResultStatus.INDEX_NOT_EXIT) { - listener.onFailure(new IndexNotFoundException(CommonName.DETECTION_STATE_INDEX)); - return null; - } - DetectorInternalState.Builder result = new DetectorInternalState.Builder().lastUpdateTime(Instant.now()); - - String error = getError(errorResultStatus); - if (error != null) { - result.error(error); - } - listener - .onResponse(TestHelpers.createGetResponse(result.build(), detector.getDetectorId(), CommonName.DETECTION_STATE_INDEX)); - - } - - return null; - }).when(client).get(any(), any()); - - setUpClientExecuteRCFPollingAction(rcfPollingStatus); - } - - private String getError(ErrorResultStatus errorResultStatus) { - switch (errorResultStatus) { - case NO_ERROR: - break; - case SHINGLE_ERROR: - return noFullShingleError; - case STOPPED_ERROR: - return stoppedError; - default: - assertTrue("should not reach here", false); - break; - } - return null; - } - - public void testDetectorNotExist() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.INDEX_NOT_EXIST, JobStatus.INDEX_NOT_EXIT, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile("x123", ActionListener.wrap(response -> { - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - }, exception -> { - assertTrue(exception.getMessage().contains(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG)); - inProgressLatch.countDown(); - }), stateNError); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testDisabledJobIndexTemplate(JobStatus status) throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.EXIST, status, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.DISABLED).build(); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), stateOnly); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testNoJobIndex() throws IOException, InterruptedException { - testDisabledJobIndexTemplate(JobStatus.INDEX_NOT_EXIT); - } - - public void testJobDisabled() throws IOException, InterruptedException { - testDisabledJobIndexTemplate(JobStatus.DISABLED); - } - - public void testInitOrRunningStateTemplate(RCFPollingStatus status, DetectorState expectedState) throws IOException, - InterruptedException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, status, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder().state(expectedState).build(); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - logger.error(exception); - for (StackTraceElement ste : exception.getStackTrace()) { - logger.info(ste); - } - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), stateOnly); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testResultNotExist() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.INIT_NOT_EXIT, DetectorState.INIT); - } - - public void testRemoteResultNotExist() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.REMOTE_INIT_NOT_EXIT, DetectorState.INIT); - } - - public void testCheckpointIndexNotExist() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.INDEX_NOT_FOUND, DetectorState.INIT); - } - - public void testRemoteCheckpointIndexNotExist() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.REMOTE_INDEX_NOT_FOUND, DetectorState.INIT); - } - - public void testResultEmpty() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.EMPTY, DetectorState.INIT); - } - - public void testResultGreaterThanZero() throws IOException, InterruptedException { - testInitOrRunningStateTemplate(RCFPollingStatus.INIT_DONE, DetectorState.RUNNING); - } - - @SuppressWarnings("unchecked") - public void testErrorStateTemplate( - RCFPollingStatus initStatus, - ErrorResultStatus status, - DetectorState state, - String error, - JobStatus jobStatus, - Set profilesToCollect - ) throws IOException, - InterruptedException { - ADTask adTask = TestHelpers.randomAdTask(); - - adTask.setError(getError(status)); - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - Consumer> function = (Consumer>) args[2]; - function.accept(Optional.of(adTask)); - return null; - }).when(adTaskManager).getAndExecuteOnLatestDetectorLevelTask(any(), any(), any(), any(), anyBoolean(), any()); - - setUpClientExecuteRCFPollingAction(initStatus); - setUpClientGet(DetectorStatus.EXIST, jobStatus, initStatus, status); - DetectorProfile.Builder builder = new DetectorProfile.Builder(); - if (profilesToCollect.contains(DetectorProfileName.STATE)) { - builder.state(state); - } - if (profilesToCollect.contains(DetectorProfileName.ERROR)) { - builder.error(error); - } - DetectorProfile expectedProfile = builder.build(); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - logger.info(exception); - for (StackTraceElement ste : exception.getStackTrace()) { - logger.info(ste); - } - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - }), profilesToCollect); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + */ + // @anomaly-detection.create-detector Commented this code until we have support of Job Scheduler for extensibility + @SuppressWarnings("unchecked") + private void setUpClientGet( + DetectorStatus detectorStatus, + JobStatus jobStatus, + RCFPollingStatus rcfPollingStatus, + ErrorResultStatus errorResultStatus + ) throws IOException { + detector = TestHelpers.randomAnomalyDetectorWithInterval(new IntervalTimeConfiguration(detectorIntervalMin, ChronoUnit.MINUTES)); + + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + GetRequest request = (GetRequest) args[0]; + ActionListener listener = (ActionListener) args[1]; + + if (request.index().equals(ANOMALY_DETECTORS_INDEX)) { + switch (detectorStatus) { + case EXIST: + listener + .onResponse( + TestHelpers.createGetResponse(detector, detector.getDetectorId(), AnomalyDetector.ANOMALY_DETECTORS_INDEX) + ); + break; + case INDEX_NOT_EXIST: + listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTORS_INDEX)); + break; + case NO_DOC: + when(detectorGetReponse.isExists()).thenReturn(false); + listener.onResponse(detectorGetReponse); + break; + default: + assertTrue("should not reach here", false); + break; + } + } else if (request.index().equals(ANOMALY_DETECTOR_JOB_INDEX)) { + AnomalyDetectorJob job = null; + switch (jobStatus) { + case INDEX_NOT_EXIT: + listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTOR_JOB_INDEX)); + break; + case DISABLED: + job = TestHelpers.randomAnomalyDetectorJob(false, jobEnabledTime, null); + listener.onResponse(TestHelpers.createGetResponse(job, detector.getDetectorId(), ANOMALY_DETECTOR_JOB_INDEX)); + break; + case ENABLED: + job = TestHelpers.randomAnomalyDetectorJob(true, jobEnabledTime, null); + listener.onResponse(TestHelpers.createGetResponse(job, detector.getDetectorId(), ANOMALY_DETECTOR_JOB_INDEX)); + break; + default: + assertTrue("should not reach here", false); + break; + } + } else { + if (errorResultStatus == ErrorResultStatus.INDEX_NOT_EXIT) { + listener.onFailure(new IndexNotFoundException(CommonName.DETECTION_STATE_INDEX)); + return null; + } + DetectorInternalState.Builder result = new DetectorInternalState.Builder().lastUpdateTime(Instant.now()); + + String error = getError(errorResultStatus); + if (error != null) { + result.error(error); + } + listener + .onResponse(TestHelpers.createGetResponse(result.build(), detector.getDetectorId(), CommonName.DETECTION_STATE_INDEX)); + + } + + return null; + }).when(client).get(any(), any()); + + // setUpClientExecuteRCFPollingAction(rcfPollingStatus); + } + + private String getError(ErrorResultStatus errorResultStatus) { + switch (errorResultStatus) { + case NO_ERROR: + break; + case SHINGLE_ERROR: + return noFullShingleError; + case STOPPED_ERROR: + return stoppedError; + default: + assertTrue("should not reach here", false); + break; } - - public void testErrorStateTemplate( - RCFPollingStatus initStatus, - ErrorResultStatus status, - DetectorState state, - String error, - JobStatus jobStatus - ) throws IOException, - InterruptedException { - testErrorStateTemplate(initStatus, status, state, error, jobStatus, stateNError); + return null; + } + + public void testDetectorNotExist() throws IOException, InterruptedException { + setUpClientGet(DetectorStatus.INDEX_NOT_EXIST, JobStatus.INDEX_NOT_EXIT, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile("x123", ActionListener.wrap(response -> { + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }, exception -> { + assertTrue(exception.getMessage().contains(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG)); + inProgressLatch.countDown(); + }), stateNError); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + public void testDisabledJobIndexTemplate(JobStatus status) throws IOException, InterruptedException { + setUpClientGet(DetectorStatus.EXIST, status, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); + DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.DISABLED).build(); + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here ", false); + inProgressLatch.countDown(); + }), stateOnly); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + // public void testNoJobIndex() throws IOException, InterruptedException { + // testDisabledJobIndexTemplate(JobStatus.INDEX_NOT_EXIT); + // } + + // public void testJobDisabled() throws IOException, InterruptedException { + // testDisabledJobIndexTemplate(JobStatus.DISABLED); + // } + + public void testInitOrRunningStateTemplate(RCFPollingStatus status, DetectorState expectedState) throws IOException, + InterruptedException { + setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, status, ErrorResultStatus.NO_ERROR); + DetectorProfile expectedProfile = new DetectorProfile.Builder().state(expectedState).build(); + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + logger.error(exception); + for (StackTraceElement ste : exception.getStackTrace()) { + logger.info(ste); + } + assertTrue("Should not reach here ", false); + inProgressLatch.countDown(); + }), stateOnly); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + // FIXME part of get detector with job scheduler implementation + // public void testResultNotExist() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.INIT_NOT_EXIT, DetectorState.INIT); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testRemoteResultNotExist() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.REMOTE_INIT_NOT_EXIT, DetectorState.INIT); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testCheckpointIndexNotExist() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.INDEX_NOT_FOUND, DetectorState.INIT); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testRemoteCheckpointIndexNotExist() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.REMOTE_INDEX_NOT_FOUND, DetectorState.INIT); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testResultEmpty() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.EMPTY, DetectorState.INIT); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testResultGreaterThanZero() throws IOException, InterruptedException { + // testInitOrRunningStateTemplate(RCFPollingStatus.INIT_DONE, DetectorState.RUNNING); + // } + + @SuppressWarnings("unchecked") + public void testErrorStateTemplate( + RCFPollingStatus initStatus, + ErrorResultStatus status, + DetectorState state, + String error, + JobStatus jobStatus, + Set profilesToCollect + ) throws IOException, + InterruptedException { + ADTask adTask = TestHelpers.randomAdTask(); + + adTask.setError(getError(status)); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + Consumer> function = (Consumer>) args[2]; + function.accept(Optional.of(adTask)); + return null; + }).when(adTaskManager).getAndExecuteOnLatestDetectorLevelTask(any(), any(), any(), any(), anyBoolean(), any()); + + // setUpClientExecuteRCFPollingAction(initStatus); + setUpClientGet(DetectorStatus.EXIST, jobStatus, initStatus, status); + DetectorProfile.Builder builder = new DetectorProfile.Builder(); + if (profilesToCollect.contains(DetectorProfileName.STATE)) { + builder.state(state); } - - public void testRunningNoError() throws IOException, InterruptedException { - testErrorStateTemplate(RCFPollingStatus.INIT_DONE, ErrorResultStatus.NO_ERROR, DetectorState.RUNNING, null, JobStatus.ENABLED); + if (profilesToCollect.contains(DetectorProfileName.ERROR)) { + builder.error(error); } - - public void testRunningWithError() throws IOException, InterruptedException { - testErrorStateTemplate( - RCFPollingStatus.INIT_DONE, - ErrorResultStatus.SHINGLE_ERROR, - DetectorState.RUNNING, - noFullShingleError, - JobStatus.ENABLED + DetectorProfile expectedProfile = builder.build(); + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + logger.info(exception); + for (StackTraceElement ste : exception.getStackTrace()) { + logger.info(ste); + } + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }), profilesToCollect); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + public void testErrorStateTemplate( + RCFPollingStatus initStatus, + ErrorResultStatus status, + DetectorState state, + String error, + JobStatus jobStatus + ) throws IOException, + InterruptedException { + testErrorStateTemplate(initStatus, status, state, error, jobStatus, stateNError); + } + + // FIXME part of get detector with job scheduler implementation + // public void testRunningNoError() throws IOException, InterruptedException { + // testErrorStateTemplate(RCFPollingStatus.INIT_DONE, ErrorResultStatus.NO_ERROR, DetectorState.RUNNING, null, JobStatus.ENABLED); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testRunningWithError() throws IOException, InterruptedException { + // testErrorStateTemplate( + // RCFPollingStatus.INIT_DONE, + // ErrorResultStatus.SHINGLE_ERROR, + // DetectorState.RUNNING, + // noFullShingleError, + // JobStatus.ENABLED + // ); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testDisabledForStateError() throws IOException, InterruptedException { + // testErrorStateTemplate( + // RCFPollingStatus.INITTING, + // ErrorResultStatus.STOPPED_ERROR, + // DetectorState.DISABLED, + // stoppedError, + // JobStatus.DISABLED + // ); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testDisabledForStateInit() throws IOException, InterruptedException { + // testErrorStateTemplate( + // RCFPollingStatus.INITTING, + // ErrorResultStatus.STOPPED_ERROR, + // DetectorState.DISABLED, + // stoppedError, + // JobStatus.DISABLED, + // stateInitProgress + // ); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testInitWithError() throws IOException, InterruptedException { + // testErrorStateTemplate( + // RCFPollingStatus.EMPTY, + // ErrorResultStatus.SHINGLE_ERROR, + // DetectorState.INIT, + // noFullShingleError, + // JobStatus.ENABLED + // ); + // } + + // FIXME part of get detector with job scheduler implementation + /*@SuppressWarnings("unchecked") + private void setUpClientExecuteProfileAction() { + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + ActionListener listener = (ActionListener) args[2]; + + node1 = "node1"; + nodeName1 = "nodename1"; + discoveryNode1 = new DiscoveryNode( + nodeName1, + node1, + new TransportAddress(TransportAddress.META_ADDRESS, 9300), + emptyMap(), + emptySet(), + Version.CURRENT ); - } - - public void testDisabledForStateError() throws IOException, InterruptedException { - testErrorStateTemplate( - RCFPollingStatus.INITTING, - ErrorResultStatus.STOPPED_ERROR, - DetectorState.DISABLED, - stoppedError, - JobStatus.DISABLED + + node2 = "node2"; + nodeName2 = "nodename2"; + discoveryNode2 = new DiscoveryNode( + nodeName2, + node2, + new TransportAddress(TransportAddress.META_ADDRESS, 9301), + emptyMap(), + emptySet(), + Version.CURRENT ); - } - - public void testDisabledForStateInit() throws IOException, InterruptedException { - testErrorStateTemplate( - RCFPollingStatus.INITTING, - ErrorResultStatus.STOPPED_ERROR, - DetectorState.DISABLED, - stoppedError, - JobStatus.DISABLED, - stateInitProgress + + modelSize = 4456448L; + model1Id = "Pl536HEBnXkDrah03glg_model_rcf_1"; + model0Id = "Pl536HEBnXkDrah03glg_model_rcf_0"; + + shingleSize = 6; + + String clusterName = "test-cluster-name"; + + Map modelSizeMap1 = new HashMap() { + { + put(model1Id, modelSize); + } + }; + + Map modelSizeMap2 = new HashMap() { + { + put(model0Id, modelSize); + } + }; + + ProfileNodeResponse profileNodeResponse1 = new ProfileNodeResponse( + discoveryNode1, + modelSizeMap1, + shingleSize, + 0L, + 0L, + new ArrayList<>(), + modelSizeMap1.size() ); - } - - public void testInitWithError() throws IOException, InterruptedException { - testErrorStateTemplate( - RCFPollingStatus.EMPTY, - ErrorResultStatus.SHINGLE_ERROR, - DetectorState.INIT, - noFullShingleError, - JobStatus.ENABLED + ProfileNodeResponse profileNodeResponse2 = new ProfileNodeResponse( + discoveryNode2, + modelSizeMap2, + -1, + 0L, + 0L, + new ArrayList<>(), + modelSizeMap2.size() ); - } - - @SuppressWarnings("unchecked") - private void setUpClientExecuteProfileAction() { - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - ActionListener listener = (ActionListener) args[2]; - - node1 = "node1"; - nodeName1 = "nodename1"; - discoveryNode1 = new DiscoveryNode( - nodeName1, - node1, - new TransportAddress(TransportAddress.META_ADDRESS, 9300), - emptyMap(), - emptySet(), - Version.CURRENT - ); - - node2 = "node2"; - nodeName2 = "nodename2"; - discoveryNode2 = new DiscoveryNode( - nodeName2, - node2, - new TransportAddress(TransportAddress.META_ADDRESS, 9301), - emptyMap(), - emptySet(), - Version.CURRENT - ); - - modelSize = 4456448L; - model1Id = "Pl536HEBnXkDrah03glg_model_rcf_1"; - model0Id = "Pl536HEBnXkDrah03glg_model_rcf_0"; - - shingleSize = 6; - - String clusterName = "test-cluster-name"; - - Map modelSizeMap1 = new HashMap() { - { - put(model1Id, modelSize); - } - }; - - Map modelSizeMap2 = new HashMap() { - { - put(model0Id, modelSize); - } - }; - - ProfileNodeResponse profileNodeResponse1 = new ProfileNodeResponse( - discoveryNode1, - modelSizeMap1, - shingleSize, - 0L, - 0L, - new ArrayList<>(), - modelSizeMap1.size() - ); - ProfileNodeResponse profileNodeResponse2 = new ProfileNodeResponse( - discoveryNode2, - modelSizeMap2, - -1, - 0L, - 0L, - new ArrayList<>(), - modelSizeMap2.size() - ); - List profileNodeResponses = Arrays.asList(profileNodeResponse1, profileNodeResponse2); - List failures = Collections.emptyList(); - ProfileResponse profileResponse = new ProfileResponse(new ClusterName(clusterName), profileNodeResponses, failures); - - listener.onResponse(profileResponse); - - return null; - }).when(client).execute(any(ProfileAction.class), any(), any()); - - } - - @SuppressWarnings("unchecked") + List profileNodeResponses = Arrays.asList(profileNodeResponse1, profileNodeResponse2); + List failures = Collections.emptyList(); + ProfileResponse profileResponse = new ProfileResponse(new ClusterName(clusterName), profileNodeResponses, failures); + + listener.onResponse(profileResponse); + + return null; + }).when(client).execute(any(ProfileAction.class), any(), any()); + + }*/ + + // FIXME part of get detector with job scheduler implementation + /*@SuppressWarnings("unchecked") private void setUpClientExecuteRCFPollingAction(RCFPollingStatus inittedEverResultStatus) { doAnswer(invocation -> { Object[] args = invocation.getArguments(); @@ -448,149 +484,153 @@ private void setUpClientExecuteRCFPollingAction(RCFPollingStatus inittedEverResu return null; }).when(client).execute(any(RCFPollingAction.class), any(), any()); - } - - public void testProfileModels() throws InterruptedException, IOException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); - setUpClientExecuteProfileAction(); - - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(profileResponse -> { - assertEquals(node1, profileResponse.getCoordinatingNode()); - assertEquals(shingleSize, profileResponse.getShingleSize()); - assertEquals(modelSize * 2, profileResponse.getTotalSizeInBytes()); - assertEquals(2, profileResponse.getModelProfile().length); - for (ModelProfileOnNode profile : profileResponse.getModelProfile()) { - assertTrue(node1.equals(profile.getNodeId()) || node2.equals(profile.getNodeId())); - assertEquals(modelSize, profile.getModelSize()); - if (node1.equals(profile.getNodeId())) { - assertEquals(model1Id, profile.getModelId()); - } - if (node2.equals(profile.getNodeId())) { - assertEquals(model0Id, profile.getModelId()); - } - } - inProgressLatch.countDown(); - }, exception -> { - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), modelProfile); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitProgress() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.INITTING, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.INIT).build(); - - // 123 / 128 rounded to 96% - InitProgressProfile profile = new InitProgressProfile("96%", neededSamples * detectorIntervalMin, neededSamples); - expectedProfile.setInitProgress(profile); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), stateInitProgress); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitProgressFailImmediately() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.NO_DOC, JobStatus.ENABLED, RCFPollingStatus.INITTING, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.INIT).build(); - - // 123 / 128 rounded to 96% - InitProgressProfile profile = new InitProgressProfile("96%", neededSamples * detectorIntervalMin, neededSamples); - expectedProfile.setInitProgress(profile); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }, exception -> { - assertTrue(exception.getMessage().contains(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG)); - inProgressLatch.countDown(); - }), stateInitProgress); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitNoUpdateNoIndex() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder() - .state(DetectorState.INIT) - .initProgress(new InitProgressProfile("0%", detectorIntervalMin * requiredSamples, requiredSamples)) - .build(); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - LOG.error(exception); - for (StackTraceElement ste : exception.getStackTrace()) { - LOG.info(ste); + }*/ + + // FIXME part of get detector with job scheduler implementation + /*public void testProfileModels() throws InterruptedException, IOException { + setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); + setUpClientExecuteProfileAction(); + + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile(detector.getDetectorId(), ActionListener.wrap(profileResponse -> { + assertEquals(node1, profileResponse.getCoordinatingNode()); + assertEquals(shingleSize, profileResponse.getShingleSize()); + assertEquals(modelSize * 2, profileResponse.getTotalSizeInBytes()); + assertEquals(2, profileResponse.getModelProfile().length); + for (ModelProfileOnNode profile : profileResponse.getModelProfile()) { + assertTrue(node1.equals(profile.getNodeId()) || node2.equals(profile.getNodeId())); + assertEquals(modelSize, profile.getModelSize()); + if (node1.equals(profile.getNodeId())) { + assertEquals(model1Id, profile.getModelId()); } - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), stateInitProgress); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitNoIndex() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.INDEX_NOT_FOUND, ErrorResultStatus.NO_ERROR); - DetectorProfile expectedProfile = new DetectorProfile.Builder() - .state(DetectorState.INIT) - .initProgress(new InitProgressProfile("0%", 0, requiredSamples)) - .build(); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - LOG.error(exception); - for (StackTraceElement ste : exception.getStackTrace()) { - LOG.info(ste); + if (node2.equals(profile.getNodeId())) { + assertEquals(model0Id, profile.getModelId()); } - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }), stateInitProgress); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInvalidRequiredSamples() { - expectThrows( - IllegalArgumentException.class, - () -> new AnomalyDetectorProfileRunner(client, xContentRegistry(), nodeFilter, 0, transportService, adTaskManager) - ); - } - - public void testFailRCFPolling() throws IOException, InterruptedException { - setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EXCEPTION, ErrorResultStatus.NO_ERROR); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { - assertTrue("Should not reach here ", false); - inProgressLatch.countDown(); - }, exception -> { - assertTrue(exception instanceof RuntimeException); - // this means we don't exit with failImmediately. failImmediately can make we return early when there are other concurrent - // requests - assertTrue(exception.getMessage(), exception.getMessage().contains("Exceptions:")); - inProgressLatch.countDown(); - }), stateNError); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitProgressProfile() { - InitProgressProfile progressOne = new InitProgressProfile("0%", 0, requiredSamples); - InitProgressProfile progressTwo = new InitProgressProfile("0%", 0, requiredSamples); - InitProgressProfile progressThree = new InitProgressProfile("96%", 2, requiredSamples); - assertTrue(progressOne.equals(progressTwo)); - assertFalse(progressOne.equals(progressThree)); - } - } - */ + } + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here ", false); + inProgressLatch.countDown(); + }), modelProfile); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + }*/ + + // FIXME part of get detector with job scheduler implementation + // public void testInitProgress() throws IOException, InterruptedException { + // setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.INITTING, ErrorResultStatus.NO_ERROR); + // DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.INIT).build(); + // + // // 123 / 128 rounded to 96% + // InitProgressProfile profile = new InitProgressProfile("96%", neededSamples * detectorIntervalMin, neededSamples); + // expectedProfile.setInitProgress(profile); + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + // assertEquals(expectedProfile, response); + // inProgressLatch.countDown(); + // }, exception -> { + // assertTrue("Should not reach here ", false); + // inProgressLatch.countDown(); + // }), stateInitProgress); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + public void testInitProgressFailImmediately() throws IOException, InterruptedException { + setUpClientGet(DetectorStatus.NO_DOC, JobStatus.ENABLED, RCFPollingStatus.INITTING, ErrorResultStatus.NO_ERROR); + DetectorProfile expectedProfile = new DetectorProfile.Builder().state(DetectorState.INIT).build(); + + // 123 / 128 rounded to 96% + InitProgressProfile profile = new InitProgressProfile("96%", neededSamples * detectorIntervalMin, neededSamples); + expectedProfile.setInitProgress(profile); + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + assertTrue("Should not reach here ", false); + inProgressLatch.countDown(); + }, exception -> { + assertTrue(exception.getMessage().contains(CommonErrorMessages.FAIL_TO_FIND_DETECTOR_MSG)); + inProgressLatch.countDown(); + }), stateInitProgress); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + // FIXME part of get detector with job scheduler implementation + // public void testInitNoUpdateNoIndex() throws IOException, InterruptedException { + // setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EMPTY, ErrorResultStatus.NO_ERROR); + // DetectorProfile expectedProfile = new DetectorProfile.Builder() + // .state(DetectorState.INIT) + // .initProgress(new InitProgressProfile("0%", detectorIntervalMin * requiredSamples, requiredSamples)) + // .build(); + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + // assertEquals(expectedProfile, response); + // inProgressLatch.countDown(); + // }, exception -> { + // LOG.error(exception); + // for (StackTraceElement ste : exception.getStackTrace()) { + // LOG.info(ste); + // } + // assertTrue("Should not reach here ", false); + // inProgressLatch.countDown(); + // }), stateInitProgress); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + // FIXME part of get detector with job scheduler implementation + // public void testInitNoIndex() throws IOException, InterruptedException { + // setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.INDEX_NOT_FOUND, ErrorResultStatus.NO_ERROR); + // DetectorProfile expectedProfile = new DetectorProfile.Builder() + // .state(DetectorState.INIT) + // .initProgress(new InitProgressProfile("0%", 0, requiredSamples)) + // .build(); + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + // assertEquals(expectedProfile, response); + // inProgressLatch.countDown(); + // }, exception -> { + // LOG.error(exception); + // for (StackTraceElement ste : exception.getStackTrace()) { + // LOG.info(ste); + // } + // assertTrue("Should not reach here ", false); + // inProgressLatch.countDown(); + // }), stateInitProgress); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + public void testInvalidRequiredSamples() { + expectThrows( + IllegalArgumentException.class, + () -> new AnomalyDetectorProfileRunner(client, xContentRegistry(), nodeFilter, 0, transportService, adTaskManager) + ); + } + + // FIXME part of get detector with job scheduler implementation + // public void testFailRCFPolling() throws IOException, InterruptedException { + // setUpClientGet(DetectorStatus.EXIST, JobStatus.ENABLED, RCFPollingStatus.EXCEPTION, ErrorResultStatus.NO_ERROR); + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detector.getDetectorId(), ActionListener.wrap(response -> { + // assertTrue("Should not reach here ", false); + // inProgressLatch.countDown(); + // }, exception -> { + // assertTrue(exception instanceof RuntimeException); + // // this means we don't exit with failImmediately. failImmediately can make we return early when there are other concurrent + // // requests + // assertTrue(exception.getMessage(), exception.getMessage().contains("Exceptions:")); + // inProgressLatch.countDown(); + // }), stateNError); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + public void testInitProgressProfile() { + InitProgressProfile progressOne = new InitProgressProfile("0%", 0, requiredSamples); + InitProgressProfile progressTwo = new InitProgressProfile("0%", 0, requiredSamples); + InitProgressProfile progressThree = new InitProgressProfile("96%", 2, requiredSamples); + assertTrue(progressOne.equals(progressTwo)); + assertFalse(progressOne.equals(progressThree)); + } +} diff --git a/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java b/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java index 81f52a5b2..f0a99010a 100644 --- a/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java @@ -10,14 +10,49 @@ * GitHub history for details. */ -/* package org.opensearch.ad; +import static java.util.Collections.emptyMap; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.opensearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; +import static org.opensearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.lucene.search.TotalHits; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchResponseSections; +import org.opensearch.action.search.ShardSearchFailure; +import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.AnomalyDetectorJob; +import org.opensearch.ad.model.Entity; +import org.opensearch.ad.model.EntityProfileName; +import org.opensearch.ad.model.EntityState; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.search.DocValueFormat; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.search.aggregations.InternalAggregations; +import org.opensearch.search.aggregations.metrics.InternalMax; +import org.opensearch.search.internal.InternalSearchResponse; public class EntityProfileRunnerTests extends AbstractADTest { private AnomalyDetector detector; private int detectorIntervalMin; - private Client client; + private SDKRestClient client; private EntityProfileRunner runner; private Set state; private Set initNInfo; @@ -63,7 +98,7 @@ public void setUp() throws Exception { entityValue = "app-0"; requiredSamples = 128; - client = mock(Client.class); + client = mock(SDKRestClient.class); runner = new EntityProfileRunner(client, xContentRegistry(), requiredSamples); @@ -78,13 +113,9 @@ public void setUp() throws Exception { String indexName = request.index(); if (indexName.equals(ANOMALY_DETECTORS_INDEX)) { - listener - .onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), AnomalyDetector.ANOMALY_DETECTORS_INDEX)); + listener.onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), ANOMALY_DETECTORS_INDEX)); } else if (indexName.equals(ANOMALY_DETECTOR_JOB_INDEX)) { - listener - .onResponse( - TestHelpers.createGetResponse(job, detector.getDetectorId(), AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX) - ); + listener.onResponse(TestHelpers.createGetResponse(job, detector.getDetectorId(), ANOMALY_DETECTOR_JOB_INDEX)); } return null; @@ -133,27 +164,27 @@ private void setUpExecuteEntityProfileAction(InittedEverResultStatus initted) { modelId = "T4c3dXUBj-2IZN7itix__entity_" + entityValue; modelSize = 712480L; nodeId = "g6pmr547QR-CfpEvO67M4g"; - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - ActionListener listener = (ActionListener) args[2]; - - EntityProfileResponse.Builder profileResponseBuilder = new EntityProfileResponse.Builder(); - if (InittedEverResultStatus.UNKNOWN == initted) { - profileResponseBuilder.setTotalUpdates(0L); - } else if (InittedEverResultStatus.NOT_INITTED == initted) { - profileResponseBuilder.setTotalUpdates(smallUpdates); - profileResponseBuilder.setLastActiveMs(latestActiveTimestamp); - profileResponseBuilder.setActive(isActive); - } else { - profileResponseBuilder.setTotalUpdates(requiredSamples + 1); - ModelProfileOnNode model = new ModelProfileOnNode(nodeId, new ModelProfile(modelId, entity, modelSize)); - profileResponseBuilder.setModelProfile(model); - } - - listener.onResponse(profileResponseBuilder.build()); - - return null; - }).when(client).execute(any(EntityProfileAction.class), any(), any()); + // doAnswer(invocation -> { + // Object[] args = invocation.getArguments(); + // ActionListener listener = (ActionListener) args[2]; + // + // EntityProfileResponse.Builder profileResponseBuilder = new EntityProfileResponse.Builder(); + // if (InittedEverResultStatus.UNKNOWN == initted) { + // profileResponseBuilder.setTotalUpdates(0L); + // } else if (InittedEverResultStatus.NOT_INITTED == initted) { + // profileResponseBuilder.setTotalUpdates(smallUpdates); + // profileResponseBuilder.setLastActiveMs(latestActiveTimestamp); + // profileResponseBuilder.setActive(isActive); + // } else { + // profileResponseBuilder.setTotalUpdates(requiredSamples + 1); + // ModelProfileOnNode model = new ModelProfileOnNode(nodeId, new ModelProfile(modelId, entity, modelSize)); + // profileResponseBuilder.setModelProfile(model); + // } + // + // listener.onResponse(profileResponseBuilder.build()); + // + // return null; + // }).when(client).execute(any(EntityProfileAction.class), any(), any()); doAnswer(invocation -> { Object[] args = invocation.getArguments(); @@ -180,9 +211,7 @@ private void setUpExecuteEntityProfileAction(InittedEverResultStatus initted) { ); } else { SearchHits collapsedHits = new SearchHits( - new SearchHit[] { - new SearchHit(2, "ID", Collections.emptyMap(), Collections.emptyMap()), - new SearchHit(3, "ID", Collections.emptyMap(), Collections.emptyMap()) }, + new SearchHit[] { new SearchHit(2, "ID", emptyMap(), emptyMap()), new SearchHit(3, "ID", emptyMap(), emptyMap()) }, new TotalHits(1, TotalHits.Relation.EQUAL_TO), 1.0F ); @@ -222,17 +251,17 @@ public void stateTestTemplate(InittedEverResultStatus returnedState, EntityState assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); } - public void testRunningState() throws InterruptedException { - stateTestTemplate(InittedEverResultStatus.INITTED, EntityState.RUNNING); - } + // public void testRunningState() throws InterruptedException { + // stateTestTemplate(InittedEverResultStatus.INITTED, EntityState.RUNNING); + // } - public void testUnknownState() throws InterruptedException { - stateTestTemplate(InittedEverResultStatus.UNKNOWN, EntityState.UNKNOWN); - } + // public void testUnknownState() throws InterruptedException { + // stateTestTemplate(InittedEverResultStatus.UNKNOWN, EntityState.UNKNOWN); + // } - public void testInitState() throws InterruptedException { - stateTestTemplate(InittedEverResultStatus.NOT_INITTED, EntityState.INIT); - } + // public void testInitState() throws InterruptedException { + // stateTestTemplate(InittedEverResultStatus.NOT_INITTED, EntityState.INIT); + // } public void testEmptyProfile() throws InterruptedException { final CountDownLatch inProgressLatch = new CountDownLatch(1); @@ -247,128 +276,127 @@ public void testEmptyProfile() throws InterruptedException { assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); } - public void testModel() throws InterruptedException { - setUpExecuteEntityProfileAction(InittedEverResultStatus.INITTED); - EntityProfile.Builder expectedProfile = new EntityProfile.Builder(); - ModelProfileOnNode modelProfile = new ModelProfileOnNode(nodeId, new ModelProfile(modelId, entity, modelSize)); - expectedProfile.modelProfile(modelProfile); - final CountDownLatch inProgressLatch = new CountDownLatch(1); - runner.profile(detectorId, entity, model, ActionListener.wrap(response -> { - assertEquals(expectedProfile.build(), response); - inProgressLatch.countDown(); - }, exception -> { - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - })); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testEmptyModelProfile() throws IOException { - ModelProfile modelProfile = new ModelProfile(modelId, null, modelSize); - BytesStreamOutput output = new BytesStreamOutput(); - modelProfile.writeTo(output); - StreamInput streamInput = output.bytes().streamInput(); - ModelProfile readResponse = new ModelProfile(streamInput); - assertEquals("serialization has the wrong model id", modelId, readResponse.getModelId()); - assertTrue("serialization has null entity", null == readResponse.getEntity()); - assertEquals("serialization has the wrong model size", modelSize, readResponse.getModelSizeInBytes()); - - } - - @SuppressWarnings("unchecked") - public void testJobIndexNotFound() throws InterruptedException { - setUpExecuteEntityProfileAction(InittedEverResultStatus.INITTED); - - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - GetRequest request = (GetRequest) args[0]; - ActionListener listener = (ActionListener) args[1]; - - String indexName = request.index(); - if (indexName.equals(ANOMALY_DETECTORS_INDEX)) { - listener - .onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), AnomalyDetector.ANOMALY_DETECTORS_INDEX)); - } else if (indexName.equals(ANOMALY_DETECTOR_JOB_INDEX)) { - listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTOR_JOB_INDEX)); - } - - return null; - }).when(client).get(any(), any()); - - EntityProfile expectedProfile = new EntityProfile.Builder().build(); - - runner.profile(detectorId, entity, initNInfo, ActionListener.wrap(response -> { - assertEquals(expectedProfile, response); - inProgressLatch.countDown(); - }, exception -> { - LOG.error("Unexpected error", exception); - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - })); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - @SuppressWarnings("unchecked") - public void testNotMultiEntityDetector() throws IOException, InterruptedException { - detector = TestHelpers.randomAnomalyDetectorWithInterval(new IntervalTimeConfiguration(detectorIntervalMin, ChronoUnit.MINUTES)); - - doAnswer(invocation -> { - Object[] args = invocation.getArguments(); - GetRequest request = (GetRequest) args[0]; - ActionListener listener = (ActionListener) args[1]; - - String indexName = request.index(); - if (indexName.equals(ANOMALY_DETECTORS_INDEX)) { - listener - .onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), AnomalyDetector.ANOMALY_DETECTORS_INDEX)); - } - - return null; - }).when(client).get(any(), any()); - - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detectorId, entity, state, ActionListener.wrap(response -> { - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - }, exception -> { - assertTrue(exception.getMessage().contains(EntityProfileRunner.NOT_HC_DETECTOR_ERR_MSG)); - inProgressLatch.countDown(); - })); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } - - public void testInitNInfo() throws InterruptedException { - setUpExecuteEntityProfileAction(InittedEverResultStatus.NOT_INITTED); - latestSampleTimestamp = 1_603_989_830_158L; - - EntityProfile.Builder expectedProfile = new EntityProfile.Builder(); - - // 1 / 128 rounded to 1% - int neededSamples = requiredSamples - smallUpdates; - InitProgressProfile profile = new InitProgressProfile( - "1%", - neededSamples * detector.getDetectorIntervalInSeconds() / 60, - neededSamples - ); - expectedProfile.initProgress(profile); - expectedProfile.isActive(isActive); - expectedProfile.lastActiveTimestampMs(latestActiveTimestamp); - expectedProfile.lastSampleTimestampMs(latestSampleTimestamp); - - final CountDownLatch inProgressLatch = new CountDownLatch(1); - - runner.profile(detectorId, entity, initNInfo, ActionListener.wrap(response -> { - assertEquals(expectedProfile.build(), response); - inProgressLatch.countDown(); - }, exception -> { - LOG.error("Unexpected error", exception); - assertTrue("Should not reach here", false); - inProgressLatch.countDown(); - })); - assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); - } + // public void testModel() throws InterruptedException { + // setUpExecuteEntityProfileAction(InittedEverResultStatus.INITTED); + // EntityProfile.Builder expectedProfile = new EntityProfile.Builder(); + // ModelProfileOnNode modelProfile = new ModelProfileOnNode(nodeId, new ModelProfile(modelId, entity, modelSize)); + // expectedProfile.modelProfile(modelProfile); + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // runner.profile(detectorId, entity, model, ActionListener.wrap(response -> { + // assertEquals(expectedProfile.build(), response); + // inProgressLatch.countDown(); + // }, exception -> { + // assertTrue("Should not reach here", false); + // inProgressLatch.countDown(); + // })); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + // public void testEmptyModelProfile() throws IOException { + // ModelProfile modelProfile = new ModelProfile(modelId, null, modelSize); + // BytesStreamOutput output = new BytesStreamOutput(); + // modelProfile.writeTo(output); + // StreamInput streamInput = output.bytes().streamInput(); + // ModelProfile readResponse = new ModelProfile(streamInput); + // assertEquals("serialization has the wrong model id", modelId, readResponse.getModelId()); + // assertTrue("serialization has null entity", null == readResponse.getEntity()); + // assertEquals("serialization has the wrong model size", modelSize, readResponse.getModelSizeInBytes()); + // + // } + + // @SuppressWarnings("unchecked") + // public void testJobIndexNotFound() throws InterruptedException { + // setUpExecuteEntityProfileAction(InittedEverResultStatus.INITTED); + // + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // doAnswer(invocation -> { + // Object[] args = invocation.getArguments(); + // GetRequest request = (GetRequest) args[0]; + // ActionListener listener = (ActionListener) args[1]; + // + // String indexName = request.index(); + // if (indexName.equals(ANOMALY_DETECTORS_INDEX)) { + // listener + // .onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), ANOMALY_DETECTORS_INDEX)); + // } else if (indexName.equals(ANOMALY_DETECTOR_JOB_INDEX)) { + // listener.onFailure(new IndexNotFoundException(ANOMALY_DETECTOR_JOB_INDEX)); + // } + // + // return null; + // }).when(client).get(any(), any()); + // + // EntityProfile expectedProfile = new EntityProfile.Builder().build(); + // + // runner.profile(detectorId, entity, initNInfo, ActionListener.wrap(response -> { + // assertEquals(expectedProfile, response); + // inProgressLatch.countDown(); + // }, exception -> { + // LOG.error("Unexpected error", exception); + // assertTrue("Should not reach here", false); + // inProgressLatch.countDown(); + // })); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + // @SuppressWarnings("unchecked") + // public void testNotMultiEntityDetector() throws IOException, InterruptedException { + // detector = TestHelpers.randomAnomalyDetectorWithInterval(new IntervalTimeConfiguration(detectorIntervalMin, ChronoUnit.MINUTES)); + // + // doAnswer(invocation -> { + // Object[] args = invocation.getArguments(); + // GetRequest request = (GetRequest) args[0]; + // ActionListener listener = (ActionListener) args[1]; + // + // String indexName = request.index(); + // if (indexName.equals(ANOMALY_DETECTORS_INDEX)) { + // listener + // .onResponse(TestHelpers.createGetResponse(detector, detector.getDetectorId(), ANOMALY_DETECTORS_INDEX)); + // } + // + // return null; + // }).when(client).get(any(), any()); + // + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detectorId, entity, state, ActionListener.wrap(response -> { + // assertTrue("Should not reach here", false); + // inProgressLatch.countDown(); + // }, exception -> { + // assertTrue(exception.getMessage().contains(EntityProfileRunner.NOT_HC_DETECTOR_ERR_MSG)); + // inProgressLatch.countDown(); + // })); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } + + // public void testInitNInfo() throws InterruptedException { + // setUpExecuteEntityProfileAction(InittedEverResultStatus.NOT_INITTED); + // latestSampleTimestamp = 1_603_989_830_158L; + // + // EntityProfile.Builder expectedProfile = new EntityProfile.Builder(); + // + // // 1 / 128 rounded to 1% + // int neededSamples = requiredSamples - smallUpdates; + // InitProgressProfile profile = new InitProgressProfile( + // "1%", + // neededSamples * detector.getDetectorIntervalInSeconds() / 60, + // neededSamples + // ); + // expectedProfile.initProgress(profile); + // expectedProfile.isActive(isActive); + // expectedProfile.lastActiveTimestampMs(latestActiveTimestamp); + // expectedProfile.lastSampleTimestampMs(latestSampleTimestamp); + // + // final CountDownLatch inProgressLatch = new CountDownLatch(1); + // + // runner.profile(detectorId, entity, initNInfo, ActionListener.wrap(response -> { + // assertEquals(expectedProfile.build(), response); + // inProgressLatch.countDown(); + // }, exception -> { + // LOG.error("Unexpected error", exception); + // assertTrue("Should not reach here", false); + // inProgressLatch.countDown(); + // })); + // assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + // } } -*/