1+ /*
2+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+ * or more contributor license agreements. Licensed under the Elastic License
4+ * 2.0; you may not use this file except in compliance with the Elastic License
5+ * 2.0.
6+ */
7+ package org .elasticsearch .xpack .ml .datafeed ;
8+
9+ import org .elasticsearch .client .internal .Client ;
10+ import org .elasticsearch .test .ESTestCase ;
11+ import org .elasticsearch .xpack .core .ml .datafeed .DatafeedTimingStats ;
12+ import org .elasticsearch .xpack .core .ml .job .config .DataDescription ;
13+ import org .elasticsearch .xpack .ml .annotations .AnnotationPersister ;
14+ import org .elasticsearch .xpack .ml .datafeed .delayeddatacheck .DelayedDataDetector ;
15+ import org .elasticsearch .xpack .ml .datafeed .delayeddatacheck .DelayedDataDetectorFactory .BucketWithMissingData ;
16+ import org .elasticsearch .xpack .ml .datafeed .extractor .DataExtractorFactory ;
17+ import org .elasticsearch .xpack .ml .notifications .AnomalyDetectionAuditor ;
18+ import org .junit .Before ;
19+
20+ import java .io .IOException ;
21+ import java .util .Collections ;
22+ import java .util .List ;
23+ import java .util .concurrent .atomic .AtomicInteger ;
24+ import java .util .function .Supplier ;
25+
26+ import static org .mockito .ArgumentMatchers .anyLong ;
27+ import static org .mockito .Mockito .*;
28+
29+ public class DatafeedJobRetryTests extends ESTestCase {
30+
31+ private DatafeedJob datafeedJob ;
32+ private DelayedDataDetector delayedDataDetector ;
33+ private AnomalyDetectionAuditor auditor ;
34+ private Client client ;
35+ private DataExtractorFactory dataExtractorFactory ;
36+ private DatafeedTimingStatsReporter timingStatsReporter ;
37+ private AnnotationPersister annotationPersister ;
38+
39+ @ Before
40+ public void setup () {
41+ String jobId = "test-job" ;
42+ DataDescription dataDescription = new DataDescription .Builder ().build ();
43+ long frequencyMs = 60000 ;
44+ long queryDelayMs = 1000 ;
45+
46+ client = mock (Client .class );
47+ dataExtractorFactory = mock (DataExtractorFactory .class );
48+ timingStatsReporter = mock (DatafeedTimingStatsReporter .class );
49+ auditor = mock (AnomalyDetectionAuditor .class );
50+ annotationPersister = mock (AnnotationPersister .class );
51+ delayedDataDetector = mock (DelayedDataDetector .class );
52+
53+ Supplier <Long > currentTimeSupplier = System ::currentTimeMillis ;
54+ Integer maxEmptySearches = 10 ;
55+ long latestFinalBucketEndTimeMs = System .currentTimeMillis () - 3600000 ;
56+ long latestRecordTimeMs = System .currentTimeMillis () - 1800000 ;
57+ boolean haveSeenDataPreviously = true ;
58+ long delayedDataCheckFreq = 900000 ; // 15 minutes
59+
60+ datafeedJob = new DatafeedJob (
61+ jobId ,
62+ dataDescription ,
63+ frequencyMs ,
64+ queryDelayMs ,
65+ dataExtractorFactory ,
66+ timingStatsReporter ,
67+ client ,
68+ auditor ,
69+ annotationPersister ,
70+ currentTimeSupplier ,
71+ delayedDataDetector ,
72+ maxEmptySearches ,
73+ latestFinalBucketEndTimeMs ,
74+ latestRecordTimeMs ,
75+ haveSeenDataPreviously ,
76+ delayedDataCheckFreq
77+ );
78+ }
79+
80+ public void testCheckForMissingDataRetriesOnFailure () throws Exception {
81+ // Simulate failures followed by success
82+ AtomicInteger callCount = new AtomicInteger (0 );
83+ when (delayedDataDetector .detectMissingData (anyLong ())).thenAnswer (invocation -> {
84+ int count = callCount .incrementAndGet ();
85+ if (count <= 2 ) {
86+ throw new IOException ("Simulated failure " + count );
87+ }
88+ return Collections .emptyList ();
89+ });
90+
91+ // This should trigger the retry logic
92+ // Note: We would need to make checkForMissingDataIfNecessary accessible for testing
93+ // or test through a public method that calls it
94+
95+ // Verify that detectMissingData was called 3 times (2 failures + 1 success)
96+ Thread .sleep (1000 ); // Allow time for retries
97+ verify (delayedDataDetector , times (3 )).detectMissingData (anyLong ());
98+ verify (auditor , never ()).warning (anyString (), anyString ());
99+ }
100+
101+ public void testCheckForMissingDataFailsAfterMaxRetries () throws Exception {
102+ // Simulate continuous failures
103+ when (delayedDataDetector .detectMissingData (anyLong ()))
104+ .thenThrow (new IOException ("Persistent failure" ));
105+
106+ // This should exhaust all retries
107+ // Note: We would need to make checkForMissingDataIfNecessary accessible for testing
108+
109+ // Verify that detectMissingData was called 4 times (initial + 3 retries)
110+ Thread .sleep (2000 ); // Allow time for all retries
111+ verify (delayedDataDetector , times (4 )).detectMissingData (anyLong ());
112+ // Verify that warning was issued after all retries failed
113+ verify (auditor , times (1 )).warning (eq ("test-job" ), contains ("Failed to check for missing data after 4 attempts" ));
114+ }
115+
116+ public void testCheckForMissingDataSucceedsOnFirstAttempt () throws Exception {
117+ // Simulate immediate success
118+ List <BucketWithMissingData > emptyList = Collections .emptyList ();
119+ when (delayedDataDetector .detectMissingData (anyLong ())).thenReturn (emptyList );
120+
121+ // This should succeed immediately without retries
122+
123+ // Verify that detectMissingData was called only once
124+ verify (delayedDataDetector , times (1 )).detectMissingData (anyLong ());
125+ verify (auditor , never ()).warning (anyString (), anyString ());
126+ }
127+
128+ public void testExponentialBackoffDelays () throws Exception {
129+ // Test that backoff delays increase exponentially
130+ AtomicInteger callCount = new AtomicInteger (0 );
131+ long startTime = System .currentTimeMillis ();
132+
133+ when (delayedDataDetector .detectMissingData (anyLong ())).thenAnswer (invocation -> {
134+ int count = callCount .incrementAndGet ();
135+ if (count <= 3 ) {
136+ throw new IOException ("Simulated failure " + count );
137+ }
138+ return Collections .emptyList ();
139+ });
140+
141+ // Execute and measure time
142+ // The total time should be at least 100ms + 200ms + 400ms = 700ms
143+
144+ Thread .sleep (1500 ); // Allow time for all retries with backoff
145+ long elapsedTime = System .currentTimeMillis () - startTime ;
146+
147+ // Verify exponential backoff was applied
148+ assertTrue ("Expected at least 700ms due to backoff delays" , elapsedTime >= 700 );
149+ verify (delayedDataDetector , times (4 )).detectMissingData (anyLong ());
150+ }
151+ }
0 commit comments