2323import org .elasticsearch .cluster .ClusterState ;
2424import org .elasticsearch .cluster .service .ClusterService ;
2525import org .elasticsearch .common .Strings ;
26- import org .elasticsearch .core .Tuple ;
2726import org .elasticsearch .common .inject .Inject ;
28- import org .elasticsearch .common .xcontent .DeprecationHandler ;
29- import org .elasticsearch .common .xcontent .NamedXContentRegistry ;
30- import org .elasticsearch .common .xcontent .XContentFactory ;
31- import org .elasticsearch .common .xcontent .XContentParser ;
32- import org .elasticsearch .common .xcontent .XContentType ;
27+ import org .elasticsearch .core .Tuple ;
3328import org .elasticsearch .index .query .BoolQueryBuilder ;
3429import org .elasticsearch .index .query .QueryBuilder ;
3530import org .elasticsearch .index .query .QueryBuilders ;
5752import org .elasticsearch .xpack .core .ml .utils .ExceptionsHelper ;
5853import org .elasticsearch .xpack .ml .utils .QueryBuilderHelper ;
5954
60- import java .io .IOException ;
61- import java .io .InputStream ;
6255import java .util .ArrayList ;
63- import java .util .Collection ;
64- import java .util .EnumSet ;
65- import java .util .HashSet ;
6656import java .util .List ;
6757import java .util .Set ;
6858import java .util .concurrent .TimeoutException ;
69- import java .util .stream .Collectors ;
59+ import java .util .stream .Stream ;
7060
61+ import static java .util .stream .Collectors .toSet ;
7162import static org .elasticsearch .xpack .core .ClientHelper .ML_ORIGIN ;
7263import static org .elasticsearch .xpack .core .ClientHelper .executeAsyncWithOrigin ;
7364
@@ -80,8 +71,10 @@ public class TransportDeleteForecastAction extends HandledTransportAction<Delete
8071 private final ClusterService clusterService ;
8172 private static final int MAX_FORECAST_TO_SEARCH = 10_000 ;
8273
83- private static final Set <ForecastRequestStatus > DELETABLE_STATUSES =
84- EnumSet .of (ForecastRequestStatus .FINISHED , ForecastRequestStatus .FAILED );
74+ private static final Set <String > DELETABLE_STATUSES =
75+ Stream .of (ForecastRequestStatus .FINISHED , ForecastRequestStatus .FAILED )
76+ .map (ForecastRequestStatus ::toString )
77+ .collect (toSet ());
8578
8679 @ Inject
8780 public TransportDeleteForecastAction (TransportService transportService ,
@@ -105,47 +98,54 @@ protected void doExecute(Task task, DeleteForecastAction.Request request, Action
10598 e -> handleFailure (e , request , listener )
10699 );
107100
108- SearchSourceBuilder source = new SearchSourceBuilder ();
109-
110- BoolQueryBuilder builder = QueryBuilders .boolQuery ()
111- .filter (QueryBuilders .termQuery (Result .RESULT_TYPE .getPreferredName (), ForecastRequestStats .RESULT_TYPE_VALUE ));
101+ BoolQueryBuilder query =
102+ QueryBuilders .boolQuery ()
103+ .filter (QueryBuilders .termQuery (Result .RESULT_TYPE .getPreferredName (), ForecastRequestStats .RESULT_TYPE_VALUE ));
112104 QueryBuilderHelper
113105 .buildTokenFilterQuery (Forecast .FORECAST_ID .getPreferredName (), forecastIds )
114- .ifPresent (builder ::filter );
115- source .query (builder );
116-
117- SearchRequest searchRequest = new SearchRequest (AnomalyDetectorsIndex .jobResultsAliasedName (jobId ));
118- searchRequest .source (source );
106+ .ifPresent (query ::filter );
107+ SearchSourceBuilder source =
108+ new SearchSourceBuilder ()
109+ .size (MAX_FORECAST_TO_SEARCH )
110+ // We only need forecast id and status, there is no need fetching the whole source
111+ .fetchSource (false )
112+ .docValueField (ForecastRequestStats .FORECAST_ID .getPreferredName ())
113+ .docValueField (ForecastRequestStats .STATUS .getPreferredName ())
114+ .query (query );
115+ SearchRequest searchRequest =
116+ new SearchRequest (AnomalyDetectorsIndex .jobResultsAliasedName (jobId ))
117+ .source (source );
119118
120119 executeAsyncWithOrigin (client , ML_ORIGIN , SearchAction .INSTANCE , searchRequest , forecastStatsHandler );
121120 }
122121
123- static void validateForecastState (Collection <ForecastRequestStats > forecastsToDelete , JobState jobState , String jobId ) {
124- List <String > badStatusForecasts = forecastsToDelete .stream ()
125- .filter ((f ) -> DELETABLE_STATUSES .contains (f .getStatus ()) == false )
126- .map (ForecastRequestStats ::getForecastId )
127- .collect (Collectors .toList ());
128- if (badStatusForecasts .size () > 0 && JobState .OPENED .equals (jobState )) {
122+ static List <String > extractForecastIds (SearchHit [] forecastsToDelete , JobState jobState , String jobId ) {
123+ List <String > forecastIds = new ArrayList <>(forecastsToDelete .length );
124+ List <String > badStatusForecastIds = new ArrayList <>();
125+ for (SearchHit hit : forecastsToDelete ) {
126+ String forecastId = hit .field (ForecastRequestStats .FORECAST_ID .getPreferredName ()).getValue ();
127+ String forecastStatus = hit .field (ForecastRequestStats .STATUS .getPreferredName ()).getValue ();
128+ if (DELETABLE_STATUSES .contains (forecastStatus )) {
129+ forecastIds .add (forecastId );
130+ } else {
131+ badStatusForecastIds .add (forecastId );
132+ }
133+ }
134+ if (badStatusForecastIds .size () > 0 && JobState .OPENED .equals (jobState )) {
129135 throw ExceptionsHelper .conflictStatusException (
130- Messages .getMessage (Messages .REST_CANNOT_DELETE_FORECAST_IN_CURRENT_STATE , badStatusForecasts , jobId ));
136+ Messages .getMessage (Messages .REST_CANNOT_DELETE_FORECAST_IN_CURRENT_STATE , badStatusForecastIds , jobId ));
131137 }
138+ return forecastIds ;
132139 }
133140
134141 private void deleteForecasts (SearchResponse searchResponse ,
135142 DeleteForecastAction .Request request ,
136143 ActionListener <AcknowledgedResponse > listener ) {
137144 final String jobId = request .getJobId ();
138- Set <ForecastRequestStats > forecastsToDelete ;
139- try {
140- forecastsToDelete = parseForecastsFromSearch (searchResponse );
141- } catch (IOException e ) {
142- listener .onFailure (e );
143- return ;
144- }
145+ SearchHits forecastsToDelete = searchResponse .getHits ();
145146
146- if (forecastsToDelete .isEmpty ()) {
147- if (Strings .isAllOrWildcard (request .getForecastId ()) &&
148- request .isAllowNoForecasts ()) {
147+ if (forecastsToDelete .getHits ().length == 0 ) {
148+ if (Strings .isAllOrWildcard (request .getForecastId ()) && request .isAllowNoForecasts ()) {
149149 listener .onResponse (AcknowledgedResponse .TRUE );
150150 } else {
151151 listener .onFailure (
@@ -156,16 +156,15 @@ private void deleteForecasts(SearchResponse searchResponse,
156156 final ClusterState state = clusterService .state ();
157157 PersistentTasksCustomMetadata persistentTasks = state .metadata ().custom (PersistentTasksCustomMetadata .TYPE );
158158 JobState jobState = MlTasks .getJobState (jobId , persistentTasks );
159+ final List <String > forecastIds ;
159160 try {
160- validateForecastState (forecastsToDelete , jobState , jobId );
161+ forecastIds = extractForecastIds (forecastsToDelete . getHits () , jobState , jobId );
161162 } catch (ElasticsearchException ex ) {
162163 listener .onFailure (ex );
163164 return ;
164165 }
165166
166- final List <String > forecastIds = forecastsToDelete .stream ().map (ForecastRequestStats ::getForecastId ).collect (Collectors .toList ());
167167 DeleteByQueryRequest deleteByQueryRequest = buildDeleteByQuery (jobId , forecastIds );
168-
169168 executeAsyncWithOrigin (client , ML_ORIGIN , DeleteByQueryAction .INSTANCE , deleteByQueryRequest , ActionListener .wrap (
170169 response -> {
171170 if (response .isTimedOut ()) {
@@ -208,45 +207,29 @@ private static Tuple<RestStatus, Throwable> getStatusAndReason(final BulkByScrol
208207 return new Tuple <>(status , reason );
209208 }
210209
211- private static Set <ForecastRequestStats > parseForecastsFromSearch (SearchResponse searchResponse ) throws IOException {
212- SearchHits hits = searchResponse .getHits ();
213- List <ForecastRequestStats > allStats = new ArrayList <>(hits .getHits ().length );
214- for (SearchHit hit : hits ) {
215- try (InputStream stream = hit .getSourceRef ().streamInput ();
216- XContentParser parser = XContentFactory .xContent (XContentType .JSON ).createParser (
217- NamedXContentRegistry .EMPTY , DeprecationHandler .THROW_UNSUPPORTED_OPERATION , stream )) {
218- allStats .add (ForecastRequestStats .STRICT_PARSER .apply (parser , null ));
219- }
220- }
221- return new HashSet <>(allStats );
222- }
223-
224210 private DeleteByQueryRequest buildDeleteByQuery (String jobId , List <String > forecastsToDelete ) {
225- DeleteByQueryRequest request = new DeleteByQueryRequest ()
226- .setAbortOnVersionConflict (false ) //since these documents are not updated, a conflict just means it was deleted previously
227- .setMaxDocs (MAX_FORECAST_TO_SEARCH )
228- .setSlices (AbstractBulkByScrollRequest .AUTO_SLICES );
229-
230- request .indices (AnomalyDetectorsIndex .jobResultsAliasedName (jobId ));
231- BoolQueryBuilder innerBoolQuery = QueryBuilders .boolQuery ();
232- innerBoolQuery
211+ BoolQueryBuilder innerBoolQuery = QueryBuilders .boolQuery ()
233212 .must (QueryBuilders .termsQuery (Result .RESULT_TYPE .getPreferredName (),
234213 ForecastRequestStats .RESULT_TYPE_VALUE , Forecast .RESULT_TYPE_VALUE ))
235214 .must (QueryBuilders .termsQuery (Forecast .FORECAST_ID .getPreferredName (),
236215 forecastsToDelete ));
237-
238216 QueryBuilder query = QueryBuilders .boolQuery ().filter (innerBoolQuery );
239- request .setQuery (query );
240- request .setRefresh (true );
241- return request ;
217+
218+ // We want *all* of the docs to be deleted. Hence, we rely on the default value of max_docs.
219+ return new DeleteByQueryRequest ()
220+ .setAbortOnVersionConflict (false ) // since these documents are not updated, a conflict just means it was deleted previously
221+ .setSlices (AbstractBulkByScrollRequest .AUTO_SLICES )
222+ .indices (AnomalyDetectorsIndex .jobResultsAliasedName (jobId ))
223+ .setQuery (query )
224+ .setRefresh (true );
242225 }
243226
244227 private static void handleFailure (Exception e ,
245228 DeleteForecastAction .Request request ,
246229 ActionListener <AcknowledgedResponse > listener ) {
247230 if (ExceptionsHelper .unwrapCause (e ) instanceof ResourceNotFoundException ) {
248231 if (request .isAllowNoForecasts () && Strings .isAllOrWildcard (request .getForecastId ())) {
249- listener .onResponse (AcknowledgedResponse .of ( true ) );
232+ listener .onResponse (AcknowledgedResponse .TRUE );
250233 } else {
251234 listener .onFailure (new ResourceNotFoundException (
252235 Messages .getMessage (Messages .REST_NO_SUCH_FORECAST , request .getForecastId (), request .getJobId ())
0 commit comments