xiangfu0 commented on code in PR #19873:
URL: https://github.com/apache/pinot/pull/19873#discussion_r4278301124


##########
pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java:
##########
@@ -2875,6 +2875,20 @@ public static class MultiStageQueryRunner {
         "pinot.query.runner.mailbox.server.permit.keep.alive.without.calls";
     public static final boolean 
DEFAULT_OF_MAILBOX_SERVER_PERMIT_KEEP_ALIVE_WITHOUT_CALLS = false;
 
+    /// Whether the mailbox server and the query server of an instance reject 
a request addressed to another instance.
+    ///
+    /// Senders identify the instance they mean in the request headers. A 
request reaches the wrong instance when the
+    /// sender's connection is stale: for example after two server pods 
restart together and take each other's IP
+    /// addresses, while every server listens on the same port. If the wrong 
instance accepts the request, the intended
+    /// one waits for data until the query times out, and the wrong one runs 
the stage plan, or keeps the block in a
+    /// mailbox that nothing reads. When the wrong instance rejects the 
request, the sender resets the connection and
+    /// the query fails with a clear error, in most cases at once rather than 
at its deadline.
+    ///
+    /// Requests without the headers, as sent by older versions, are always 
accepted. The value is read when the
+    /// instance starts, on brokers and servers.
+    public static final String KEY_OF_REJECT_MISROUTED_REQUESTS = 
"pinot.query.runner.reject.misrouted.requests";
+    public static final boolean DEFAULT_REJECT_MISROUTED_REQUESTS = true;

Review Comment:
   This is a new behavior-changing check defaulting to ON (C1.6 usually wants 
new features OFF). It's backward-compatible since older senders are accepted, 
but a true-by-default check that can *reject* traffic is exactly where a subtle 
id mismatch (e.g. server's `CONFIG_OF_INSTANCE_ID` diverging from the Helix 
instance name the broker routes by) would silently fail queries instead of 
hanging them. Worth confirming in the description why ON-by-default is safe for 
existing clusters, and whether a release-notes callout is warranted given it's 
a default-on safety behavior.



##########
pinot-query-planner/src/main/java/org/apache/pinot/query/routing/QueryServerInstance.java:
##########
@@ -60,6 +75,11 @@ public int getQueryServicePort() {
     return _queryServicePort;
   }
 
+  /// Returns the hash of the instance id. See [#hashInstanceId(String)].
+  public long getInstanceIdHash() {
+    return _instanceIdHash;

Review Comment:
   Note that `equals`/`hashCode` (just below) deliberately exclude this hash 
and the instance id, so two instances sharing hostname:port collapse to one key 
— e.g. in `createMailboxInfo`'s `workersByUniqueHostPort` grouping. That's fine 
at plan time (distinct instances have distinct hostnames), but now that the 
identity hash rides along it's a latent trap if a future caller ever keys on 
these objects expecting id-distinctness. A one-line comment noting the equality 
contract is intentionally address-only would help.



##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java:
##########
@@ -408,13 +453,83 @@ public void onCompleted() {
           }
         };
 
+    return openStream(responseObserver);
+  }
+
+  private ClientCallStreamObserver<MailboxContent> 
openStream(StreamObserver<MailboxStatus> responseObserver) {
+    Metadata metadata = new Metadata();
+    metadata.put(ChannelUtils.MAILBOX_ID_METADATA_KEY, _id);
+    ReceiverIdentity.addTargetInstanceIdHash(metadata, 
_receiverInstanceIdHash);
     return (ClientCallStreamObserver<MailboxContent>) PinotMailboxGrpc.newStub(
             _channelManager.getChannel(_hostname, _port))
         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata))
         .withDeadlineAfter(_deadlineMs - System.currentTimeMillis(), 
TimeUnit.MILLISECONDS)
         .open(responseObserver);
   }
 
