47
47
import org .apache .http .impl .nio .client .CloseableHttpAsyncClient ;
48
48
import org .apache .http .impl .nio .client .HttpAsyncClientBuilder ;
49
49
import org .apache .http .message .BasicHttpResponse ;
50
+ import org .apache .http .message .BasicRequestLine ;
50
51
import org .apache .http .message .BasicStatusLine ;
51
52
import org .apache .http .nio .protocol .HttpAsyncRequestProducer ;
52
53
import org .apache .http .nio .protocol .HttpAsyncResponseConsumer ;
54
+ import org .apache .http .protocol .HttpContext ;
53
55
import org .opensearch .LegacyESVersion ;
54
56
import org .opensearch .OpenSearchStatusException ;
55
57
import org .opensearch .Version ;
56
58
import org .opensearch .action .bulk .BackoffPolicy ;
57
59
import org .opensearch .action .search .SearchRequest ;
58
60
import org .opensearch .client .HeapBufferedAsyncResponseConsumer ;
61
+ import org .opensearch .client .ResponseException ;
59
62
import org .opensearch .client .RestClient ;
60
63
import org .opensearch .common .io .Streams ;
61
64
import org .opensearch .common .unit .TimeValue ;
79
82
80
83
import java .io .IOException ;
81
84
import java .io .InputStreamReader ;
85
+ import java .net .ConnectException ;
82
86
import java .net .URL ;
83
87
import java .nio .charset .StandardCharsets ;
84
88
import java .util .Queue ;
85
89
import java .util .concurrent .ExecutorService ;
86
90
import java .util .concurrent .Future ;
87
91
import java .util .concurrent .LinkedBlockingQueue ;
88
92
import java .util .concurrent .atomic .AtomicBoolean ;
93
+ import java .util .concurrent .atomic .AtomicInteger ;
89
94
import java .util .concurrent .atomic .AtomicReference ;
90
95
import java .util .function .Consumer ;
91
96
import java .util .stream .Stream ;
92
97
98
+ import org .mockito .Mockito ;
93
99
import org .mockito .invocation .InvocationOnMock ;
94
100
import org .mockito .stubbing .Answer ;
95
101
@@ -490,7 +496,7 @@ public void testInvalidJsonThinksRemoteIsNotES() throws IOException {
490
496
Exception e = expectThrows (RuntimeException .class , () -> sourceWithMockedRemoteCall ("some_text.txt" ).start ());
491
497
assertEquals (
492
498
"Error parsing the response, remote is likely not an OpenSearch instance" ,
493
- e .getCause ().getCause ().getCause ().getMessage ()
499
+ e .getCause ().getCause ().getCause ().getCause (). getMessage ()
494
500
);
495
501
}
496
502
@@ -499,7 +505,7 @@ public void testUnexpectedJsonThinksRemoteIsNotES() throws IOException {
499
505
Exception e = expectThrows (RuntimeException .class , () -> sourceWithMockedRemoteCall ("main/2_3_3.json" ).start ());
500
506
assertEquals (
501
507
"Error parsing the response, remote is likely not an OpenSearch instance" ,
502
- e .getCause ().getCause ().getCause ().getMessage ()
508
+ e .getCause ().getCause ().getCause ().getCause (). getMessage ()
503
509
);
504
510
}
505
511
@@ -650,4 +656,91 @@ private <T extends Exception, V> T expectListenerFailure(Class<T> expectedExcept
650
656
assertNotNull (exception .get ());
651
657
return exception .get ();
652
658
}
659
+
660
+ RemoteScrollableHitSource createRemoteSourceWithFailure (
661
+ boolean shouldMockRemoteVersion ,
662
+ Exception failure ,
663
+ AtomicInteger invocationCount
664
+ ) {
665
+ CloseableHttpAsyncClient httpClient = new CloseableHttpAsyncClient () {
666
+ @ Override
667
+ public <T > Future <T > execute (
668
+ HttpAsyncRequestProducer requestProducer ,
669
+ HttpAsyncResponseConsumer <T > responseConsumer ,
670
+ HttpContext context ,
671
+ FutureCallback <T > callback
672
+ ) {
673
+ invocationCount .getAndIncrement ();
674
+ callback .failed (failure );
675
+ return null ;
676
+ }
677
+
678
+ @ Override
679
+ public void close () throws IOException {}
680
+
681
+ @ Override
682
+ public boolean isRunning () {
683
+ return false ;
684
+ }
685
+
686
+ @ Override
687
+ public void start () {}
688
+ };
689
+ return sourceWithMockedClient (shouldMockRemoteVersion , httpClient );
690
+ }
691
+
692
+ void verifyRetries (boolean shouldMockRemoteVersion , Exception failureResponse , boolean expectedToRetry ) {
693
+ retriesAllowed = 5 ;
694
+ AtomicInteger invocations = new AtomicInteger ();
695
+ invocations .set (0 );
696
+ RemoteScrollableHitSource source = createRemoteSourceWithFailure (shouldMockRemoteVersion , failureResponse , invocations );
697
+
698
+ Throwable e = expectThrows (RuntimeException .class , source ::start );
699
+ int expectedInvocations = 0 ;
700
+ if (shouldMockRemoteVersion ) {
701
+ expectedInvocations += 1 ; // first search
702
+ if (expectedToRetry ) expectedInvocations += retriesAllowed ;
703
+ } else {
704
+ expectedInvocations = 1 ; // the first should fail and not trigger any retry.
705
+ }
706
+
707
+ assertEquals (expectedInvocations , invocations .get ());
708
+
709
+ // Unwrap the some artifacts from the test
710
+ while (e .getMessage ().equals ("failed" )) {
711
+ e = e .getCause ();
712
+ }
713
+ // There is an additional wrapper for ResponseException.
714
+ if (failureResponse instanceof ResponseException ) {
715
+ e = e .getCause ();
716
+ }
717
+
718
+ assertSame (failureResponse , e );
719
+ }
720
+
721
+ ResponseException withResponseCode (int statusCode , String errorMsg ) throws IOException {
722
+ org .opensearch .client .Response mockResponse = Mockito .mock (org .opensearch .client .Response .class );
723
+ ProtocolVersion protocolVersion = new ProtocolVersion ("https" , 1 , 1 );
724
+ Mockito .when (mockResponse .getEntity ()).thenReturn (new StringEntity (errorMsg , ContentType .TEXT_PLAIN ));
725
+ Mockito .when (mockResponse .getStatusLine ()).thenReturn (new BasicStatusLine (protocolVersion , statusCode , errorMsg ));
726
+ Mockito .when (mockResponse .getRequestLine ()).thenReturn (new BasicRequestLine ("GET" , "/" , protocolVersion ));
727
+ return new ResponseException (mockResponse );
728
+ }
729
+
730
+ public void testRetryOnCallFailure () throws Exception {
731
+ // First call succeeds. Search calls failing with 5xxs and 429s should be retried but not 400s.
732
+ verifyRetries (true , withResponseCode (500 , "Internal Server Error" ), true );
733
+ verifyRetries (true , withResponseCode (429 , "Too many requests" ), true );
734
+ verifyRetries (true , withResponseCode (400 , "Client Error" ), false );
735
+
736
+ // First call succeeds. Search call failed with exceptions other than ResponseException
737
+ verifyRetries (true , new ConnectException ("blah" ), true ); // should retry connect exceptions.
738
+ verifyRetries (true , new RuntimeException ("foobar" ), false );
739
+
740
+ // First call(remote version lookup) failed and no retries expected
741
+ verifyRetries (false , withResponseCode (500 , "Internal Server Error" ), false );
742
+ verifyRetries (false , withResponseCode (429 , "Too many requests" ), false );
743
+ verifyRetries (false , withResponseCode (400 , "Client Error" ), false );
744
+ verifyRetries (false , new ConnectException ("blah" ), false );
745
+ }
653
746
}
0 commit comments