This is an automated email from the ASF dual-hosted git repository.

chungen0126 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 80be70dcdc6 HDDS-16324. XceiverClientGrpc.streamRead should wait for 
flow-control readiness via onReadyHandler instead of a 10 ms poll (#11195)
80be70dcdc6 is described below

commit 80be70dcdc61c95ff2f07f4580aa061a574ab776
Author: Sergey Soldatov <[email protected]>
AuthorDate: Tue Sep 15 11:53:09 2026 -0700

    HDDS-16324. XceiverClientGrpc.streamRead should wait for flow-control 
readiness via onReadyHandler instead of a 10 ms poll (#11195)
---
 .../apache/hadoop/hdds/scm/XceiverClientGrpc.java  |  68 ++++++--
 .../hadoop/hdds/scm/StreamingReadResponse.java     |  52 +++++++
 .../hadoop/hdds/scm/TestStreamingReadResponse.java | 172 +++++++++++++++++++++
 .../hadoop/hdds/scm/TestXceiverClientGrpc.java     |  73 +++++++--
 4 files changed, 334 insertions(+), 31 deletions(-)

diff --git 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
index 93e7c005934..4fe56e6ac09 100644
--- 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
+++ 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientGrpc.java
@@ -39,7 +39,6 @@
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.LockSupport;
 import org.apache.hadoop.hdds.HddsConfigKeys;
 import org.apache.hadoop.hdds.HddsUtils;
 import org.apache.hadoop.hdds.client.BlockID;
@@ -72,6 +71,7 @@
 import org.apache.ratis.thirdparty.io.grpc.netty.GrpcSslContexts;
 import org.apache.ratis.thirdparty.io.grpc.netty.NettyChannelBuilder;
 import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
+import org.apache.ratis.thirdparty.io.grpc.stub.ClientResponseObserver;
 import org.apache.ratis.thirdparty.io.grpc.stub.StreamObserver;
 import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContextBuilder;
 import org.slf4j.Logger;
@@ -573,16 +573,15 @@ public void streamRead(ContainerCommandRequestProto 
request,
           request.getReadBlock().getBlockID().getLocalID(),
           request.getReadBlock().getOffset(),
           request.getReadBlock().getLength());
-      final long deadlineNs = System.nanoTime() + streamReadTimeoutNanos;
-      while (!obs.isReady() && System.nanoTime() - deadlineNs < 0) {
-        LockSupport.parkNanos(10_000_000L);
-        if (Thread.currentThread().isInterrupted()) {
-          Thread.currentThread().interrupt();
-          throw new InterruptedIOException("Interrupted while waiting for 
stream to become ready: " + streamObserver);
+      try {
+        if (!streamObserver.awaitReady(streamReadTimeoutNanos)) {
+          throw new TimeoutIOException("Timed out waiting for stream to become 
ready: " + streamObserver);
         }
-      }
-      if (!obs.isReady()) {
-        throw new TimeoutIOException("Timed out waiting for stream to become 
ready: " + streamObserver);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw (IOException) new InterruptedIOException(
+            "Interrupted while waiting for stream to become ready: " + 
streamObserver)
+            .initCause(e);
       }
     }
 
@@ -619,11 +618,8 @@ public void initStreamRead(BlockID blockID, 
StreamingReaderSpi streamObserver,
           throw new IOException("Failed to get gRPC stub for DataNode: " + dn);
         }
         LOG.debug("initStreamRead {} on datanode {}", 
blockID.getContainerBlockID(), dn);
-        StreamObserver<ContainerCommandRequestProto> requestObserver = stub
-            .withDeadlineAfter(timeout, TimeUnit.SECONDS)
-            .send(streamObserver);
-        streamObserver.setStreamingReadResponse(new StreamingReadResponse(dn,
-            (ClientCallStreamObserver<ContainerCommandRequestProto>) 
requestObserver));
+        stub.withDeadlineAfter(timeout, TimeUnit.SECONDS)
+            .send(new ReadyAwareResponseObserver(dn, streamObserver));
         return;
       } catch (IOException e) {
         LOG.error("Failed to start streaming read to DataNode {}", dn, e);
@@ -641,6 +637,48 @@ public void initStreamRead(BlockID blockID, 
StreamingReaderSpi streamObserver,
     }
   }
 
+  /**
+   * Registers the request stream's {@code onReadyHandler} in {@code 
beforeStart}, the only point gRPC allows it,
+   * hands the {@link StreamingReadResponse} to the reader from there, and 
tells it when the call terminates so a
+   * sender blocked in {@link StreamingReadResponse#awaitReady(long)} fails 
right away.
+   */
+  @VisibleForTesting
+  static final class ReadyAwareResponseObserver
+      implements ClientResponseObserver<ContainerCommandRequestProto, 
ContainerCommandResponseProto> {
+    private final DatanodeDetails dn;
+    private final StreamingReaderSpi reader;
+    private StreamingReadResponse response;
+
+    ReadyAwareResponseObserver(DatanodeDetails dn, StreamingReaderSpi reader) {
+      this.dn = dn;
+      this.reader = reader;
+    }
+
+    @Override
+    public void 
beforeStart(ClientCallStreamObserver<ContainerCommandRequestProto> 
requestStream) {
+      response = new StreamingReadResponse(dn, requestStream);
+      requestStream.setOnReadyHandler(response::signalReady);
+      reader.setStreamingReadResponse(response);
+    }
+
+    @Override
+    public void onNext(ContainerCommandResponseProto value) {
+      reader.onNext(value);
+    }
+
+    @Override
+    public void onError(Throwable t) {
+      response.signalTerminated(t);
+      reader.onError(t);
+    }
+
+    @Override
+    public void onCompleted() {
+      response.signalTerminated(null);
+      reader.onCompleted();
+    }
+  }
+
   /**
    * This method should be called to indicate the end of streaming read. Its 
primary purpose is to release the
    * semaphore acquired when starting the streaming read, but is also used to 
update any metrics or debug logs as
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/StreamingReadResponse.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/StreamingReadResponse.java
index 3018fda7ea6..e78eab4e365 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/StreamingReadResponse.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/StreamingReadResponse.java
@@ -17,6 +17,8 @@
 
 package org.apache.hadoop.hdds.scm;
 
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
 import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
@@ -27,6 +29,11 @@
  */
 public class StreamingReadResponse {
 
+  private final Object readyLock = new Object();
+  /** Set once the call has terminated; guarded by {@link #readyLock}. */
+  private boolean terminated;
+  /** The failure that terminated the call, or null if it completed normally; 
guarded by {@link #readyLock}. */
+  private Throwable terminationCause;
   private final DatanodeDetails dn;
   private final 
ClientCallStreamObserver<ContainerProtos.ContainerCommandRequestProto> 
requestObserver;
   private final String name;
@@ -48,6 +55,51 @@ public 
ClientCallStreamObserver<ContainerProtos.ContainerCommandRequestProto> ge
     return requestObserver;
   }
 
+  /** Registered as the request stream's {@code onReadyHandler}. */
+  public void signalReady() {
+    synchronized (readyLock) {
+      readyLock.notifyAll();
+    }
+  }
+
+  /**
+   * Called when the call has terminated, so that {@link #awaitReady(long)} 
fails immediately instead of waiting
+   * for the timeout: a terminated stream never becomes ready and gRPC no 
longer invokes the {@code onReadyHandler}.
+   *
+   * @param cause the error that terminated the call, or null if it completed 
normally.
+   */
+  public void signalTerminated(Throwable cause) {
+    synchronized (readyLock) {
+      terminated = true;
+      terminationCause = cause;
+      readyLock.notifyAll();
+    }
+  }
+
+  /**
+   * Wait until the request stream can accept another message.
+   *
+   * @return true if the stream is ready, false if the timeout expired first.
+   * @throws IOException if the call terminated before the stream became ready.
+   */
+  public boolean awaitReady(long timeoutNanos) throws InterruptedException, 
IOException {
+    final long deadlineNanos = System.nanoTime() + timeoutNanos;
+    synchronized (readyLock) {
+      while (!requestObserver.isReady()) {
+        if (terminated) {
+          throw new IOException("Stream " + name + " terminated while waiting 
for it to become ready",
+              terminationCause);
+        }
+        final long remainingNanos = deadlineNanos - System.nanoTime();
+        if (remainingNanos <= 0) {
+          return false;
+        }
+        TimeUnit.NANOSECONDS.timedWait(readyLock, remainingNanos);
+      }
+    }
+    return true;
+  }
+
   @Override
   public String toString() {
     return name;
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/TestStreamingReadResponse.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/TestStreamingReadResponse.java
new file mode 100644
index 00000000000..14a76b35e9f
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/TestStreamingReadResponse.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
+import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
+import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link StreamingReadResponse#awaitReady(long)}.
+ */
+class TestStreamingReadResponse {
+
+  private static final class FakeRequestObserver extends 
ClientCallStreamObserver<ContainerCommandRequestProto> {
+    private final AtomicBoolean ready = new AtomicBoolean();
+
+    @Override
+    public boolean isReady() {
+      return ready.get();
+    }
+
+    @Override
+    public void setOnReadyHandler(Runnable onReadyHandler) {
+    }
+
+    @Override
+    public void disableAutoInboundFlowControl() {
+    }
+
+    @Override
+    public void request(int count) {
+    }
+
+    @Override
+    public void setMessageCompression(boolean enable) {
+    }
+
+    @Override
+    public void cancel(String message, Throwable cause) {
+    }
+
+    @Override
+    public void onNext(ContainerCommandRequestProto value) {
+    }
+
+    @Override
+    public void onError(Throwable t) {
+    }
+
+    @Override
+    public void onCompleted() {
+    }
+  }
+
+  private static StreamingReadResponse newResponse(FakeRequestObserver 
observer) {
+    return new 
StreamingReadResponse(MockDatanodeDetails.randomDatanodeDetails(), observer);
+  }
+
+  @Test
+  void returnsImmediatelyWhenAlreadyReady() throws Exception {
+    final FakeRequestObserver observer = new FakeRequestObserver();
+    observer.ready.set(true);
+    assertTrue(newResponse(observer).awaitReady(TimeUnit.SECONDS.toNanos(10)));
+  }
+
+  @Test
+  void timesOutWithoutASignal() throws Exception {
+    final FakeRequestObserver observer = new FakeRequestObserver();
+    final long start = System.nanoTime();
+    
assertFalse(newResponse(observer).awaitReady(TimeUnit.MILLISECONDS.toNanos(50)));
+    assertTrue(System.nanoTime() - start >= TimeUnit.MILLISECONDS.toNanos(50), 
"must wait out the timeout");
+  }
+
+  @Test
+  void wakesOnSignalReady() throws Exception {
+    final FakeRequestObserver observer = new FakeRequestObserver();
+    final StreamingReadResponse response = newResponse(observer);
+    final AtomicBoolean result = new AtomicBoolean();
+    final Thread waiter = new Thread(() -> {
+      try {
+        result.set(response.awaitReady(TimeUnit.SECONDS.toNanos(30)));
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
+    });
+    waiter.start();
+    awaitBlocked(waiter);
+
+    // a spurious signalReady() while isReady() is still false must not wake 
the waiter with a wrong result
+    response.signalReady();
+    awaitBlocked(waiter);
+    assertTrue(waiter.isAlive(), "waiter must still be waiting after a 
spurious signalReady()");
+
+    observer.ready.set(true);
+    response.signalReady();
+    waiter.join(TimeUnit.SECONDS.toMillis(10));
+    assertFalse(waiter.isAlive(), "waiter did not return");
+    assertTrue(result.get());
+  }
+
+  /** Waits until {@code thread} is actually parked in {@link 
StreamingReadResponse#awaitReady(long)}. */
+  private static void awaitBlocked(Thread thread) throws Exception {
+    GenericTestUtils.waitFor(() -> thread.getState() == 
Thread.State.TIMED_WAITING, 10, 10_000);
+  }
+
+  @Test
+  void failsImmediatelyWhenAlreadyTerminated() {
+    final FakeRequestObserver observer = new FakeRequestObserver();
+    final StreamingReadResponse response = newResponse(observer);
+    final Throwable cause = new RuntimeException("boom");
+    response.signalTerminated(cause);
+    final long start = System.nanoTime();
+    final IOException e = assertThrows(IOException.class, () -> 
response.awaitReady(TimeUnit.SECONDS.toNanos(30)));
+    assertSame(cause, e.getCause());
+    assertTrue(System.nanoTime() - start < TimeUnit.SECONDS.toNanos(5), "must 
not wait out the timeout");
+  }
+
+  @Test
+  void wakesOnSignalTerminated() throws Exception {
+    final FakeRequestObserver observer = new FakeRequestObserver();
+    final StreamingReadResponse response = newResponse(observer);
+    final CountDownLatch waiting = new CountDownLatch(1);
+    final AtomicReference<Throwable> failure = new AtomicReference<>();
+    final Thread waiter = new Thread(() -> {
+      try {
+        waiting.countDown();
+        response.awaitReady(TimeUnit.SECONDS.toNanos(30));
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      } catch (IOException e) {
+        failure.set(e);
+      }
+    });
+    waiter.start();
+    assertTrue(waiting.await(10, TimeUnit.SECONDS));
+
+    response.signalTerminated(null);
+    waiter.join(TimeUnit.SECONDS.toMillis(10));
+    assertFalse(waiter.isAlive(), "waiter did not return");
+    assertTrue(failure.get() instanceof IOException, "waiter was not failed by 
signalTerminated");
+  }
+}
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpc.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpc.java
index 8cfcac04334..b2ff5ea70d9 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpc.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestXceiverClientGrpc.java
@@ -21,6 +21,8 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
@@ -29,6 +31,9 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.hadoop.hdds.client.BlockID;
@@ -44,10 +49,12 @@
 import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
 import org.apache.hadoop.hdds.scm.storage.ContainerProtocolCalls;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.ozone.test.GenericTestUtils;
 import org.apache.ratis.protocol.exceptions.TimeoutIOException;
 import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 /**
  * Tests for TestXceiverClientGrpc, to ensure topology aware reads work
@@ -300,7 +307,7 @@ private void 
invokeXceiverClientReadSmallFile(XceiverClientSpi client)
   /** streamRead() calls onNext() immediately when isReady() is true from the 
start. */
   @Test
   public void testStreamReadSendsImmediatelyWhenReady() throws Exception {
-    TrackingStreamObserver obs = new TrackingStreamObserver(0);
+    TrackingStreamObserver obs = new TrackingStreamObserver(true);
     StreamingReadResponse response = new StreamingReadResponse(
         MockDatanodeDetails.randomDatanodeDetails(), obs);
     ContainerProtos.ContainerCommandRequestProto request = 
buildReadBlockRequest();
@@ -315,21 +322,41 @@ public void testStreamReadSendsImmediatelyWhenReady() 
throws Exception {
         "isReady() must be checked exactly once when stream is immediately 
ready");
   }
 
-  /** streamRead() spin-waits until isReady() becomes true, then calls 
onNext(). */
+  /** streamRead() blocks until the onReadyHandler fires and isReady() becomes 
true, then calls onNext(). */
   @Test
   public void testStreamReadWaitsUntilReadyThenSends() throws Exception {
-    TrackingStreamObserver obs = new TrackingStreamObserver(3);
-    StreamingReadResponse response = new StreamingReadResponse(
-        MockDatanodeDetails.randomDatanodeDetails(), obs);
+    TrackingStreamObserver obs = new TrackingStreamObserver(false);
+    StreamingReaderSpi reader = mock(StreamingReaderSpi.class);
+    new 
XceiverClientGrpc.ReadyAwareResponseObserver(MockDatanodeDetails.randomDatanodeDetails(),
 reader)
+        .beforeStart(obs);
+    ArgumentCaptor<StreamingReadResponse> captured = 
ArgumentCaptor.forClass(StreamingReadResponse.class);
+    verify(reader).setStreamingReadResponse(captured.capture());
     ContainerProtos.ContainerCommandRequestProto request = 
buildReadBlockRequest();
 
-    try (XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf)) {
-      client.streamRead(request, response);
+    OzoneConfiguration longTimeout = new OzoneConfiguration();
+    longTimeout.set("ozone.client.stream.read.timeout", "60s");
+    try (XceiverClientGrpc client = new XceiverClientGrpc(pipeline, 
longTimeout)) {
+      FutureTask<Void> send = new FutureTask<>(() -> {
+        client.streamRead(request, captured.getValue());
+        return null;
+      });
+      Thread sender = new Thread(send);
+      sender.start();
+      try {
+        GenericTestUtils.waitFor(() -> sender.getState() == 
Thread.State.TIMED_WAITING, 10, 5_000);
+        assertThrows(TimeoutException.class, () -> send.get(200, 
TimeUnit.MILLISECONDS));
+        assertThat(obs.getSent()).isEmpty();
+        obs.markReady();
+        send.get(5, TimeUnit.SECONDS);
+        assertThat(obs.getReadyCalls().get()).as("isReady() calls must stay 
bounded while waiting").isBetween(3, 5);
+      } finally {
+        sender.interrupt();
+        sender.join(TimeUnit.SECONDS.toMillis(5));
+      }
     }
 
     assertEquals(1, obs.getSent().size(), "onNext must be called exactly 
once");
     assertEquals(request, obs.getSent().get(0));
-    assertThat(obs.getReadyCalls().get()).isGreaterThanOrEqualTo(4);
   }
 
   /**
@@ -340,7 +367,7 @@ public void testStreamReadFailsAfterTimeoutIfNeverReady() 
throws Exception {
     OzoneConfiguration timeoutConf = new OzoneConfiguration();
     timeoutConf.set("ozone.client.stream.read.timeout", "1s");
 
-    TrackingStreamObserver obs = new TrackingStreamObserver(Integer.MAX_VALUE);
+    TrackingStreamObserver obs = new TrackingStreamObserver(false);
     StreamingReadResponse response = new StreamingReadResponse(
         MockDatanodeDetails.randomDatanodeDetails(), obs);
     ContainerProtos.ContainerCommandRequestProto request = 
buildReadBlockRequest();
@@ -357,10 +384,10 @@ public void testStreamReadFailsAfterTimeoutIfNeverReady() 
throws Exception {
     assertThat(elapsed).isLessThan(10_000L);
   }
 
-  /** streamRead() exits the spin-wait immediately on interrupt and restores 
the interrupt flag. */
+  /** streamRead() exits the ready wait immediately on interrupt and restores 
the interrupt flag. */
   @Test
   public void testStreamReadRestoresInterruptFlagOnInterruption() throws 
Exception {
-    TrackingStreamObserver obs = new TrackingStreamObserver(Integer.MAX_VALUE);
+    TrackingStreamObserver obs = new TrackingStreamObserver(false);
     StreamingReadResponse response = new StreamingReadResponse(
         MockDatanodeDetails.randomDatanodeDetails(), obs);
     ContainerProtos.ContainerCommandRequestProto request = 
buildReadBlockRequest();
@@ -390,16 +417,28 @@ public void 
testStreamReadRestoresInterruptFlagOnInterruption() throws Exception
     }
   }
 
-  /** Records onNext() calls and controls when isReady() starts returning 
true. */
+  /**
+   * Records onNext() calls and models gRPC flow control: isReady() reflects a 
flag that {@link #markReady()} flips,
+   * after which the registered onReadyHandler is invoked, as gRPC does when 
the transport becomes writable.
+   */
   private static final class TrackingStreamObserver
       extends 
ClientCallStreamObserver<ContainerProtos.ContainerCommandRequestProto> {
 
     private final List<ContainerProtos.ContainerCommandRequestProto> sent = 
new ArrayList<>();
     private final AtomicInteger readyCalls = new AtomicInteger();
-    private final int readyAfter;
+    private volatile boolean ready;
+    private volatile Runnable onReadyHandler;
 
-    TrackingStreamObserver(int readyAfter) {
-      this.readyAfter = readyAfter;
+    TrackingStreamObserver(boolean ready) {
+      this.ready = ready;
+    }
+
+    void markReady() {
+      ready = true;
+      final Runnable handler = onReadyHandler;
+      if (handler != null) {
+        handler.run();
+      }
     }
 
     List<ContainerProtos.ContainerCommandRequestProto> getSent() {
@@ -412,7 +451,8 @@ AtomicInteger getReadyCalls() {
 
     @Override
     public boolean isReady() {
-      return readyCalls.incrementAndGet() > readyAfter;
+      readyCalls.incrementAndGet();
+      return ready;
     }
 
     @Override
@@ -426,6 +466,7 @@ public void cancel(String msg, Throwable cause) {
 
     @Override
     public void setOnReadyHandler(Runnable r) {
+      onReadyHandler = r;
     }
 
     @Override


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to