+  /// Handles the rejection of the stream by an instance that is not the 
receiver the plan meant. Runs on a gRPC
+  /// thread, after [#_misroutedError] is set.
+  ///
+  /// The channel to [#_hostname] leads to another instance, typically because 
that instance took over the IP address
+  /// the hostname used to resolve to and listens on the same port. Whatever 
this mailbox sent went nowhere. So:
+  ///
+  /// - the channel is refreshed, so that the next stream to this address 
resolves the hostname again;
+  /// - every later data send fails, which fails this stage and, through its 
error EOS, the query;
+  /// - the intended receiver is told through a new stream, so that it fails 
now instead of waiting for data until the
+  ///   query times out. This is best effort, see [#rerouteError].

Review Comment:
   The best-effort reroute here is nicely documented, but the "DNS still 
returns the stale address so the reroute stream is itself rejected" branch (the 
`onError`/`isMisrouted` path in `rerouteError`) looks untested — every test 
remaps DNS to the new A before the reroute runs, so it always succeeds. Since 
that's the whole reason this path is best-effort, a test that leaves DNS 
pointing at B would be worth adding to lock in the second `refreshChannel` 
behavior.



##########
pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineIntegrationTest.java:
##########
@@ -295,6 +311,94 @@ public void 
testAllLeafStagesEmptyBrokerResponsesWithPhysicalOptimizer()
         List.of(), "LONG", "LONG");
   }
 
+  /// The mailbox and query servers of a running cluster reject requests that 
say they are meant for another instance.
+  /// Shows that the instance ids reach the receivers of a real server and 
broker, and that the check is on by default.
+  /// The other tests of this class show that the requests of real plans, 
which carry the ids, are accepted.
+  @Test
+  public void testReceiversRejectRequestsForAnotherInstance()
+      throws Exception {
+    String otherInstanceId = "Server_other-host_8098";
+    String serverInstanceId = _serverStarters.get(0).getInstanceId();
+    InstanceConfig serverConfig = 
_helixAdmin.getInstanceConfig(getHelixClusterName(), serverInstanceId);
+    // Every instance of the test cluster runs on this host
+    String host = "localhost";
+
+    // Stage plan request (the cancel of an unknown query, which is harmless 
when accepted)
+    int queryServerPort = serverConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_SERVICE_PORT_KEY,
 -1);
+    ManagedChannel queryServerChannel = ManagedChannelBuilder.forAddress(host, 
queryServerPort).usePlaintext().build();
+    try {
+      Metadata headers = new Metadata();
+      ReceiverIdentity.addTargetInstanceIdHash(headers, 
QueryServerInstance.hashInstanceId(otherInstanceId));
+      StatusRuntimeException e = expectThrows(StatusRuntimeException.class, () 
-> cancel(queryServerChannel, headers));
+      assertMisroutedTo(e, serverInstanceId);
+
+      Metadata ownHeaders = new Metadata();
+      ReceiverIdentity.addTargetInstanceIdHash(ownHeaders, 
QueryServerInstance.hashInstanceId(serverInstanceId));
+      cancel(queryServerChannel, ownHeaders);
+    } finally {
+      queryServerChannel.shutdownNow();
+    }
+
+    // Mailbox streams, to the server and to the broker
+    int serverMailboxPort = serverConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY,
 -1);
+    assertMailboxStreamRejected(host, serverMailboxPort, otherInstanceId, 
serverInstanceId);
+    String brokerInstanceId = _brokerStarters.get(0).getInstanceId();
+    InstanceConfig brokerConfig = 
_helixAdmin.getInstanceConfig(getHelixClusterName(), brokerInstanceId);
+    int brokerMailboxPort = brokerConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY,
 -1);
