FrankChen021 commented on code in PR #19754:
URL: https://github.com/apache/druid/pull/19754#discussion_r3674251650


##########
processing/src/main/java/org/apache/druid/java/util/http/client/pool/ChannelResourceFactory.java:
##########
@@ -273,78 +304,81 @@ public void operationComplete(ChannelFuture f2)
   @Override
   public boolean isGood(ChannelFuture resource)
   {
-    Channel channel = resource.awaitUninterruptibly().getChannel();
+    Channel channel = resource.channel();
 
     boolean isSuccess = resource.isSuccess();

Review Comment:
   [P2] Wait for connection futures before rejecting them
   
   `ResourcePool` calls `isGood` immediately after generating or borrowing a 
resource. A still-pending connect, proxy, or TLS future reports `isSuccess() == 
false`, so the pool closes a valid in-flight channel and opens a replacement. 
This creates redundant connections and handshakes for each host key. Preserve 
the previous completion wait before classifying the future.



##########
processing/src/main/java/org/apache/druid/java/util/http/client/NettyHttpClient.java:
##########
@@ -474,4 +520,124 @@ private String getPoolKey(URL url)
     return url.getProtocol() + "://" + url.getHost() + ":"
            + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
   }
+
+  /**
+   * A read-timeout handler that fires a {@link ReadTimeoutException} down the 
pipeline if no inbound message is read
+   * within the configured timeout. It behaves like Netty's {@link 
io.netty.handler.timeout.ReadTimeoutHandler} but
+   * drives its timer off a shared {@link Timer} (a {@code HashedWheelTimer} 
dedicated thread) rather than the channel's
+   * event loop.
+   *
+   * This avoids a class of problems where the event loop's blocking {@code 
epoll_wait}/{@code select} is interrupted by
+   * signals (for example a profiler agent), which can reset the wait and 
cause event-loop-scheduled timeouts to be
+   * delayed or never fire (see netty/netty#14368 and netty/netty#16244). The 
pre-Netty-4 Druid client scheduled read
+   * timeouts on a {@code HashedWheelTimer} for the same reason.
+   */
+  static class TimerReadTimeoutHandler extends ChannelInboundHandlerAdapter
+  {
+    private final Timer timer;
+    private final long timeoutNanos;
+
+    private volatile long lastReadTimeNanos;
+    private volatile Timeout scheduledTimeout;
+    private volatile boolean destroyed;
+    private boolean timedOut;
+
+    TimerReadTimeoutHandler(Timer timer, long timeoutMillis)
+    {
+      this.timer = Preconditions.checkNotNull(timer, "timer");
+      this.timeoutNanos = 
Math.max(TimeUnit.MILLISECONDS.toNanos(timeoutMillis), 1L);
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx)
+    {
+      // The channel is typically already active (taken from the pool) by the 
time this handler is added.
+      if (ctx.channel().isActive()) {
+        initialize(ctx);
+      }
+    }
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx)
+    {
+      initialize(ctx);
+      ctx.fireChannelActive();
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg)
+    {
+      lastReadTimeNanos = System.nanoTime();
+      ctx.fireChannelRead(msg);
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx)
+    {
+      destroy();
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx)
+    {
+      destroy();
+      ctx.fireChannelInactive();
+    }
+
+    private void initialize(ChannelHandlerContext ctx)
+    {
+      if (destroyed) {
+        return;
+      }
+      lastReadTimeNanos = System.nanoTime();
+      schedule(ctx, timeoutNanos);
+    }
+
+    private void schedule(final ChannelHandlerContext ctx, final long 
delayNanos)
+    {
+      if (destroyed) {
+        return;
+      }
+      scheduledTimeout = timer.newTimeout(
+          new TimerTask()
+          {
+            @Override
+            public void run(Timeout t)
+            {
+              if (t.isCancelled() || destroyed || !ctx.channel().isOpen()) {
+                return;
+              }
+
+              final long nextDelayNanos = timeoutNanos - (System.nanoTime() - 
lastReadTimeNanos);
+              if (nextDelayNanos <= 0) {
+                // Fire the timeout on the event loop, since pipeline events 
must run there.
+                ctx.executor().execute(() -> {

Review Comment:
   [P2] Recheck activity before firing the queued timeout
   
   After the timer observes the deadline, it queues this event-loop task but 
never rechecks `lastReadTimeNanos`. If the event loop processes an inbound read 
before the queued task, `channelRead` refreshes the timestamp and this task 
still closes a valid response. Recompute the remaining delay on the event loop 
and reschedule when a read arrived.



##########
server/src/main/java/org/apache/druid/discovery/DataServerResponseHandler.java:
##########
@@ -215,11 +223,11 @@ public void exceptionCaught(ClientResponse<InputStream> 
clientResponse, Throwabl
     setupResponseReadFailure(msg, e);
   }
 
-  private boolean enqueue(ChannelBuffer buffer, long chunkNum) throws 
InterruptedException
+  private boolean enqueue(ByteBuf buffer, long chunkNum) throws 
InterruptedException
   {
     // Increment queuedByteCount before queueing the object, so 
queuedByteCount is at least as high as
     // the actual number of queued bytes at any particular time.
-    final InputStreamHolder holder = 
InputStreamHolder.fromChannelBuffer(buffer, chunkNum);
+    final InputStreamHolder holder = InputStreamHolder.fromByteBuf(buffer, 
chunkNum);

Review Comment:
   The drain closes holders that were already queued, but it still races with 
enqueue. `setupResponseReadFailure` can run on the consumer thread after 
`handleChunk` passes `checkQueryTimeout()` but before `queue.put(holder)` on 
the event loop; the drain then sees nothing, the late holder retains the 
ByteBuf, and `fail` prevents the consumer from ever dequeuing it. Please 
serialize enqueue with failure teardown or recheck failure after put and 
close/remove the holder. Reviewed 171 of 171 changed files.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to