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


##########
pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java:
##########
@@ -478,14 +481,25 @@ public byte[] toBytes()
     // NOTE: We ignore metadata serialization time in 
"responseSerializationCpuTimeNs" as it's negligible while
     // considering it will bring a lot code complexity.
     byte[] metadataBytes = serializeMetadata();
-    if (metadataBytes.length != metadataLength) {
-      throw new IllegalStateException(
-          "Metadata length changed during serialization from " + 
metadataLength + " to " + metadataBytes.length);
+    // Readers parse the whole table as one buffer, so the total must fit in 
an int as well.
+    long totalLength = sectionsLength + Integer.BYTES + metadataBytes.length;
+    if (totalLength > Integer.MAX_VALUE) {
+      throw new IllegalStateException("Serialized data table size " + 
totalLength + " exceeds the 2 GB limit");
     }
-    byteBuffer.putInt(metadataBytes.length);
-    byteBuffer.put(metadataBytes);
-    assert !byteBuffer.hasRemaining();
-    return bytes;
+    ByteBuffer metadata = ByteBuffer.allocate(Integer.BYTES + 
metadataBytes.length);
+    metadata.putInt(metadataBytes.length);
+    metadata.put(metadataBytes);
+
+    CompoundDataBuffer.Builder builder = new 
CompoundDataBuffer.Builder(ByteOrder.BIG_ENDIAN, false);

Review Comment:
   Good call building with `owner=false` here — it means closing the returned 
buffer won't release the table's own fixed/variable sections, so the table 
keeps ownership. Combined with the heap-backed builder pages, there's nothing 
to release and GC handles the lifetime once Netty/gRPC are done. This is the 
key bit that makes sharing the buffers safe.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/QueryScheduler.java:
##########
@@ -74,6 +78,19 @@ public QueryScheduler(PinotConfiguration config, String 
instanceId, QueryExecuto
     _threadAccountant = threadAccountant;
     _latestQueryTime = latestQueryTime;
     _resourceManager = resourceManager;
+    // submit() and submitResponse() call each other by default. Fail here 
instead of with a StackOverflowError on the
+    // first query.
+    Preconditions.checkState(overrides("submit") || 
overrides("submitResponse"),
+        "%s must override submitResponse(ServerQueryRequest) or the deprecated 
submit(ServerQueryRequest)",
+        getClass().getName());
+  }
+
+  private boolean overrides(String methodName) {
+    try {
+      return getClass().getMethod(methodName, 
ServerQueryRequest.class).getDeclaringClass() != QueryScheduler.class;

Review Comment:
   Minor: this check now turns "subclass overrides neither method" from a 
compile error (old abstract `submit`) into a constructor-time 
`IllegalStateException`. That's a reasonable trade-off and you call it out in 
the PR. `getMethod` also only sees public methods, so a subclass that declared 
`submit`/`submitResponse` as protected wouldn't be detected — not possible here 
since both are public, just noting the assumption.



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtils.java:
##########
@@ -47,6 +52,19 @@ public static Server.ServerResponse 
getNonStreamingResponse(DataTable dataTable)
   private static Server.ServerResponse getResponse(DataTable dataTable, String 
responseType)
       throws IOException {
     return 
Server.ServerResponse.newBuilder().putMetadata(Response.MetadataKeys.RESPONSE_TYPE,
 responseType)
-        .setPayload(ByteString.copyFrom(dataTable.toBytes())).build();
+        .setPayload(toByteString(dataTable.toDataBuffer())).build();
+  }
+
+  /// Wraps the buffers of a serialized data table in one [ByteString] without 
copying them. The buffers must not change
+  /// after this call, which holds for a serialized data table.
+  private static ByteString toByteString(DataBuffer dataBuffer) {
+    List<ByteBuffer> buffers = new ArrayList<>();
+    dataBuffer.appendAsByteBuffers(buffers);
+    List<ByteString> byteStrings = new ArrayList<>(buffers.size());
+    for (ByteBuffer buffer : buffers) {
+      byteStrings.add(UnsafeByteOperations.unsafeWrap(buffer));

Review Comment:
   `unsafeWrap` is safe here only because these buffers are never mutated after 
the table is built, which the comment above correctly notes. Worth keeping in 
mind that gRPC may re-serialize the payload on a retry, so the DataTable's 
buffers must stay immutable for the whole lifetime of this `ByteString` — which 
holds for a serialized DataTable. No change needed, just flagging the contract.



##########
pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java:
##########
@@ -310,16 +313,21 @@ private void sendErrorResponse(ChannelHandlerContext ctx, 
long requestId, String
   }
 
   /// Send a response (either query results or exception) back to broker as 
response to the query request.
+  ///
+  /// The response buffers are wrapped in one composite Netty buffer without 
copying them.
   private void sendResponse(ChannelHandlerContext ctx, long requestId, String 
tableNameWithType,
-      long queryArrivalTimeMs, byte[] serializedDataTable) {
+      long queryArrivalTimeMs, DataBuffer response) {
     long sendResponseStartTimeMs = System.currentTimeMillis();
     int queryProcessingTimeMs = (int) (sendResponseStartTimeMs - 
queryArrivalTimeMs);
-    
ctx.writeAndFlush(Unpooled.wrappedBuffer(serializedDataTable)).addListener(f -> 
{
+    long responseSizeBytes = response.size();
+    List<ByteBuffer> responseBuffers = new ArrayList<>();
+    response.appendAsByteBuffers(responseBuffers);
+    ctx.writeAndFlush(Unpooled.wrappedBuffer(responseBuffers.toArray(new 
ByteBuffer[0]))).addListener(f -> {

Review Comment:
   The composite `Unpooled.wrappedBuffer(ByteBuffer[])` avoids the heap copy, 
nice. Netty still copies heap→direct before the socket write (as the PR notes), 
so the win is removing the extra full-size `byte[]`, not the socket-level copy. 
For a zero-length response this would wrap an empty array, but a serialized 
DataTable always has a header + metadata so that can't happen — fine as-is.



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