|  | @@ -42,6 +42,7 @@ import java.io.IOException;
 | 
	
		
			
				|  |  |  import java.util.Map;
 | 
	
		
			
				|  |  |  import java.util.Set;
 | 
	
		
			
				|  |  |  import java.util.concurrent.Executor;
 | 
	
		
			
				|  |  | +import java.util.concurrent.atomic.AtomicBoolean;
 | 
	
		
			
				|  |  |  import java.util.concurrent.atomic.AtomicLong;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  /**
 | 
	
	
		
			
				|  | @@ -292,6 +293,7 @@ public final class ExchangeService extends AbstractLifecycleComponent {
 | 
	
		
			
				|  |  |          final Executor responseExecutor;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          final AtomicLong estimatedPageSizeInBytes = new AtomicLong(0L);
 | 
	
		
			
				|  |  | +        final AtomicBoolean finished = new AtomicBoolean(false);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          TransportRemoteSink(
 | 
	
		
			
				|  |  |              TransportService transportService,
 | 
	
	
		
			
				|  | @@ -311,6 +313,32 @@ public final class ExchangeService extends AbstractLifecycleComponent {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          @Override
 | 
	
		
			
				|  |  |          public void fetchPageAsync(boolean allSourcesFinished, ActionListener<ExchangeResponse> listener) {
 | 
	
		
			
				|  |  | +            if (allSourcesFinished) {
 | 
	
		
			
				|  |  | +                if (finished.compareAndSet(false, true)) {
 | 
	
		
			
				|  |  | +                    doFetchPageAsync(true, listener);
 | 
	
		
			
				|  |  | +                } else {
 | 
	
		
			
				|  |  | +                    // already finished or promised
 | 
	
		
			
				|  |  | +                    listener.onResponse(new ExchangeResponse(blockFactory, null, true));
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            } else {
 | 
	
		
			
				|  |  | +                // already finished
 | 
	
		
			
				|  |  | +                if (finished.get()) {
 | 
	
		
			
				|  |  | +                    listener.onResponse(new ExchangeResponse(blockFactory, null, true));
 | 
	
		
			
				|  |  | +                    return;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                doFetchPageAsync(false, ActionListener.wrap(r -> {
 | 
	
		
			
				|  |  | +                    if (r.finished()) {
 | 
	
		
			
				|  |  | +                        finished.set(true);
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    listener.onResponse(r);
 | 
	
		
			
				|  |  | +                }, e -> {
 | 
	
		
			
				|  |  | +                    finished.set(true);
 | 
	
		
			
				|  |  | +                    listener.onFailure(e);
 | 
	
		
			
				|  |  | +                }));
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private void doFetchPageAsync(boolean allSourcesFinished, ActionListener<ExchangeResponse> listener) {
 | 
	
		
			
				|  |  |              final long reservedBytes = allSourcesFinished ? 0 : estimatedPageSizeInBytes.get();
 | 
	
		
			
				|  |  |              if (reservedBytes > 0) {
 | 
	
		
			
				|  |  |                  // This doesn't fully protect ESQL from OOM, but reduces the likelihood.
 |