Skip to content

Commit 8a28cb0

Browse files
committed
add unit test for ExpiredModelSnapshotsRemover
1 parent 64ed211 commit 8a28cb0

File tree

2 files changed

+73
-8
lines changed

2 files changed

+73
-8
lines changed

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemover.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ private void deleteModelSnapshots(List<ModelSnapshot> modelSnapshots, String job
272272

273273
// Remove read-only indices
274274
var indicesToQuery = writableIndexExpander.getWritableIndices(indices);
275+
if (indicesToQuery.isEmpty()) {
276+
LOGGER.info("No writable model snapshot indices found for [{}] job. No expired model snapshots to remove.", jobId);
277+
listener.onResponse(true);
278+
return;
279+
}
275280

276281
DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(indicesToQuery.toArray(new String[0])).setRefresh(true)
277282
.setIndicesOptions(IndicesOptions.lenientExpandOpen())

x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemoverTests.java

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212
import org.elasticsearch.action.support.master.AcknowledgedResponse;
1313
import org.elasticsearch.client.internal.Client;
1414
import org.elasticsearch.client.internal.OriginSettingClient;
15-
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
16-
import org.elasticsearch.cluster.service.ClusterService;
1715
import org.elasticsearch.core.TimeValue;
1816
import org.elasticsearch.index.query.IdsQueryBuilder;
1917
import org.elasticsearch.index.reindex.DeleteByQueryAction;
2018
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
21-
import org.elasticsearch.indices.TestIndexNameExpressionResolver;
2219
import org.elasticsearch.search.SearchHit;
2320
import org.elasticsearch.tasks.TaskId;
2421
import org.elasticsearch.test.ESTestCase;
@@ -38,12 +35,14 @@
3835
import org.elasticsearch.xpack.ml.test.MockOriginSettingClient;
3936
import org.elasticsearch.xpack.ml.test.SearchHitBuilder;
4037
import org.junit.Before;
38+
import org.mockito.ArgumentMatchers;
4139
import org.mockito.invocation.InvocationOnMock;
4240
import org.mockito.stubbing.Answer;
4341

4442
import java.io.IOException;
4543
import java.util.ArrayList;
4644
import java.util.Arrays;
45+
import java.util.Collection;
4746
import java.util.Collections;
4847
import java.util.Date;
4948
import java.util.HashMap;
@@ -61,6 +60,7 @@
6160
import static org.mockito.ArgumentMatchers.any;
6261
import static org.mockito.ArgumentMatchers.anyBoolean;
6362
import static org.mockito.ArgumentMatchers.anyInt;
63+
import static org.mockito.ArgumentMatchers.anyString;
6464
import static org.mockito.ArgumentMatchers.eq;
6565
import static org.mockito.ArgumentMatchers.same;
6666
import 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

Comments
 (0)