|  | @@ -52,6 +52,8 @@ import org.elasticsearch.common.unit.ByteSizeValue;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.unit.TimeValue;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.util.concurrent.EsExecutors;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException;
 | 
	
		
			
				|  |  | +import org.elasticsearch.index.reindex.RejectAwareActionListener;
 | 
	
		
			
				|  |  | +import org.elasticsearch.index.reindex.ScrollableHitSource;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.reindex.ScrollableHitSource.Response;
 | 
	
		
			
				|  |  |  import org.elasticsearch.rest.RestStatus;
 | 
	
		
			
				|  |  |  import org.elasticsearch.search.builder.SearchSourceBuilder;
 | 
	
	
		
			
				|  | @@ -67,10 +69,14 @@ import java.io.IOException;
 | 
	
		
			
				|  |  |  import java.io.InputStreamReader;
 | 
	
		
			
				|  |  |  import java.net.URL;
 | 
	
		
			
				|  |  |  import java.nio.charset.StandardCharsets;
 | 
	
		
			
				|  |  | +import java.util.Queue;
 | 
	
		
			
				|  |  |  import java.util.concurrent.ExecutorService;
 | 
	
		
			
				|  |  |  import java.util.concurrent.Future;
 | 
	
		
			
				|  |  | +import java.util.concurrent.LinkedBlockingQueue;
 | 
	
		
			
				|  |  |  import java.util.concurrent.atomic.AtomicBoolean;
 | 
	
		
			
				|  |  | +import java.util.concurrent.atomic.AtomicReference;
 | 
	
		
			
				|  |  |  import java.util.function.Consumer;
 | 
	
		
			
				|  |  | +import java.util.stream.Stream;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import static org.elasticsearch.common.unit.TimeValue.timeValueMillis;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.common.unit.TimeValue.timeValueMinutes;
 | 
	
	
		
			
				|  | @@ -91,6 +97,8 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |      private SearchRequest searchRequest;
 | 
	
		
			
				|  |  |      private int retriesAllowed;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private final Queue<ScrollableHitSource.AsyncResponse> responseQueue = new LinkedBlockingQueue<>();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      @Before
 | 
	
		
			
				|  |  |      @Override
 | 
	
		
			
				|  |  |      public void setUp() throws Exception {
 | 
	
	
		
			
				|  | @@ -122,6 +130,11 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          terminate(threadPool);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @After
 | 
	
		
			
				|  |  | +    public void validateAllConsumed() {
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public void testLookupRemoteVersion() throws Exception {
 | 
	
		
			
				|  |  |          assertLookupRemoteVersion(Version.fromString("0.20.5"), "main/0_20_5.json");
 | 
	
		
			
				|  |  |          assertLookupRemoteVersion(Version.fromString("0.90.13"), "main/0_90_13.json");
 | 
	
	
		
			
				|  | @@ -135,16 +148,17 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      private void assertLookupRemoteVersion(Version expected, String s) throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall(false, ContentType.APPLICATION_JSON, s).lookupRemoteVersion(v -> {
 | 
	
		
			
				|  |  | -            assertEquals(expected, v);
 | 
	
		
			
				|  |  | -            called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall(false, ContentType.APPLICATION_JSON, s)
 | 
	
		
			
				|  |  | +            .lookupRemoteVersion(wrapAsListener(v -> {
 | 
	
		
			
				|  |  | +                assertEquals(expected, v);
 | 
	
		
			
				|  |  | +                called.set(true);
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testParseStartOk() throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("start_ok.json").doStart(r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("start_ok.json").doStart(wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertFalse(r.isTimedOut());
 | 
	
		
			
				|  |  |              assertEquals(FAKE_SCROLL_ID, r.getScrollId());
 | 
	
		
			
				|  |  |              assertEquals(4, r.getTotalHits());
 | 
	
	
		
			
				|  | @@ -156,13 +170,13 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test2\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              assertNull(r.getHits().get(0).getRouting());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testParseScrollOk() throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("scroll_ok.json").doStartNextScroll("", timeValueMillis(0), r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("scroll_ok.json").doStartNextScroll("", timeValueMillis(0), wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertFalse(r.isTimedOut());
 | 
	
		
			
				|  |  |              assertEquals(FAKE_SCROLL_ID, r.getScrollId());
 | 
	
		
			
				|  |  |              assertEquals(4, r.getTotalHits());
 | 
	
	
		
			
				|  | @@ -174,7 +188,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test3\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              assertNull(r.getHits().get(0).getRouting());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -183,12 +197,12 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      public void testParseScrollFullyLoaded() throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("scroll_fully_loaded.json").doStartNextScroll("", timeValueMillis(0), r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("scroll_fully_loaded.json").doStartNextScroll("", timeValueMillis(0), wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertEquals("AVToMiDL50DjIiBO3yKA", r.getHits().get(0).getId());
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test3\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              assertEquals("testrouting", r.getHits().get(0).getRouting());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -197,12 +211,12 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      public void testParseScrollFullyLoadedFrom1_7() throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("scroll_fully_loaded_1_7.json").doStartNextScroll("", timeValueMillis(0), r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("scroll_fully_loaded_1_7.json").doStartNextScroll("", timeValueMillis(0), wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertEquals("AVToMiDL50DjIiBO3yKA", r.getHits().get(0).getId());
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test3\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              assertEquals("testrouting", r.getHits().get(0).getRouting());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -212,7 +226,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      public void testScanJumpStart() throws Exception {
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("start_scan.json", "scroll_ok.json").doStart(r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("start_scan.json", "scroll_ok.json").doStart(wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertFalse(r.isTimedOut());
 | 
	
		
			
				|  |  |              assertEquals(FAKE_SCROLL_ID, r.getScrollId());
 | 
	
		
			
				|  |  |              assertEquals(4, r.getTotalHits());
 | 
	
	
		
			
				|  | @@ -224,7 +238,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test3\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              assertNull(r.getHits().get(0).getRouting());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -252,10 +266,10 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test1\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("rejection.json").doStart(checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("rejection.json").doStart(wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |          called.set(false);
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("rejection.json").doStartNextScroll("scroll", timeValueMillis(0), checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("rejection.json").doStartNextScroll("scroll", timeValueMillis(0), wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -281,10 +295,11 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals("{\"test\":\"test10000\"}", r.getHits().get(0).getSource().utf8ToString());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("failure_with_status.json").doStart(checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("failure_with_status.json").doStart(wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |          called.set(false);
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("failure_with_status.json").doStartNextScroll("scroll", timeValueMillis(0), checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("failure_with_status.json").doStartNextScroll("scroll", timeValueMillis(0),
 | 
	
		
			
				|  |  | +            wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -302,48 +317,51 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals(14, failure.getColumnNumber());
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("request_failure.json").doStart(checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("request_failure.json").doStart(wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |          called.set(false);
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("request_failure.json").doStartNextScroll("scroll", timeValueMillis(0), checkResponse);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("request_failure.json").doStartNextScroll("scroll", timeValueMillis(0), wrapAsListener(checkResponse));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testRetryAndSucceed() throws Exception {
 | 
	
		
			
				|  |  | -        AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        Consumer<Response> checkResponse = r -> {
 | 
	
		
			
				|  |  | -            assertThat(r.getFailures(), hasSize(0));
 | 
	
		
			
				|  |  | -            called.set(true);
 | 
	
		
			
				|  |  | -        };
 | 
	
		
			
				|  |  |          retriesAllowed = between(1, Integer.MAX_VALUE);
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("fail:rejection.json", "start_ok.json").doStart(checkResponse);
 | 
	
		
			
				|  |  | -        assertTrue(called.get());
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("fail:rejection.json", "start_ok.json", "fail:rejection.json", "scroll_ok.json").start();
 | 
	
		
			
				|  |  | +        ScrollableHitSource.AsyncResponse response = responseQueue.poll();
 | 
	
		
			
				|  |  | +        assertNotNull(response);
 | 
	
		
			
				|  |  | +        assertThat(response.response().getFailures(), empty());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  |          assertEquals(1, retries);
 | 
	
		
			
				|  |  |          retries = 0;
 | 
	
		
			
				|  |  | -        called.set(false);
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("fail:rejection.json", "scroll_ok.json").doStartNextScroll("scroll", timeValueMillis(0),
 | 
	
		
			
				|  |  | -                checkResponse);
 | 
	
		
			
				|  |  | -        assertTrue(called.get());
 | 
	
		
			
				|  |  | +        response.done(timeValueMillis(0));
 | 
	
		
			
				|  |  | +        response = responseQueue.poll();
 | 
	
		
			
				|  |  | +        assertNotNull(response);
 | 
	
		
			
				|  |  | +        assertThat(response.response().getFailures(), empty());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  |          assertEquals(1, retries);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testRetryUntilYouRunOutOfTries() throws Exception {
 | 
	
		
			
				|  |  | -        AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        Consumer<Response> checkResponse = r -> called.set(true);
 | 
	
		
			
				|  |  |          retriesAllowed = between(0, 10);
 | 
	
		
			
				|  |  |          String[] paths = new String[retriesAllowed + 2];
 | 
	
		
			
				|  |  |          for (int i = 0; i < retriesAllowed + 2; i++) {
 | 
	
		
			
				|  |  |              paths[i] = "fail:rejection.json";
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        RuntimeException e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall(paths).doStart(checkResponse));
 | 
	
		
			
				|  |  | +        RuntimeException e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall(paths).start());
 | 
	
		
			
				|  |  |          assertEquals("failed", e.getMessage());
 | 
	
		
			
				|  |  | -        assertFalse(called.get());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  |          assertEquals(retriesAllowed, retries);
 | 
	
		
			
				|  |  |          retries = 0;
 | 
	
		
			
				|  |  | -        e = expectThrows(RuntimeException.class,
 | 
	
		
			
				|  |  | -                () -> sourceWithMockedRemoteCall(paths).doStartNextScroll("scroll", timeValueMillis(0), checkResponse));
 | 
	
		
			
				|  |  | +        String[] searchOKPaths = Stream.concat(Stream.of("start_ok.json"), Stream.of(paths)).toArray(String[]::new);
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall(searchOKPaths).start();
 | 
	
		
			
				|  |  | +        ScrollableHitSource.AsyncResponse response = responseQueue.poll();
 | 
	
		
			
				|  |  | +        assertNotNull(response);
 | 
	
		
			
				|  |  | +        assertThat(response.response().getFailures(), empty());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        e = expectThrows(RuntimeException.class, () -> response.done(timeValueMillis(0)));
 | 
	
		
			
				|  |  |          assertEquals("failed", e.getMessage());
 | 
	
		
			
				|  |  | -        assertFalse(called.get());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  |          assertEquals(retriesAllowed, retries);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -351,10 +369,10 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          String header = randomAlphaOfLength(5);
 | 
	
		
			
				|  |  |          threadPool.getThreadContext().putHeader("test", header);
 | 
	
		
			
				|  |  |          AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        sourceWithMockedRemoteCall("start_ok.json").doStart(r -> {
 | 
	
		
			
				|  |  | +        sourceWithMockedRemoteCall("start_ok.json").doStart(wrapAsListener(r -> {
 | 
	
		
			
				|  |  |              assertEquals(header, threadPool.getThreadContext().getHeader("test"));
 | 
	
		
			
				|  |  |              called.set(true);
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | +        }));
 | 
	
		
			
				|  |  |          assertTrue(called.get());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -424,10 +442,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |          RemoteScrollableHitSource source = sourceWithMockedClient(true, httpClient);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        AtomicBoolean called = new AtomicBoolean();
 | 
	
		
			
				|  |  | -        Consumer<Response> checkResponse = r -> called.set(true);
 | 
	
		
			
				|  |  | -        Throwable e = expectThrows(RuntimeException.class,
 | 
	
		
			
				|  |  | -                () -> source.doStartNextScroll(FAKE_SCROLL_ID, timeValueMillis(0), checkResponse));
 | 
	
		
			
				|  |  | +        Throwable e = expectThrows(RuntimeException.class, source::start);
 | 
	
		
			
				|  |  |          // Unwrap the some artifacts from the test
 | 
	
		
			
				|  |  |          while (e.getMessage().equals("failed")) {
 | 
	
		
			
				|  |  |              e = e.getCause();
 | 
	
	
		
			
				|  | @@ -436,24 +451,24 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          assertEquals("Remote responded with a chunk that was too large. Use a smaller batch size.", e.getMessage());
 | 
	
		
			
				|  |  |          // And that exception is reported as being caused by the underlying exception returned by the client
 | 
	
		
			
				|  |  |          assertSame(tooLong, e.getCause());
 | 
	
		
			
				|  |  | -        assertFalse(called.get());
 | 
	
		
			
				|  |  | +        assertTrue(responseQueue.isEmpty());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void testNoContentTypeIsError() throws Exception {
 | 
	
		
			
				|  |  | -        Exception e = expectThrows(RuntimeException.class, () ->
 | 
	
		
			
				|  |  | -                sourceWithMockedRemoteCall(false, null, "main/0_20_5.json").lookupRemoteVersion(null));
 | 
	
		
			
				|  |  | -        assertThat(e.getCause().getCause().getCause().getMessage(), containsString("Response didn't include Content-Type: body={"));
 | 
	
		
			
				|  |  | +    public void testNoContentTypeIsError() {
 | 
	
		
			
				|  |  | +        RuntimeException e = expectListenerFailure(RuntimeException.class, (RejectAwareActionListener<Version> listener) ->
 | 
	
		
			
				|  |  | +                sourceWithMockedRemoteCall(false, null, "main/0_20_5.json").lookupRemoteVersion(listener));
 | 
	
		
			
				|  |  | +        assertThat(e.getMessage(), containsString("Response didn't include Content-Type: body={"));
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void testInvalidJsonThinksRemoveIsNotES() throws IOException {
 | 
	
		
			
				|  |  | -        Exception e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall("some_text.txt").doStart(null));
 | 
	
		
			
				|  |  | +    public void testInvalidJsonThinksRemoteIsNotES() throws IOException {
 | 
	
		
			
				|  |  | +        Exception e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall("some_text.txt").start());
 | 
	
		
			
				|  |  |          assertEquals("Error parsing the response, remote is likely not an Elasticsearch instance",
 | 
	
		
			
				|  |  |                  e.getCause().getCause().getCause().getMessage());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void testUnexpectedJsonThinksRemoveIsNotES() throws IOException {
 | 
	
		
			
				|  |  | +    public void testUnexpectedJsonThinksRemoteIsNotES() throws IOException {
 | 
	
		
			
				|  |  |          // Use the response from a main action instead of a proper start response to generate a parse error
 | 
	
		
			
				|  |  | -        Exception e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall("main/2_3_3.json").doStart(null));
 | 
	
		
			
				|  |  | +        Exception e = expectThrows(RuntimeException.class, () -> sourceWithMockedRemoteCall("main/2_3_3.json").start());
 | 
	
		
			
				|  |  |          assertEquals("Error parsing the response, remote is likely not an Elasticsearch instance",
 | 
	
		
			
				|  |  |                  e.getCause().getCause().getCause().getMessage());
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -486,8 +501,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |       * synchronously rather than asynchronously.
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      @SuppressWarnings("unchecked")
 | 
	
		
			
				|  |  | -    private RemoteScrollableHitSource sourceWithMockedRemoteCall(boolean mockRemoteVersion, ContentType contentType, String... paths)
 | 
	
		
			
				|  |  | -            throws Exception {
 | 
	
		
			
				|  |  | +    private RemoteScrollableHitSource sourceWithMockedRemoteCall(boolean mockRemoteVersion, ContentType contentType, String... paths) {
 | 
	
		
			
				|  |  |          URL[] resources = new URL[paths.length];
 | 
	
		
			
				|  |  |          for (int i = 0; i < paths.length; i++) {
 | 
	
		
			
				|  |  |              resources[i] = Thread.currentThread().getContextClassLoader().getResource("responses/" + paths[i].replace("fail:", ""));
 | 
	
	
		
			
				|  | @@ -533,8 +547,7 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          return sourceWithMockedClient(mockRemoteVersion, httpClient);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    private RemoteScrollableHitSource sourceWithMockedClient(boolean mockRemoteVersion, CloseableHttpAsyncClient httpClient)
 | 
	
		
			
				|  |  | -            throws Exception {
 | 
	
		
			
				|  |  | +    private RemoteScrollableHitSource sourceWithMockedClient(boolean mockRemoteVersion, CloseableHttpAsyncClient httpClient) {
 | 
	
		
			
				|  |  |          HttpAsyncClientBuilder clientBuilder = mock(HttpAsyncClientBuilder.class);
 | 
	
		
			
				|  |  |          when(clientBuilder.build()).thenReturn(httpClient);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -543,11 +556,11 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          TestRemoteScrollableHitSource hitSource = new TestRemoteScrollableHitSource(restClient) {
 | 
	
		
			
				|  |  |              @Override
 | 
	
		
			
				|  |  | -            void lookupRemoteVersion(Consumer<Version> onVersion) {
 | 
	
		
			
				|  |  | +            void lookupRemoteVersion(RejectAwareActionListener<Version> listener) {
 | 
	
		
			
				|  |  |                  if (mockRemoteVersion) {
 | 
	
		
			
				|  |  | -                    onVersion.accept(Version.CURRENT);
 | 
	
		
			
				|  |  | +                    listener.onResponse(Version.CURRENT);
 | 
	
		
			
				|  |  |                  } else {
 | 
	
		
			
				|  |  | -                    super.lookupRemoteVersion(onVersion);
 | 
	
		
			
				|  |  | +                    super.lookupRemoteVersion(listener);
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          };
 | 
	
	
		
			
				|  | @@ -572,8 +585,30 @@ public class RemoteScrollableHitSourceTests extends ESTestCase {
 | 
	
		
			
				|  |  |      private class TestRemoteScrollableHitSource extends RemoteScrollableHitSource {
 | 
	
		
			
				|  |  |          TestRemoteScrollableHitSource(RestClient client) {
 | 
	
		
			
				|  |  |              super(RemoteScrollableHitSourceTests.this.logger, backoff(), RemoteScrollableHitSourceTests.this.threadPool,
 | 
	
		
			
				|  |  | -                    RemoteScrollableHitSourceTests.this::countRetry, r -> fail(), RemoteScrollableHitSourceTests.this::failRequest, client,
 | 
	
		
			
				|  |  | -                    new BytesArray("{}"), RemoteScrollableHitSourceTests.this.searchRequest);
 | 
	
		
			
				|  |  | +                RemoteScrollableHitSourceTests.this::countRetry,
 | 
	
		
			
				|  |  | +                responseQueue::add, RemoteScrollableHitSourceTests.this::failRequest,
 | 
	
		
			
				|  |  | +                client, new BytesArray("{}"), RemoteScrollableHitSourceTests.this.searchRequest);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private <T> RejectAwareActionListener<T> wrapAsListener(Consumer<T> consumer) {
 | 
	
		
			
				|  |  | +        Consumer<Exception> throwing = e -> {
 | 
	
		
			
				|  |  | +            throw new AssertionError(e);
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +        return RejectAwareActionListener.wrap(consumer::accept, throwing, throwing);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @SuppressWarnings("unchecked")
 | 
	
		
			
				|  |  | +    private <T extends Exception, V> T expectListenerFailure(Class<T> expectedException, Consumer<RejectAwareActionListener<V>> subject) {
 | 
	
		
			
				|  |  | +        AtomicReference<T> exception = new AtomicReference<>();
 | 
	
		
			
				|  |  | +        subject.accept(RejectAwareActionListener.wrap(
 | 
	
		
			
				|  |  | +            r -> fail(),
 | 
	
		
			
				|  |  | +            e -> {
 | 
	
		
			
				|  |  | +                assertThat(e, instanceOf(expectedException));
 | 
	
		
			
				|  |  | +                assertTrue(exception.compareAndSet(null, (T) e));
 | 
	
		
			
				|  |  | +            },
 | 
	
		
			
				|  |  | +            e -> fail()));
 | 
	
		
			
				|  |  | +        assertNotNull(exception.get());
 | 
	
		
			
				|  |  | +        return exception.get();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  }
 |