+    assertMailboxStreamRejected(host, brokerMailboxPort, otherInstanceId, 
brokerInstanceId);
+  }
+
+  private static void cancel(ManagedChannel channel, Metadata headers) {
+    
PinotQueryWorkerGrpc.newBlockingStub(channel).withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
+        .withDeadlineAfter(10, 
TimeUnit.SECONDS).cancel(Worker.CancelRequest.newBuilder().setRequestId(-1).build());
+  }
+
+  private static void assertMailboxStreamRejected(String host, int port, 
String targetInstanceId,
+      String receiverInstanceId)
+      throws Exception {
+    ManagedChannel channel = ManagedChannelBuilder.forAddress(host, 
port).usePlaintext().build();
+    try {
+      Metadata headers = new Metadata();
+      headers.put(ChannelUtils.MAILBOX_ID_METADATA_KEY, "-1|1|0|0|0");
+      ReceiverIdentity.addTargetInstanceIdHash(headers, 
QueryServerInstance.hashInstanceId(targetInstanceId));
+      CompletableFuture<Throwable> error = new CompletableFuture<>();
+      StreamObserver<Mailbox.MailboxContent> stream = 
PinotMailboxGrpc.newStub(channel)
+          .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
+          .withDeadlineAfter(10, TimeUnit.SECONDS)
+          .open(new StreamObserver<>() {
+            @Override
+            public void onNext(Mailbox.MailboxStatus value) {
+            }
+
+            @Override
+            public void onError(Throwable t) {
+              error.complete(t);
+            }
+
+            @Override
+            public void onCompleted() {
+              error.complete(null);
+            }
+          });
+      
stream.onNext(Mailbox.MailboxContent.newBuilder().setMailboxId("-1|1|0|0|0").setWaitForMore(true).build());
+      Throwable t = error.get(10, TimeUnit.SECONDS);
+      assertNotNull(t, "The mailbox server at " + host + ":" + port + " 
accepted a stream for another instance");
+      assertMisroutedTo(t, receiverInstanceId);
+    } finally {
+      channel.shutdownNow();
+    }
+  }
+
+  /// Asserts that `t` is the rejection of a request by the instance with the 
given id.
+  private static void assertMisroutedTo(Throwable t, String 
receiverInstanceId) {
+    String misroutedTo = ReceiverIdentity.getMisroutedTo(t);
+    assertNotNull(misroutedTo, "Not a rejection of a request meant for another 
instance: " + t);
+    assertEquals(misroutedTo.split(" ")[0], receiverInstanceId);

Review Comment:
   Minor: `misroutedTo.split(" ")[0]` relies on the exact `"<id> at <addr>"` 
shape of `ReceiverIdentity.toString()`. If that description format ever changes 
this assertion breaks confusingly. A `startsWith(receiverInstanceId + " ")` (or 
asserting it contains the id) would be a touch more robust.



##########
pinot-query-planner/src/main/java/org/apache/pinot/query/routing/QueryServerInstance.java:
##########
@@ -60,6 +75,11 @@ public int getQueryServicePort() {
     return _queryServicePort;
   }
 
+  /// Returns the hash of the instance id. See [#hashInstanceId(String)].
+  public long getInstanceIdHash() {
+    return _instanceIdHash;

Review Comment:
   Note that `equals`/`hashCode` (just below) deliberately exclude this hash 
and the instance id, so two instances sharing hostname:port collapse to one key 
— e.g. in `createMailboxInfo`'s `workersByUniqueHostPort` grouping. That's fine 
at plan time (distinct instances have distinct hostnames), but now that the 
identity hash rides along it's a latent trap if a future caller ever keys on 
these objects expecting id-distinctness. A one-line comment noting the equality 
contract is intentionally address-only would help.



##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java:
##########
@@ -408,13 +453,83 @@ public void onCompleted() {
           }
         };
 
+    return openStream(responseObserver);
+  }
+
+  private ClientCallStreamObserver<MailboxContent> 
openStream(StreamObserver<MailboxStatus> responseObserver) {
+    Metadata metadata = new Metadata();
+    metadata.put(ChannelUtils.MAILBOX_ID_METADATA_KEY, _id);
+    ReceiverIdentity.addTargetInstanceIdHash(metadata, 
_receiverInstanceIdHash);
     return (ClientCallStreamObserver<MailboxContent>) PinotMailboxGrpc.newStub(
             _channelManager.getChannel(_hostname, _port))
         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata))
         .withDeadlineAfter(_deadlineMs - System.currentTimeMillis(), 
TimeUnit.MILLISECONDS)
         .open(responseObserver);
   }
 
