1212import org .elasticsearch .action .support .master .AcknowledgedResponse ;
1313import org .elasticsearch .client .internal .Client ;
1414import org .elasticsearch .client .internal .OriginSettingClient ;
15- import org .elasticsearch .cluster .metadata .IndexNameExpressionResolver ;
16- import org .elasticsearch .cluster .service .ClusterService ;
1715import org .elasticsearch .core .TimeValue ;
1816import org .elasticsearch .index .query .IdsQueryBuilder ;
1917import org .elasticsearch .index .reindex .DeleteByQueryAction ;
2018import org .elasticsearch .index .reindex .DeleteByQueryRequest ;
21- import org .elasticsearch .indices .TestIndexNameExpressionResolver ;
2219import org .elasticsearch .search .SearchHit ;
2320import org .elasticsearch .tasks .TaskId ;
2421import org .elasticsearch .test .ESTestCase ;
3835import org .elasticsearch .xpack .ml .test .MockOriginSettingClient ;
3936import org .elasticsearch .xpack .ml .test .SearchHitBuilder ;
4037import org .junit .Before ;
38+ import org .mockito .ArgumentMatchers ;
4139import org .mockito .invocation .InvocationOnMock ;
4240import org .mockito .stubbing .Answer ;
4341
4442import java .io .IOException ;
4543import java .util .ArrayList ;
4644import java .util .Arrays ;
45+ import java .util .Collection ;
4746import java .util .Collections ;
4847import java .util .Date ;
4948import java .util .HashMap ;
6160import static org .mockito .ArgumentMatchers .any ;
6261import static org .mockito .ArgumentMatchers .anyBoolean ;
6362import static org .mockito .ArgumentMatchers .anyInt ;
63+ import static org .mockito .ArgumentMatchers .anyString ;
6464import static org .mockito .ArgumentMatchers .eq ;
6565import static org .mockito .ArgumentMatchers .same ;
6666import static org .mockito .Mockito .doAnswer ;
@@ -77,8 +77,6 @@ public class ExpiredModelSnapshotsRemoverTests extends ESTestCase {
7777 private List <String > capturedJobIds ;
7878 private List <DeleteByQueryRequest > capturedDeleteModelSnapshotRequests ;
7979 private TestListener listener ;
80- private ClusterService clusterService ;
81- private IndexNameExpressionResolver indexNameExpressionResolver = TestIndexNameExpressionResolver .newInstance ();
8280
8381 @ Before
8482 public void setUpTests () {
@@ -88,8 +86,6 @@ public void setUpTests() {
8886 client = mock (Client .class );
8987 originSettingClient = MockOriginSettingClient .mockOriginSettingClient (client , ClientHelper .ML_ORIGIN );
9088 resultsProvider = mock (JobResultsProvider .class );
91- clusterService = mock (ClusterService .class );
92-
9389 listener = new TestListener ();
9490 }
9591
@@ -285,7 +281,44 @@ public void testCalcCutoffEpochMs() {
285281 verify (cutoffListener ).onResponse (eq (new AbstractExpiredJobDataRemover .CutoffDetails (oneDayAgo .getTime (), expectedCutoffTime )));
286282 }
287283
284+ public void testRemove_GivenIndexNotWritable_ShouldHandleGracefully () {
285+ List <SearchResponse > searchResponses = new ArrayList <>();
286+ List <Job > jobs = Arrays .asList (
287+ JobTests .buildJobBuilder ("job-1" ).setModelSnapshotRetentionDays (7L ).setModelSnapshotId ("active" ).build ()
288+ );
289+
290+ Date now = new Date ();
291+ Date oneDayAgo = new Date (now .getTime () - TimeValue .timeValueDays (1 ).getMillis ());
292+ SearchHit snapshot1 = createModelSnapshotQueryHit ("job-1" , "fresh-snapshot" , oneDayAgo );
293+ searchResponses .add (AbstractExpiredJobDataRemoverTests .createSearchResponseFromHits (Collections .singletonList (snapshot1 )));
294+
295+ Date eightDaysAndOneMsAgo = new Date (now .getTime () - TimeValue .timeValueDays (8 ).getMillis () - 1 );
296+ Map <String , List <ModelSnapshot >> snapshotResponses = new HashMap <>();
297+ snapshotResponses .put (
298+ "job-1" ,
299+ Arrays .asList (
300+ // Keeping active as its expiration is not known. We can assume "worst case" and verify it is not removed
301+ createModelSnapshot ("job-1" , "active" , eightDaysAndOneMsAgo ),
302+ createModelSnapshot ("job-1" , "old-snapshot" , eightDaysAndOneMsAgo )
303+ )
304+ );
305+
306+ givenClientRequestsSucceed (searchResponses , snapshotResponses );
307+
308+ // Create remover with state index not writable
309+ createExpiredModelSnapshotsRemover (jobs .iterator (), false ).remove (1.0f , listener , () -> false );
310+
311+ listener .waitToCompletion ();
312+ // Should succeed, but not attempt to delete anything
313+ assertThat (listener .success , is (true ));
314+ assertThat (capturedDeleteModelSnapshotRequests .size (), equalTo (0 ));
315+ }
316+
288317 private ExpiredModelSnapshotsRemover createExpiredModelSnapshotsRemover (Iterator <Job > jobIterator ) {
318+ return createExpiredModelSnapshotsRemover (jobIterator , true );
319+ }
320+
321+ private ExpiredModelSnapshotsRemover createExpiredModelSnapshotsRemover (Iterator <Job > jobIterator , boolean isStateIndexWritable ) {
289322 ThreadPool threadPool = mock (ThreadPool .class );
290323 ExecutorService executor = mock (ExecutorService .class );
291324
@@ -296,17 +329,44 @@ private ExpiredModelSnapshotsRemover createExpiredModelSnapshotsRemover(Iterator
296329 run .run ();
297330 return null ;
298331 }).when (executor ).execute (any ());
332+
333+ WritableIndexExpander writableIndexExpander = mockWritableIndexExpander (isStateIndexWritable );
334+
299335 return new ExpiredModelSnapshotsRemover (
300336 originSettingClient ,
301337 jobIterator ,
302338 new TaskId ("test" , 0L ),
303- new WritableIndexExpander ( clusterService , indexNameExpressionResolver ) ,
339+ writableIndexExpander ,
304340 threadPool ,
305341 resultsProvider ,
306342 mock (AnomalyDetectionAuditor .class )
307343 );
308344 }
309345
346+ private static WritableIndexExpander mockWritableIndexExpander (boolean stateIndexWritable ) {
347+ WritableIndexExpander writableIndexExpander = mock (WritableIndexExpander .class );
348+ if (stateIndexWritable ) {
349+ mockWhenIndicesAreWritable (writableIndexExpander );
350+ } else {
351+ mockWhenIndicesAreNotWritable (writableIndexExpander );
352+ }
353+ return writableIndexExpander ;
354+ }
355+
356+ private static void mockWhenIndicesAreNotWritable (WritableIndexExpander writableIndexExpander ) {
357+ when (writableIndexExpander .getWritableIndices (anyString ()))
358+ .thenReturn (new ArrayList <>());
359+ when (writableIndexExpander .getWritableIndices (ArgumentMatchers .<Collection <String >>any ()))
360+ .thenReturn (new ArrayList <>());
361+ }
362+
363+ private static void mockWhenIndicesAreWritable (WritableIndexExpander writableIndexExpander ) {
364+ when (writableIndexExpander .getWritableIndices (anyString ()))
365+ .thenAnswer (invocation -> List .of (invocation .getArgument (0 )));
366+ when (writableIndexExpander .getWritableIndices (ArgumentMatchers .<Collection <String >>any ()))
367+ .thenAnswer (invocation -> new ArrayList <>(invocation .getArgument (0 )));
368+ }
369+
310370 private static ModelSnapshot createModelSnapshot (String jobId , String snapshotId , Date date ) {
311371 return new ModelSnapshot .Builder (jobId ).setSnapshotId (snapshotId ).setTimestamp (date ).build ();
312372 }
0 commit comments