+  /// Handles the rejection of the stream by an instance that is not the 
receiver the plan meant. Runs on a gRPC
+  /// thread, after [#_misroutedError] is set.
+  ///
+  /// The channel to [#_hostname] leads to another instance, typically because 
that instance took over the IP address
+  /// the hostname used to resolve to and listens on the same port. Whatever 
this mailbox sent went nowhere. So:
+  ///
+  /// - the channel is refreshed, so that the next stream to this address 
resolves the hostname again;
+  /// - every later data send fails, which fails this stage and, through its 
error EOS, the query;
+  /// - the intended receiver is told through a new stream, so that it fails 
now instead of waiting for data until the
+  ///   query times out. This is best effort, see [#rerouteError].

Review Comment:
   The best-effort reroute here is nicely documented, but the "DNS still 
returns the stale address so the reroute stream is itself rejected" branch (the 
`onError`/`isMisrouted` path in `rerouteError`) looks untested — every test 
remaps DNS to the new A before the reroute runs, so it always succeeds. Since 
that's the whole reason this path is best-effort, a test that leaves DNS 
pointing at B would be worth adding to lock in the second `refreshChannel` 
behavior.



##########
pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java:
##########
@@ -2875,6 +2875,20 @@ public static class MultiStageQueryRunner {
         "pinot.query.runner.mailbox.server.permit.keep.alive.without.calls";
     public static final boolean 
DEFAULT_OF_MAILBOX_SERVER_PERMIT_KEEP_ALIVE_WITHOUT_CALLS = false;
 
+    /// Whether the mailbox server and the query server of an instance reject 
a request addressed to another instance.
+    ///
+    /// Senders identify the instance they mean in the request headers. A 
request reaches the wrong instance when the
+    /// sender's connection is stale: for example after two server pods 
restart together and take each other's IP
+    /// addresses, while every server listens on the same port. If the wrong 
instance accepts the request, the intended
+    /// one waits for data until the query times out, and the wrong one runs 
the stage plan, or keeps the block in a
+    /// mailbox that nothing reads. When the wrong instance rejects the 
request, the sender resets the connection and
+    /// the query fails with a clear error, in most cases at once rather than 
at its deadline.
+    ///
+    /// Requests without the headers, as sent by older versions, are always 
accepted. The value is read when the
+    /// instance starts, on brokers and servers.
+    public static final String KEY_OF_REJECT_MISROUTED_REQUESTS = 
"pinot.query.runner.reject.misrouted.requests";
+    public static final boolean DEFAULT_REJECT_MISROUTED_REQUESTS = true;

Review Comment:
   This is a new behavior-changing check defaulting to ON (C1.6 usually wants 
new features OFF). It's backward-compatible since older senders are accepted, 
but a true-by-default check that can *reject* traffic is exactly where a subtle 
id mismatch (e.g. server's `CONFIG_OF_INSTANCE_ID` diverging from the Helix 
instance name the broker routes by) would silently fail queries instead of 
hanging them. Worth confirming in the description why ON-by-default is safe for 
existing clusters, and whether a release-notes callout is warranted given it's 
a default-on safety behavior.



##########
pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineIntegrationTest.java:
##########
@@ -295,6 +311,94 @@ public void 
testAllLeafStagesEmptyBrokerResponsesWithPhysicalOptimizer()
         List.of(), "LONG", "LONG");
   }
 
+  /// The mailbox and query servers of a running cluster reject requests that 
say they are meant for another instance.
+  /// Shows that the instance ids reach the receivers of a real server and 
broker, and that the check is on by default.
+  /// The other tests of this class show that the requests of real plans, 
which carry the ids, are accepted.
+  @Test
+  public void testReceiversRejectRequestsForAnotherInstance()
+      throws Exception {
+    String otherInstanceId = "Server_other-host_8098";
+    String serverInstanceId = _serverStarters.get(0).getInstanceId();
+    InstanceConfig serverConfig = 
_helixAdmin.getInstanceConfig(getHelixClusterName(), serverInstanceId);
+    // Every instance of the test cluster runs on this host
+    String host = "localhost";
+
+    // Stage plan request (the cancel of an unknown query, which is harmless 
when accepted)
+    int queryServerPort = serverConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_SERVICE_PORT_KEY,
 -1);
+    ManagedChannel queryServerChannel = ManagedChannelBuilder.forAddress(host, 
queryServerPort).usePlaintext().build();
+    try {
+      Metadata headers = new Metadata();
+      ReceiverIdentity.addTargetInstanceIdHash(headers, 
QueryServerInstance.hashInstanceId(otherInstanceId));
+      StatusRuntimeException e = expectThrows(StatusRuntimeException.class, () 
-> cancel(queryServerChannel, headers));
+      assertMisroutedTo(e, serverInstanceId);
+
+      Metadata ownHeaders = new Metadata();
+      ReceiverIdentity.addTargetInstanceIdHash(ownHeaders, 
QueryServerInstance.hashInstanceId(serverInstanceId));
+      cancel(queryServerChannel, ownHeaders);
+    } finally {
+      queryServerChannel.shutdownNow();
+    }
+
+    // Mailbox streams, to the server and to the broker
+    int serverMailboxPort = serverConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY,
 -1);
+    assertMailboxStreamRejected(host, serverMailboxPort, otherInstanceId, 
serverInstanceId);
+    String brokerInstanceId = _brokerStarters.get(0).getInstanceId();
+    InstanceConfig brokerConfig = 
_helixAdmin.getInstanceConfig(getHelixClusterName(), brokerInstanceId);
+    int brokerMailboxPort = brokerConfig.getRecord()
+        
.getIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY,
 -1);
+    assertMailboxStreamRejected(host, brokerMailboxPort, otherInstanceId, 
brokerInstanceId);
+  }
+
+  private static void cancel(ManagedChannel channel, Metadata headers) {
+    
PinotQueryWorkerGrpc.newBlockingStub(channel).withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
+        .withDeadlineAfter(10, 
TimeUnit.SECONDS).cancel(Worker.CancelRequest.newBuilder().setRequestId(-1).build());
+  }
+
+  private static void assertMailboxStreamRejected(String host, int port, 
String targetInstanceId,
+      String receiverInstanceId)
+      throws Exception {
+    ManagedChannel channel = ManagedChannelBuilder.forAddress(host, 
port).usePlaintext().build();
+    try {
+      Metadata headers = new Metadata();
+      headers.put(ChannelUtils.MAILBOX_ID_METADATA_KEY, "-1|1|0|0|0");
+      ReceiverIdentity.addTargetInstanceIdHash(headers, 
QueryServerInstance.hashInstanceId(targetInstanceId));
+      CompletableFuture<Throwable> error = new CompletableFuture<>();
+      StreamObserver<Mailbox.MailboxContent> stream = 
PinotMailboxGrpc.newStub(channel)
+          .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
+          .withDeadlineAfter(10, TimeUnit.SECONDS)
+          .open(new StreamObserver<>() {
+            @Override
+            public void onNext(Mailbox.MailboxStatus value) {
+            }
+
+            @Override
+            public void onError(Throwable t) {
+              error.complete(t);
+            }
+
+            @Override
+            public void onCompleted() {
+              error.complete(null);
+            }
+          });
+      
stream.onNext(Mailbox.MailboxContent.newBuilder().setMailboxId("-1|1|0|0|0").setWaitForMore(true).build());
+      Throwable t = error.get(10, TimeUnit.SECONDS);
+      assertNotNull(t, "The mailbox server at " + host + ":" + port + " 
accepted a stream for another instance");
+      assertMisroutedTo(t, receiverInstanceId);
+    } finally {
+      channel.shutdownNow();
+    }
+  }
+
+  /// Asserts that `t` is the rejection of a request by the instance with the 
given id.
+  private static void assertMisroutedTo(Throwable t, String 
receiverInstanceId) {
+    String misroutedTo = ReceiverIdentity.getMisroutedTo(t);
+    assertNotNull(misroutedTo, "Not a rejection of a request meant for another 
instance: " + t);
+    assertEquals(misroutedTo.split(" ")[0], receiverInstanceId);

Review Comment:
   Minor: `misroutedTo.split(" ")[0]` relies on the exact `"<id> at <addr>"` 
shape of `ReceiverIdentity.toString()`. If that description format ever changes 
this assertion breaks confusingly. A `startsWith(receiverInstanceId + " ")` (or 
asserting it contains the id) would be a touch more robust.



-- 
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