This is an automated email from the ASF dual-hosted git repository.
gortiz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 3f6fef49e05 Send v1 DataTable responses to Netty and gRPC as pages
instead of one byte[] (#19943)
3f6fef49e05 is described below
commit 3f6fef49e05dec8414ebecca0373ce0cea14cef6
Author: Gonzalo Ortiz Jaureguizar <[email protected]>
AuthorDate: Thu Oct 29 11:09:32 2026 +0100
Send v1 DataTable responses to Netty and gRPC as pages instead of one
byte[] (#19943)
DataTable.toDataBuffer() returns the serialized table as a DataBuffer over
the builder's existing pages, so the v1 server response path no longer copies
the response into one full-size byte[] (a G1 humongous object for large
responses). The wire format does not change.
- QueryScheduler gets submitResponse() and DataBuffer-returning helpers.
submit() and the byte[] helpers stay as deprecated defaults; a subclass must
override one of submit()/submitResponse(), checked in the constructor.
- FCFSQueryScheduler, PriorityScheduler, BinaryWorkloadScheduler and
WorkloadScheduler override submitResponse(); SchedulerQueryContext holds a
SettableFuture<DataBuffer>.
- InstanceRequestHandler sends the pages with
Unpooled.wrappedBuffer(ByteBuffer...).
- StreamingResponseUtils wraps the pages with
UnsafeByteOperations.unsafeWrap into a ByteString rope instead of
ByteString.copyFrom(byte[]).
- PageCacheWarmupServerQueryExecutor calls submitResponse().
---
.../apache/pinot/common/datatable/DataTable.java | 11 ++
.../pinot/common/datatable/DataTableImplV4.java | 89 ++++++++-------
.../operator/streaming/StreamingResponseUtils.java | 20 +++-
.../query/scheduler/BinaryWorkloadScheduler.java | 15 +--
.../core/query/scheduler/PriorityScheduler.java | 11 +-
.../pinot/core/query/scheduler/QueryScheduler.java | 119 ++++++++++++++++++---
.../query/scheduler/SchedulerQueryContext.java | 7 +-
.../core/query/scheduler/WorkloadScheduler.java | 9 +-
.../query/scheduler/fcfs/FCFSQueryScheduler.java | 7 +-
.../core/transport/InstanceRequestHandler.java | 34 +++---
.../core/common/datatable/DataTableSerDeTest.java | 23 +++-
.../streaming/StreamingResponseUtilsTest.java | 72 +++++++++++++
.../scheduler/QuerySchedulerResponseApiTest.java | 98 +++++++++++++++++
.../core/transport/InstanceRequestHandlerTest.java | 6 +-
.../pinot/core/transport/QueryRoutingTest.java | 62 ++++++++++-
.../warmup/PageCacheWarmupServerQueryExecutor.java | 5 +-
.../PageCacheWarmupServerQueryExecutorTest.java | 7 +-
17 files changed, 491 insertions(+), 104 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
index 14d916154c1..a329e7d2273 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
@@ -26,6 +26,8 @@ import java.util.Map;
import javax.annotation.Nullable;
import org.apache.pinot.common.CustomObject;
import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.spi.exception.QueryErrorCode;
import org.apache.pinot.spi.utils.ByteArray;
import org.roaringbitmap.RoaringBitmap;
@@ -47,6 +49,15 @@ public interface DataTable {
byte[] toBytes()
throws IOException;
+ /// Serializes the table into the same bytes as [#toBytes()], but possibly
split into several buffers. An
+ /// implementation can return its sections without copying them, so a large
table does not need one large array.
+ ///
+ /// The default implementation wraps [#toBytes()].
+ default DataBuffer toDataBuffer()
+ throws IOException {
+ return PinotByteBuffer.wrap(toBytes());
+ }
+
Map<String, String> getMetadata();
DataSchema getDataSchema();
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
index 8b0b58cd4ba..a058d95addb 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
@@ -26,6 +26,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
@@ -34,6 +35,7 @@ import org.apache.pinot.common.datablock.DataBlockUtils;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.HashUtil;
import org.apache.pinot.common.utils.RoaringBitmapUtils;
+import org.apache.pinot.segment.spi.memory.CompoundDataBuffer;
import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.spi.accounting.ThreadResourceSnapshot;
@@ -433,43 +435,44 @@ public class DataTableImplV4 implements DataTable {
return _errCodeToExceptionMap;
}
- /// Serializes the table into one array of the exact final size, so the
fixed and variable size sections are copied
- /// once and no buffer grows by doubling.
+ /// Serializes the table into one array of the exact final size. Prefer
[#toDataBuffer()], which does not copy the
+ /// fixed and variable size sections.
@Override
public byte[] toBytes()
throws IOException {
- ThreadResourceSnapshot resourceSnapshot = new ThreadResourceSnapshot();
+ DataBuffer dataBuffer = toDataBuffer();
+ byte[] bytes = new byte[Math.toIntExact(dataBuffer.size())];
+ dataBuffer.copyTo(0, bytes);
+ return bytes;
+ }
- // Add table serialization time and memory metadata when the corresponding
measurement is enabled.
- // When CPU time/memory usage is not collectable, we omit these values
from the metadata.
- // The entries are reserved before the array is sized and filled in after
the sections are copied. Both are LONG
- // keys, whose encoded length does not depend on the value, so the
metadata length does not change.
- boolean measureCpuTime =
ThreadResourceUsageProvider.isThreadCpuTimeMeasurementEnabled();
- boolean measureAllocatedBytes =
ThreadResourceUsageProvider.isThreadMemoryMeasurementEnabled();
- if (measureCpuTime) {
- _metadata.put(MetadataKey.RESPONSE_SER_CPU_TIME_NS.getName(), "0");
- }
- if (measureAllocatedBytes) {
- _metadata.put(MetadataKey.RESPONSE_SER_MEM_ALLOCATED_BYTES.getName(),
"0");
- }
+ /// Serializes the table without copying the fixed and variable size
sections. The header, the small sections and
+ /// the metadata are new arrays, and the two data sections are the buffers
of this table. On a table built in this
+ /// process, they are the builder's pages, so no buffer is larger than one
page plus these small arrays.
+ @Override
+ public DataBuffer toDataBuffer()
+ throws IOException {
+ ThreadResourceSnapshot resourceSnapshot = new ThreadResourceSnapshot();
byte[] exceptionsBytes = serializeExceptions();
byte[] dictionaryBytes = _stringDictionary != null ?
serializeStringDictionary() : null;
byte[] dataSchemaBytes = _dataSchema != null ? _dataSchema.toBytes() :
null;
- int metadataLength = serializeMetadata().length;
- long totalLength = HEADER_SIZE + exceptionsBytes.length +
lengthOf(dictionaryBytes) + lengthOf(dataSchemaBytes)
- + sizeOf(_fixedSizeData) + sizeOf(_variableSizeData) + Integer.BYTES +
metadataLength;
- if (totalLength > Integer.MAX_VALUE) {
- throw new IllegalStateException("Serialized data table size " +
totalLength + " exceeds the 2 GB limit");
+ // The header stores every section offset as an int.
+ long sectionsLength = HEADER_SIZE + exceptionsBytes.length +
lengthOf(dictionaryBytes) + lengthOf(dataSchemaBytes)
+ + sizeOf(_fixedSizeData) + sizeOf(_variableSizeData);
+ if (sectionsLength > Integer.MAX_VALUE) {
+ throw new IllegalStateException("Serialized data table size " +
sectionsLength + " exceeds the 2 GB limit");
}
- byte[] bytes = new byte[(int) totalLength];
- ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
- writeLeadingSections(byteBuffer, exceptionsBytes, dictionaryBytes,
dataSchemaBytes);
+ ByteBuffer leadingSections = ByteBuffer.allocate(
+ HEADER_SIZE + exceptionsBytes.length + lengthOf(dictionaryBytes) +
lengthOf(dataSchemaBytes));
+ writeLeadingSections(leadingSections, exceptionsBytes, dictionaryBytes,
dataSchemaBytes);
- if (measureCpuTime) {
+ // Add table serialization time and memory metadata when the corresponding
measurement is enabled.
+ // When CPU time/memory usage is not collectable, we omit these values
from the metadata.
+ if (ThreadResourceUsageProvider.isThreadCpuTimeMeasurementEnabled()) {
_metadata.put(MetadataKey.RESPONSE_SER_CPU_TIME_NS.getName(),
String.valueOf(resourceSnapshot.getCpuTimeNs()));
}
- if (measureAllocatedBytes) {
+ if (ThreadResourceUsageProvider.isThreadMemoryMeasurementEnabled()) {
_metadata.put(MetadataKey.RESPONSE_SER_MEM_ALLOCATED_BYTES.getName(),
String.valueOf(resourceSnapshot.getAllocatedBytes()));
}
@@ -478,14 +481,25 @@ public class DataTableImplV4 implements DataTable {
// 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);
+ builder.addBuffer(leadingSections.flip());
+ if (_fixedSizeData != null) {
+ builder.addBuffer(_fixedSizeData);
+ }
+ if (_variableSizeData != null) {
+ builder.addBuffer(_variableSizeData);
+ }
+ builder.addBuffer(metadata.flip());
+ return builder.build();
}
private static int lengthOf(@Nullable byte[] bytes) {
@@ -496,6 +510,8 @@ public class DataTableImplV4 implements DataTable {
return dataBuffer != null ? dataBuffer.size() : 0;
}
+ /// Writes the header and the exceptions, dictionary and data schema
sections. The fixed and variable size sections
+ /// follow them in the serialized table.
private void writeLeadingSections(ByteBuffer byteBuffer, byte[]
exceptionsBytes, @Nullable byte[] dictionaryBytes,
@Nullable byte[] dataSchemaBytes) {
int fixedSizeDataLength = (int) sizeOf(_fixedSizeData);
@@ -538,15 +554,6 @@ public class DataTableImplV4 implements DataTable {
if (dataSchemaBytes != null) {
byteBuffer.put(dataSchemaBytes);
}
- copySection(_fixedSizeData, byteBuffer, fixedSizeDataLength);
- copySection(_variableSizeData, byteBuffer, variableSizeDataLength);
- }
-
- private static void copySection(@Nullable DataBuffer section, ByteBuffer
byteBuffer, int length) {
- if (length > 0) {
- section.copyTo(0, byteBuffer.array(), byteBuffer.position(), length);
- byteBuffer.position(byteBuffer.position() + length);
- }
}
/// Serialize metadata section to bytes.
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtils.java
index bdf5464aaaf..c4f453144c9 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtils.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtils.java
@@ -19,9 +19,14 @@
package org.apache.pinot.core.operator.streaming;
import com.google.protobuf.ByteString;
+import com.google.protobuf.UnsafeByteOperations;
import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
import org.apache.pinot.common.datatable.DataTable;
import org.apache.pinot.common.proto.Server;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.utils.CommonConstants.Query.Response;
@@ -47,6 +52,19 @@ public class StreamingResponseUtils {
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));
+ }
+ // Despite its name, copyFrom(Iterable) concatenates the byte strings into
a balanced rope without copying them.
+ return ByteString.copyFrom(byteStrings);
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/BinaryWorkloadScheduler.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/BinaryWorkloadScheduler.java
index fdae2557ab5..6d78893daa7 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/BinaryWorkloadScheduler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/BinaryWorkloadScheduler.java
@@ -33,6 +33,7 @@ import org.apache.pinot.core.query.executor.QueryExecutor;
import org.apache.pinot.core.query.request.ServerQueryRequest;
import
org.apache.pinot.core.query.scheduler.resources.BinaryWorkloadResourceManager;
import org.apache.pinot.core.query.scheduler.resources.QueryExecutorService;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.slf4j.Logger;
@@ -84,15 +85,15 @@ public class BinaryWorkloadScheduler extends QueryScheduler
{
}
@Override
- public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
if (!_isRunning) {
- return shuttingDown(queryRequest);
+ return shuttingDownResponse(queryRequest);
}
queryRequest.getTimerContext().startNewPhaseTimer(ServerQueryPhase.SCHEDULER_WAIT);
if
(!QueryOptionsUtils.isSecondaryWorkload(queryRequest.getQueryContext().getQueryOptions()))
{
QueryExecutorService executorService =
_resourceManager.getExecutorService(queryRequest, null);
- ListenableFutureTask<byte[]> queryTask =
createQueryFutureTask(queryRequest, executorService);
+ ListenableFutureTask<DataBuffer> queryTask =
createResponseFutureTask(queryRequest, executorService);
_resourceManager.getQueryRunners().submit(queryTask);
return queryTask;
}
@@ -106,13 +107,13 @@ public class BinaryWorkloadScheduler extends
QueryScheduler {
} catch (OutOfCapacityException e) {
LOGGER.error("Out of capacity for query {} table {}, message: {}",
queryRequest.getRequestId(),
queryRequest.getTableNameWithType(), e.getMessage());
- return outOfCapacity(queryRequest);
+ return outOfCapacityResponse(queryRequest);
} catch (Exception e) {
// We should not throw any other exception other than
OutOfCapacityException. Signal that there's an issue with
// the scheduler if any other exception is thrown.
LOGGER.error("Internal error for query {} table {}, message {}",
queryRequest.getRequestId(),
queryRequest.getTableNameWithType(), e.getMessage());
- return shuttingDown(queryRequest);
+ return shuttingDownResponse(queryRequest);
}
return schedQueryContext.getResultFuture();
}
@@ -151,7 +152,7 @@ public class BinaryWorkloadScheduler extends QueryScheduler
{
ServerQueryRequest queryRequest = request.getQueryRequest();
SchedulerGroup schedulerGroup = request.getSchedulerGroup();
QueryExecutorService executorService =
_resourceManager.getExecutorService(queryRequest, schedulerGroup);
- ListenableFutureTask<byte[]> queryFutureTask =
createQueryFutureTask(queryRequest, executorService);
+ ListenableFutureTask<DataBuffer> queryFutureTask =
createResponseFutureTask(queryRequest, executorService);
queryFutureTask.addListener(new Runnable() {
@Override
public void run() {
@@ -209,7 +210,7 @@ public class BinaryWorkloadScheduler extends QueryScheduler
{
synchronized private void failAllPendingQueries() {
List<SchedulerQueryContext> pending = _secondaryQueryQ.drain();
for (SchedulerQueryContext queryContext : pending) {
- ListenableFuture<byte[]> serverShuttingDown =
shuttingDown(queryContext.getQueryRequest());
+ ListenableFuture<DataBuffer> serverShuttingDown =
shuttingDownResponse(queryContext.getQueryRequest());
queryContext.setResultFuture(serverShuttingDown);
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/PriorityScheduler.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/PriorityScheduler.java
index b1e0a8e5d70..fa155afe6eb 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/PriorityScheduler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/PriorityScheduler.java
@@ -30,6 +30,7 @@ import org.apache.pinot.core.query.executor.QueryExecutor;
import org.apache.pinot.core.query.request.ServerQueryRequest;
import org.apache.pinot.core.query.scheduler.resources.QueryExecutorService;
import org.apache.pinot.core.query.scheduler.resources.ResourceManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.slf4j.Logger;
@@ -82,9 +83,9 @@ public abstract class PriorityScheduler extends
QueryScheduler {
}
@Override
- public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
if (!_isRunning) {
- return shuttingDown(queryRequest);
+ return shuttingDownResponse(queryRequest);
}
queryRequest.getTimerContext().startNewPhaseTimer(ServerQueryPhase.SCHEDULER_WAIT);
final SchedulerQueryContext schedQueryContext = new
SchedulerQueryContext(queryRequest);
@@ -92,7 +93,7 @@ public abstract class PriorityScheduler extends
QueryScheduler {
_queryQueue.put(schedQueryContext);
} catch (OutOfCapacityException e) {
LOGGER.error("Out of capacity for table {}, message: {}",
queryRequest.getTableNameWithType(), e.getMessage());
- return outOfCapacity(queryRequest);
+ return outOfCapacityResponse(queryRequest);
}
return schedQueryContext.getResultFuture();
}
@@ -122,7 +123,7 @@ public abstract class PriorityScheduler extends
QueryScheduler {
ServerQueryRequest queryRequest = request.getQueryRequest();
SchedulerGroup schedulerGroup = request.getSchedulerGroup();
QueryExecutorService executorService =
_resourceManager.getExecutorService(queryRequest, schedulerGroup);
- ListenableFutureTask<byte[]> queryFutureTask =
createQueryFutureTask(queryRequest, executorService);
+ ListenableFutureTask<DataBuffer> queryFutureTask =
createResponseFutureTask(queryRequest, executorService);
queryFutureTask.addListener(new Runnable() {
@Override
public void run() {
@@ -170,7 +171,7 @@ public abstract class PriorityScheduler extends
QueryScheduler {
synchronized private void failAllPendingQueries() {
List<SchedulerQueryContext> pending = _queryQueue.drain();
for (SchedulerQueryContext queryContext : pending) {
-
queryContext.setResultFuture(shuttingDown(queryContext.getQueryRequest()));
+
queryContext.setResultFuture(shuttingDownResponse(queryContext.getQueryRequest()));
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/QueryScheduler.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/QueryScheduler.java
index 8b39f257c99..3e8bf2c8c87 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/QueryScheduler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/QueryScheduler.java
@@ -18,9 +18,11 @@
*/
package org.apache.pinot.core.query.scheduler;
+import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.LongAccumulator;
@@ -37,6 +39,8 @@ import org.apache.pinot.core.query.logger.ServerQueryLogger;
import org.apache.pinot.core.query.request.ServerQueryRequest;
import org.apache.pinot.core.query.request.context.TimerContext;
import org.apache.pinot.core.query.scheduler.resources.ResourceManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.apache.pinot.spi.exception.QueryErrorCode;
@@ -74,6 +78,19 @@ public abstract class QueryScheduler {
_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;
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(e);
+ }
}
public ResourceManager getResourceManager() {
@@ -84,7 +101,29 @@ public abstract class QueryScheduler {
/// @param queryRequest query to schedule for execution
/// @return Listenable future for query result representing serialized
response. It is possible that the
/// future may return immediately or be scheduled for execution at a
later time.
- public abstract ListenableFuture<byte[]> submit(ServerQueryRequest
queryRequest);
+ /// @deprecated Use [#submitResponse(ServerQueryRequest)], which does not
copy the response into one array. The
+ /// default implementation copies the result of
[#submitResponse(ServerQueryRequest)]. Subclasses must override
+ /// at least one of the two methods. The server calls
[#submitResponse(ServerQueryRequest)], so a subclass of a
+ /// scheduler that already overrides
[#submitResponse(ServerQueryRequest)] (all the schedulers in this package)
+ /// must override that method too: an override of this method alone is
not called.
+ @Deprecated
+ public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ return Futures.transform(submitResponse(queryRequest),
QueryScheduler::toByteArray, MoreExecutors.directExecutor());
+ }
+
+ /// Submit a query for execution. The query will be scheduled for execution
as per the scheduling algorithm.
+ ///
+ /// The serialized response can be split into several buffers, so a large
response does not need one large array.
+ ///
+ /// The default implementation wraps the result of the deprecated
[#submit(ServerQueryRequest)], so that subclasses
+ /// that only override that method keep working. Subclasses must override at
least one of the two methods.
+ ///
+ /// @param queryRequest query to schedule for execution
+ /// @return Listenable future for the serialized response. It is possible
that the future may return immediately or
+ /// be scheduled for execution at a later time.
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
+ return Futures.transform(submit(queryRequest), QueryScheduler::wrap,
MoreExecutors.directExecutor());
+ }
/// Query scheduler name for logging
public abstract String name();
@@ -107,17 +146,34 @@ public abstract class QueryScheduler {
/// internally used to parallelize query processing.
/// @return Future task that can be scheduled for execution on an
ExecutorService. Ideally, this future
/// should be executed on a different executor service than
`executorService` to avoid deadlock.
+ protected ListenableFutureTask<DataBuffer>
createResponseFutureTask(ServerQueryRequest queryRequest,
+ ExecutorService executorService) {
+ return ListenableFutureTask.create(() ->
processQueryAndSerializeResponse(queryRequest, executorService));
+ }
+
+ /// @deprecated Use [#createResponseFutureTask(ServerQueryRequest,
ExecutorService)], which does not copy the
+ /// response into one array.
+ @Deprecated
protected ListenableFutureTask<byte[]>
createQueryFutureTask(ServerQueryRequest queryRequest,
ExecutorService executorService) {
return ListenableFutureTask.create(() ->
processQueryAndSerialize(queryRequest, executorService));
}
+ /// @deprecated Use [#processQueryAndSerializeResponse(ServerQueryRequest,
ExecutorService)], which does not copy
+ /// the response into one array.
+ @Deprecated
+ @Nullable
+ protected byte[] processQueryAndSerialize(ServerQueryRequest queryRequest,
ExecutorService executorService) {
+ return toByteArray(processQueryAndSerializeResponse(queryRequest,
executorService));
+ }
+
/// Process query and serialize response
/// @param queryRequest incoming query request
/// @param executorService Executor service to use for parallelizing query
processing
/// @return serialized query response
@Nullable
- protected byte[] processQueryAndSerialize(ServerQueryRequest queryRequest,
ExecutorService executorService) {
+ protected DataBuffer processQueryAndSerializeResponse(ServerQueryRequest
queryRequest,
+ ExecutorService executorService) {
QueryExecutionContext executionContext =
queryRequest.toExecutionContext(_instanceId);
_latestQueryTime.accumulate(executionContext.getStartTimeMs());
try (QueryThreadContext ignore = QueryThreadContext.open(executionContext,
_threadAccountant)) {
@@ -141,7 +197,7 @@ public abstract class QueryScheduler {
responseMetadata.put(MetadataKey.REQUEST_ID.getName(),
Long.toString(requestId));
responseMetadata.put(MetadataKey.QUERY_ID.getName(), queryId);
responseMetadata.put(MetadataKey.WORKLOAD_NAME.getName(), workloadName);
- byte[] responseBytes = serializeResponse(queryRequest, instanceResponse);
+ DataBuffer response = serializeResponse(queryRequest, instanceResponse);
// Log the statistics
if (_queryLogger != null) {
@@ -150,8 +206,8 @@ public abstract class QueryScheduler {
// TODO: Perform this check sooner during the serialization of DataTable.
Map<String, String> queryOptions =
queryRequest.getQueryContext().getQueryOptions();
- if (responseBytes != null) {
- int responseSizeBytes = responseBytes.length;
+ if (response != null) {
+ long responseSizeBytes = response.size();
String tableNameWithType = queryRequest.getTableNameWithType();
_serverMetrics.addMeteredTableValue(tableNameWithType,
ServerMeter.QUERY_RESPONSE_SIZE, responseSizeBytes);
Long maxResponseSizeBytes =
QueryOptionsUtils.getMaxServerResponseSizeBytes(queryOptions);
@@ -167,29 +223,29 @@ public abstract class QueryScheduler {
instanceResponse.addMetadata(MetadataKey.REQUEST_ID.getName(),
Long.toString(requestId));
instanceResponse.addMetadata(MetadataKey.QUERY_ID.getName(),
queryId);
instanceResponse.addMetadata(MetadataKey.WORKLOAD_NAME.getName(),
workloadName);
- responseBytes = serializeResponse(queryRequest, instanceResponse);
+ response = serializeResponse(queryRequest, instanceResponse);
}
}
- return responseBytes;
+ return response;
}
}
/// Serialize the instance response for query request
/// @param queryRequest Server query request for which response is serialized
/// @param instanceResponse instance response to serialize
- /// @return serialized response bytes
+ /// @return serialized response
@Nullable
- private byte[] serializeResponse(ServerQueryRequest queryRequest,
InstanceResponseBlock instanceResponse) {
+ private DataBuffer serializeResponse(ServerQueryRequest queryRequest,
InstanceResponseBlock instanceResponse) {
TimerContext timerContext = queryRequest.getTimerContext();
TimerContext.Timer responseSerializationTimer =
timerContext.startNewPhaseTimer(ServerQueryPhase.RESPONSE_SERIALIZATION);
long requestId = queryRequest.getRequestId();
String brokerId = queryRequest.getBrokerId();
- byte[] responseBytes = null;
+ DataBuffer response = null;
try {
- responseBytes = instanceResponse.toDataTable().toBytes();
+ response = instanceResponse.toDataTable().toDataBuffer();
} catch (Exception e) {
// First check terminate exception and use it as the response if exists.
We want to return the termination reason
// when query is explicitly terminated.
@@ -214,7 +270,7 @@ public abstract class QueryScheduler {
errorResponse.addMetadata(MetadataKey.QUERY_ID.getName(),
queryRequest.getCid());
String workloadName =
QueryOptionsUtils.getWorkloadName(queryRequest.getQueryContext().getQueryOptions());
errorResponse.addMetadata(MetadataKey.WORKLOAD_NAME.getName(),
workloadName);
- responseBytes = errorResponse.toDataTable().toBytes();
+ response = errorResponse.toDataTable().toDataBuffer();
} catch (Exception e1) {
_serverMetrics.addMeteredGlobalValue(ServerMeter.RESPONSE_SERIALIZATION_EXCEPTIONS,
1);
LOGGER.error("Caught exception while constructing error response for
requestId: {}, brokerId: {}",
@@ -226,12 +282,12 @@ public abstract class QueryScheduler {
timerContext.startNewPhaseTimer(ServerQueryPhase.TOTAL_QUERY_TIME,
timerContext.getQueryArrivalTimeMs())
.stopAndRecord();
- return responseBytes;
+ return response;
}
/// Error response future in case of internal error where query response is
not available. This can happen if the
/// query can not be executed.
- protected ListenableFuture<byte[]> immediateErrorResponse(ServerQueryRequest
queryRequest, QueryErrorCode errorCode) {
+ protected ListenableFuture<DataBuffer> errorResponse(ServerQueryRequest
queryRequest, QueryErrorCode errorCode) {
Map<String, String> queryOptions =
queryRequest.getQueryContext().getQueryOptions();
String workloadName = QueryOptionsUtils.getWorkloadName(queryOptions);
InstanceResponseBlock instanceResponse = new InstanceResponseBlock();
@@ -242,11 +298,46 @@ public abstract class QueryScheduler {
return Futures.immediateFuture(serializeResponse(queryRequest,
instanceResponse));
}
+ protected ListenableFuture<DataBuffer>
shuttingDownResponse(ServerQueryRequest queryRequest) {
+ return errorResponse(queryRequest, QueryErrorCode.SERVER_SHUTTING_DOWN);
+ }
+
+ protected ListenableFuture<DataBuffer>
outOfCapacityResponse(ServerQueryRequest queryRequest) {
+ return errorResponse(queryRequest, QueryErrorCode.SERVER_OUT_OF_CAPACITY);
+ }
+
+ /// @deprecated Use [#errorResponse(ServerQueryRequest, QueryErrorCode)].
+ @Deprecated
+ protected ListenableFuture<byte[]> immediateErrorResponse(ServerQueryRequest
queryRequest, QueryErrorCode errorCode) {
+ return Futures.transform(errorResponse(queryRequest, errorCode),
QueryScheduler::toByteArray,
+ MoreExecutors.directExecutor());
+ }
+
+ /// @deprecated Use [#shuttingDownResponse(ServerQueryRequest)].
+ @Deprecated
protected ListenableFuture<byte[]> shuttingDown(ServerQueryRequest
queryRequest) {
return immediateErrorResponse(queryRequest,
QueryErrorCode.SERVER_SHUTTING_DOWN);
}
+ /// @deprecated Use [#outOfCapacityResponse(ServerQueryRequest)].
+ @Deprecated
protected ListenableFuture<byte[]> outOfCapacity(ServerQueryRequest
queryRequest) {
return immediateErrorResponse(queryRequest,
QueryErrorCode.SERVER_OUT_OF_CAPACITY);
}
+
+ /// Copies a serialized response into one array, for the deprecated methods
that return `byte[]`.
+ @Nullable
+ private static byte[] toByteArray(@Nullable DataBuffer response) {
+ if (response == null) {
+ return null;
+ }
+ byte[] bytes = new byte[Math.toIntExact(response.size())];
+ response.copyTo(0, bytes);
+ return bytes;
+ }
+
+ @Nullable
+ private static DataBuffer wrap(@Nullable byte[] responseBytes) {
+ return responseBytes != null ? PinotByteBuffer.wrap(responseBytes) : null;
+ }
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/SchedulerQueryContext.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/SchedulerQueryContext.java
index c67bd784cb8..104527dabdc 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/SchedulerQueryContext.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/SchedulerQueryContext.java
@@ -23,13 +23,14 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import javax.annotation.Nullable;
import org.apache.pinot.core.query.request.ServerQueryRequest;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
/// Query specific scheduling context
public class SchedulerQueryContext {
private final ServerQueryRequest _queryRequest;
- private final SettableFuture<byte[]> _resultFuture;
+ private final SettableFuture<DataBuffer> _resultFuture;
private SchedulerGroup _schedulerGroup;
public SchedulerQueryContext(ServerQueryRequest queryRequest) {
@@ -42,11 +43,11 @@ public class SchedulerQueryContext {
return _queryRequest;
}
- public SettableFuture<byte[]> getResultFuture() {
+ public SettableFuture<DataBuffer> getResultFuture() {
return _resultFuture;
}
- public void setResultFuture(ListenableFuture<byte[]> f) {
+ public void setResultFuture(ListenableFuture<DataBuffer> f) {
_resultFuture.setFuture(f);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/WorkloadScheduler.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/WorkloadScheduler.java
index e84d6037d24..ea29289f80e 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/WorkloadScheduler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/WorkloadScheduler.java
@@ -28,6 +28,7 @@ import org.apache.pinot.core.query.executor.QueryExecutor;
import org.apache.pinot.core.query.request.ServerQueryRequest;
import org.apache.pinot.core.query.scheduler.resources.QueryExecutorService;
import
org.apache.pinot.core.query.scheduler.resources.UnboundedResourceManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.accounting.WorkloadBudgetManager;
import org.apache.pinot.spi.accounting.WorkloadBudgetManagerFactory;
@@ -65,9 +66,9 @@ public class WorkloadScheduler extends QueryScheduler {
}
@Override
- public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
if (!_isRunning) {
- return shuttingDown(queryRequest);
+ return shuttingDownResponse(queryRequest);
}
boolean isSecondary =
QueryOptionsUtils.isSecondaryWorkload(queryRequest.getQueryContext().getQueryOptions());
@@ -85,11 +86,11 @@ public class WorkloadScheduler extends QueryScheduler {
_serverMetrics.addMeteredValue(workloadName,
ServerMeter.WORKLOAD_BUDGET_EXCEEDED, 1L);
_serverMetrics.addMeteredTableValue(tableName,
ServerMeter.WORKLOAD_BUDGET_EXCEEDED, 1L);
_serverMetrics.addMeteredGlobalValue(ServerMeter.WORKLOAD_BUDGET_EXCEEDED, 1L);
- return immediateErrorResponse(queryRequest,
QueryErrorCode.SERVER_RESOURCE_LIMIT_EXCEEDED);
+ return errorResponse(queryRequest,
QueryErrorCode.SERVER_RESOURCE_LIMIT_EXCEEDED);
}
queryRequest.getTimerContext().startNewPhaseTimer(ServerQueryPhase.SCHEDULER_WAIT);
QueryExecutorService executorService =
_resourceManager.getExecutorService(queryRequest, null);
- ListenableFutureTask<byte[]> queryTask =
createQueryFutureTask(queryRequest, executorService);
+ ListenableFutureTask<DataBuffer> queryTask =
createResponseFutureTask(queryRequest, executorService);
_resourceManager.getQueryRunners().submit(queryTask);
return queryTask;
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/fcfs/FCFSQueryScheduler.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/fcfs/FCFSQueryScheduler.java
index a34887d3bbf..83f615bde06 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/fcfs/FCFSQueryScheduler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/scheduler/fcfs/FCFSQueryScheduler.java
@@ -27,6 +27,7 @@ import org.apache.pinot.core.query.request.ServerQueryRequest;
import org.apache.pinot.core.query.scheduler.QueryScheduler;
import org.apache.pinot.core.query.scheduler.resources.QueryExecutorService;
import
org.apache.pinot.core.query.scheduler.resources.UnboundedResourceManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.env.PinotConfiguration;
@@ -42,13 +43,13 @@ public class FCFSQueryScheduler extends QueryScheduler {
}
@Override
- public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
if (!_isRunning) {
- return shuttingDown(queryRequest);
+ return shuttingDownResponse(queryRequest);
}
queryRequest.getTimerContext().startNewPhaseTimer(ServerQueryPhase.SCHEDULER_WAIT);
QueryExecutorService executorService =
_resourceManager.getExecutorService(queryRequest, null);
- ListenableFutureTask<byte[]> queryTask =
createQueryFutureTask(queryRequest, executorService);
+ ListenableFutureTask<DataBuffer> queryTask =
createResponseFutureTask(queryRequest, executorService);
_resourceManager.getQueryRunners().submit(queryTask);
return queryTask;
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java
b/pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java
index 1e6a1177374..2e1789607b0 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java
@@ -30,7 +30,10 @@ import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
@@ -48,6 +51,7 @@ import org.apache.pinot.common.request.InstanceRequest;
import org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
import org.apache.pinot.core.query.request.ServerQueryRequest;
import org.apache.pinot.core.query.scheduler.QueryScheduler;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.server.access.AccessControl;
import org.apache.pinot.spi.accounting.ThreadAccountant;
import org.apache.pinot.spi.env.PinotConfiguration;
@@ -185,7 +189,7 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
void submitQuery(ServerQueryRequest queryRequest, ChannelHandlerContext ctx,
long queryArrivalTimeMs) {
QueryExecutionContext executionContext =
queryRequest.toExecutionContext(_instanceName);
try (QueryThreadContext ignore = QueryThreadContext.open(executionContext,
_threadAccountant)) {
- ListenableFuture<byte[]> future = _queryScheduler.submit(queryRequest);
+ ListenableFuture<DataBuffer> future =
_queryScheduler.submitResponse(queryRequest);
if (_executionContexts != null) {
String queryId = queryRequest.getQueryId();
// Track the running query for cancellation.
@@ -199,11 +203,11 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
}
}
- private FutureCallback<byte[]> createCallback(ServerQueryRequest
queryRequest, ChannelHandlerContext ctx,
+ private FutureCallback<DataBuffer> createCallback(ServerQueryRequest
queryRequest, ChannelHandlerContext ctx,
long queryArrivalTimeMs) {
return new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable byte[] responseBytes) {
+ public void onSuccess(@Nullable DataBuffer response) {
if (_executionContexts != null) {
String queryId = queryRequest.getQueryId();
if (LOGGER.isDebugEnabled()) {
@@ -213,9 +217,9 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
}
long requestId = queryRequest.getRequestId();
String tableNameWithType = queryRequest.getTableNameWithType();
- if (responseBytes != null) {
- // responseBytes contains either query results or exception.
- sendResponse(ctx, requestId, tableNameWithType, queryArrivalTimeMs,
responseBytes);
+ if (response != null) {
+ // response contains either query results or exception.
+ sendResponse(ctx, requestId, tableNameWithType, queryArrivalTimeMs,
response);
} else {
// Send exception response.
sendErrorResponse(ctx, requestId, tableNameWithType,
queryArrivalTimeMs,
@@ -315,8 +319,7 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
dataTable.addException(QueryErrorCode.QUERY_EXECUTION.getId(),
"Query execution error on: " + _instanceName + " " +
e.getMessage());
}
- byte[] serializedDataTable = dataTable.toBytes();
- sendResponse(ctx, requestId, tableNameWithType, queryArrivalTimeMs,
serializedDataTable);
+ sendResponse(ctx, requestId, tableNameWithType, queryArrivalTimeMs,
dataTable.toDataBuffer());
} catch (Exception exception) {
LOGGER.error("Exception while sending query processing error to
Broker.", exception);
} finally {
@@ -329,16 +332,21 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
}
/// 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 -> {
if (f.isSuccess()) {
long sendResponseEndTimeMs = System.currentTimeMillis();
int sendResponseLatencyMs = (int) (sendResponseEndTimeMs -
sendResponseStartTimeMs);
_serverMetrics.addMeteredGlobalValue(ServerMeter.NETTY_CONNECTION_RESPONSES_SENT,
1);
-
_serverMetrics.addMeteredGlobalValue(ServerMeter.NETTY_CONNECTION_BYTES_SENT,
serializedDataTable.length);
+
_serverMetrics.addMeteredGlobalValue(ServerMeter.NETTY_CONNECTION_BYTES_SENT,
responseSizeBytes);
_serverMetrics.addTimedTableValue(tableNameWithType,
ServerTimer.NETTY_CONNECTION_SEND_RESPONSE_LATENCY,
sendResponseLatencyMs, TimeUnit.MILLISECONDS);
@@ -348,9 +356,9 @@ public class InstanceRequestHandler extends
SimpleChannelInboundHandler<ByteBuf>
"Slow query ({}): request handler processing time: {}, send
response latency: {}, total time to handle "
+ "request: {}", requestId, queryProcessingTimeMs,
sendResponseLatencyMs, totalQueryTimeMs);
}
- if (serializedDataTable.length > LARGE_RESPONSE_SIZE_THRESHOLD_BYTES) {
+ if (responseSizeBytes > LARGE_RESPONSE_SIZE_THRESHOLD_BYTES) {
LOGGER.warn("Large query ({}): response size in bytes: {}, table
name {}", requestId,
- serializedDataTable.length, tableNameWithType);
+ responseSizeBytes, tableNameWithType);
_serverMetrics.addMeteredTableValue(tableNameWithType,
ServerMeter.LARGE_QUERY_RESPONSES_SENT, 1);
}
} else {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
index 41b2ed68b15..fd2dc9bf158 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
@@ -22,8 +22,10 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
@@ -36,6 +38,8 @@ import org.apache.pinot.common.datatable.DataTableFactory;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.segment.spi.memory.PagedPinotOutputStream;
import org.apache.pinot.spi.accounting.ThreadResourceUsageProvider;
import org.apache.pinot.spi.exception.QueryErrorCode;
import org.apache.pinot.spi.utils.ByteArray;
@@ -372,7 +376,24 @@ public class DataTableSerDeTest {
DataTable dataTable = dataTableBuilder.build();
DataTable newDataTable =
DataTableFactory.getDataTable(dataTable.toBytes());
- for (DataTable table : new DataTable[]{dataTable,
dataTable.toDataOnlyDataTable(), newDataTable}) {
+
+ // toDataBuffer() returns the builder's pages without copying them. The
first buffer holds the header and the
+ // small sections, and the last one the metadata. Every buffer between
them is one page at most.
+ DataBuffer dataBuffer = dataTable.toDataBuffer();
+ List<ByteBuffer> buffers = new ArrayList<>();
+ dataBuffer.appendAsByteBuffers(buffers);
+ assertTrue(buffers.size() > 2, "Expected the sections in pages: " +
buffers.size());
+ for (ByteBuffer buffer : buffers.subList(1, buffers.size() - 1)) {
+ assertTrue(buffer.remaining() <=
PagedPinotOutputStream.PageAllocator.MIN_RECOMMENDED_PAGE_SIZE,
+ "Buffer of " + buffer.remaining() + " bytes");
+ }
+ byte[] dataBufferBytes = new byte[Math.toIntExact(dataBuffer.size())];
+ dataBuffer.copyTo(0, dataBufferBytes);
+ DataTable dataBufferTable = DataTableFactory.getDataTable(dataBufferBytes);
+
+ for (DataTable table : new DataTable[]{
+ dataTable, dataTable.toDataOnlyDataTable(), newDataTable,
dataBufferTable
+ }) {
assertEquals(table.getNumberOfRows(), numRows, ERROR_MESSAGE);
RoaringBitmap nullRowIds = table.getNullRowIds(6);
assertNotNull(nullRowIds, ERROR_MESSAGE);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtilsTest.java
new file mode 100644
index 00000000000..51386128c13
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/operator/streaming/StreamingResponseUtilsTest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.pinot.core.operator.streaming;
+
+import java.util.Arrays;
+import org.apache.pinot.common.datatable.DataTable;
+import org.apache.pinot.common.datatable.DataTableFactory;
+import org.apache.pinot.common.proto.Server;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.datatable.DataTableBuilder;
+import org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
+import org.apache.pinot.spi.utils.ByteArray;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+/// Tests that [StreamingResponseUtils] puts a paged data table into the gRPC
payload with the same bytes as
+/// `DataTable.toBytes()`.
+public class StreamingResponseUtilsTest {
+ private static final int NUM_ROWS = 200;
+ private static final int VALUE_SIZE = 10_000;
+
+ @Test
+ public void testPayloadOfPagedDataTable()
+ throws Exception {
+ DataSchema dataSchema = new DataSchema(new String[]{"bytes"}, new
ColumnDataType[]{ColumnDataType.BYTES});
+ DataTableBuilder dataTableBuilder =
DataTableBuilderFactory.getDataTableBuilder(dataSchema);
+ for (int rowId = 0; rowId < NUM_ROWS; rowId++) {
+ dataTableBuilder.startRow();
+ dataTableBuilder.setColumn(0, new ByteArray(valueOf(rowId)));
+ dataTableBuilder.finishRow();
+ }
+ DataTable dataTable = dataTableBuilder.build();
+
+ for (Server.ServerResponse response : new Server.ServerResponse[]{
+ StreamingResponseUtils.getDataResponse(dataTable),
StreamingResponseUtils.getNonStreamingResponse(dataTable)
+ }) {
+ DataTable receivedDataTable =
DataTableFactory.getDataTable(response.getPayload().toByteArray());
+ assertEquals(receivedDataTable.getNumberOfRows(), NUM_ROWS);
+ for (int rowId = 0; rowId < NUM_ROWS; rowId++) {
+ assertEquals(receivedDataTable.getBytes(rowId, 0).getBytes(),
valueOf(rowId));
+ }
+ // The payload must also survive the protobuf wire encoding, which
writes each part of the rope.
+ Server.ServerResponse parsed =
Server.ServerResponse.parseFrom(response.toByteArray());
+ assertEquals(parsed.getPayload(), response.getPayload());
+ }
+ }
+
+ private static byte[] valueOf(int rowId) {
+ byte[] value = new byte[VALUE_SIZE];
+ Arrays.fill(value, (byte) rowId);
+ return value;
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/scheduler/QuerySchedulerResponseApiTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/scheduler/QuerySchedulerResponseApiTest.java
new file mode 100644
index 00000000000..23abaa22e1f
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/scheduler/QuerySchedulerResponseApiTest.java
@@ -0,0 +1,98 @@
+/**
+ * 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.pinot.core.query.scheduler;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.LongAccumulator;
+import org.apache.pinot.core.query.executor.QueryExecutor;
+import org.apache.pinot.core.query.request.ServerQueryRequest;
+import
org.apache.pinot.core.query.scheduler.resources.UnboundedResourceManager;
+import org.apache.pinot.segment.spi.memory.CompoundDataBuffer;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.spi.accounting.ThreadAccountantUtils;
+import org.apache.pinot.spi.env.PinotConfiguration;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+
+/// Tests the bridge between the deprecated `byte[]` API of [QueryScheduler]
and the [DataBuffer] API, in both
+/// directions.
+public class QuerySchedulerResponseApiTest {
+ private static final byte[] RESPONSE_BYTES = {1, 2, 3, 4, 5, 6, 7};
+ private static final PinotConfiguration CONFIG = new PinotConfiguration();
+
+ /// A scheduler written against the old API, like the ones outside this
repository.
+ @Test
+ public void testSubmitResponseWrapsDeprecatedSubmit()
+ throws Exception {
+ QueryScheduler scheduler = new TestScheduler() {
+ @Override
+ @SuppressWarnings("deprecation")
+ public ListenableFuture<byte[]> submit(ServerQueryRequest queryRequest) {
+ return Futures.immediateFuture(RESPONSE_BYTES);
+ }
+ };
+ DataBuffer response =
scheduler.submitResponse(mock(ServerQueryRequest.class)).get();
+ byte[] bytes = new byte[Math.toIntExact(response.size())];
+ response.copyTo(0, bytes);
+ assertEquals(bytes, RESPONSE_BYTES);
+ }
+
+ /// A scheduler written against the new API, called through the deprecated
method.
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testDeprecatedSubmitCopiesSubmitResponse()
+ throws Exception {
+ QueryScheduler scheduler = new TestScheduler() {
+ @Override
+ public ListenableFuture<DataBuffer> submitResponse(ServerQueryRequest
queryRequest) {
+ // Split the response into three buffers.
+ CompoundDataBuffer.Builder builder = new CompoundDataBuffer.Builder();
+ builder.addBuffer(ByteBuffer.wrap(RESPONSE_BYTES, 0, 2).slice());
+ builder.addBuffer(ByteBuffer.wrap(RESPONSE_BYTES, 2, 3).slice());
+ builder.addBuffer(ByteBuffer.wrap(RESPONSE_BYTES, 5, 2).slice());
+ return Futures.immediateFuture(builder.build());
+ }
+ };
+ assertEquals(scheduler.submit(mock(ServerQueryRequest.class)).get(),
RESPONSE_BYTES);
+ }
+
+ @Test
+ public void testSchedulerMustOverrideOneSubmitMethod() {
+ assertThrows(IllegalStateException.class, TestScheduler::new);
+ }
+
+ /// Overrides neither submit method, so subclasses choose which one to
override.
+ private static class TestScheduler extends QueryScheduler {
+ TestScheduler() {
+ super(CONFIG, "serverId", mock(QueryExecutor.class),
ThreadAccountantUtils.getNoOpAccountant(),
+ new LongAccumulator(Long::max, 0), new
UnboundedResourceManager(CONFIG));
+ }
+
+ @Override
+ public String name() {
+ return "test";
+ }
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/transport/InstanceRequestHandlerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/transport/InstanceRequestHandlerTest.java
index ecf17e252f2..81eb6985029 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/transport/InstanceRequestHandlerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/transport/InstanceRequestHandlerTest.java
@@ -98,7 +98,7 @@ public class InstanceRequestHandlerTest {
DataTable dataTable = DataTableFactory.getDataTable(replyBytes);
assertEquals(dataTable.getMetadata().get(DataTable.MetadataKey.REQUEST_ID.getName()),
Long.toString(requestId),
"The broker matches the reply to its ping by request id");
- verify(queryScheduler, never()).submit(any());
+ verify(queryScheduler, never()).submitResponse(any());
channel.finishAndReleaseAll();
}
@@ -117,12 +117,12 @@ public class InstanceRequestHandlerTest {
assertEquals(queryRequest.getRequestId(), -42L);
QueryScheduler queryScheduler = mock(QueryScheduler.class);
- when(queryScheduler.submit(any())).thenReturn(SettableFuture.create());
+
when(queryScheduler.submitResponse(any())).thenReturn(SettableFuture.create());
EmbeddedChannel channel = new EmbeddedChannel(
new InstanceRequestHandler("server01", new PinotConfiguration(),
queryScheduler, mock(AccessControl.class),
ThreadAccountantUtils.getNoOpAccountant()));
channel.writeInbound(Unpooled.wrappedBuffer(serialize(asSeenByOldServer)));
- verify(queryScheduler).submit(any());
+ verify(queryScheduler).submitResponse(any());
channel.finishAndReleaseAll();
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/transport/QueryRoutingTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/transport/QueryRoutingTest.java
index 379326ca7fa..b8516d1dc5d 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/transport/QueryRoutingTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/transport/QueryRoutingTest.java
@@ -35,7 +35,9 @@ import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -53,6 +55,8 @@ import
org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
import org.apache.pinot.core.query.scheduler.QueryScheduler;
import org.apache.pinot.core.routing.SegmentsToQuery;
import
org.apache.pinot.core.transport.server.routing.stats.ServerRoutingStatsManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.server.access.AccessControl;
import org.apache.pinot.spi.accounting.ThreadAccountantUtils;
import org.apache.pinot.spi.config.table.TableType;
@@ -60,6 +64,7 @@ import org.apache.pinot.spi.env.PinotConfiguration;
import org.apache.pinot.spi.exception.QueryErrorCode;
import org.apache.pinot.spi.metrics.PinotMetricUtils;
import org.apache.pinot.spi.metrics.PinotMetricsRegistry;
+import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.sql.parsers.CalciteSqlCompiler;
import org.apache.pinot.util.TestUtils;
@@ -140,8 +145,12 @@ public class QueryRoutingTest {
}
private QueryServer getQueryServer(int responseDelayMs, byte[]
responseBytes, int port) {
+ return getQueryServer(responseDelayMs,
PinotByteBuffer.wrap(responseBytes), port);
+ }
+
+ private QueryServer getQueryServer(int responseDelayMs, DataBuffer response,
int port) {
InstanceRequestHandler handler = new InstanceRequestHandler("server01",
new PinotConfiguration(),
- mockQueryScheduler(responseDelayMs, responseBytes),
mock(AccessControl.class),
+ mockQueryScheduler(responseDelayMs, response),
mock(AccessControl.class),
ThreadAccountantUtils.getNoOpAccountant());
return new QueryServer(port, null, handler);
}
@@ -154,11 +163,11 @@ public class QueryRoutingTest {
return ((InetSocketAddress) server.getChannel().localAddress()).getPort();
}
- private QueryScheduler mockQueryScheduler(int responseDelayMs, byte[]
responseBytes) {
+ private QueryScheduler mockQueryScheduler(int responseDelayMs, DataBuffer
response) {
QueryScheduler queryScheduler = mock(QueryScheduler.class);
- when(queryScheduler.submit(any())).thenAnswer(invocation -> {
+ when(queryScheduler.submitResponse(any())).thenAnswer(invocation -> {
Thread.sleep(responseDelayMs);
- return Futures.immediateFuture(responseBytes);
+ return Futures.immediateFuture(response);
});
return queryScheduler;
}
@@ -231,6 +240,51 @@ public class QueryRoutingTest {
assertEquals(_serverRoutingStatsManager.fetchNumInFlightRequestsForServer(serverId).intValue(),
0);
}
+ /// The server sends a response that is split into many pages. The broker
must receive the same table.
+ @Test
+ public void testPagedResponse()
+ throws Exception {
+ long requestId = 123;
+ DataSchema dataSchema =
+ new DataSchema(new String[]{"bytes"}, new
DataSchema.ColumnDataType[]{DataSchema.ColumnDataType.BYTES});
+ DataTableBuilder dataTableBuilder =
DataTableBuilderFactory.getDataTableBuilder(dataSchema);
+ int numRows = 200;
+ for (int rowId = 0; rowId < numRows; rowId++) {
+ dataTableBuilder.startRow();
+ byte[] value = new byte[10_000];
+ Arrays.fill(value, (byte) rowId);
+ dataTableBuilder.setColumn(0, new ByteArray(value));
+ dataTableBuilder.finishRow();
+ }
+ DataTable dataTable = dataTableBuilder.build();
+ dataTable.getMetadata().put(MetadataKey.REQUEST_ID.getName(),
Long.toString(requestId));
+ DataBuffer response = dataTable.toDataBuffer();
+ List<ByteBuffer> responseBuffers = new ArrayList<>();
+ response.appendAsByteBuffers(responseBuffers);
+ assertTrue(responseBuffers.size() > 100, "Expected the response in many
pages: " + responseBuffers.size());
+
+ _queryServer = getQueryServer(0, response, 0);
+ initializeTestFixtures(startAndGetPort(_queryServer));
+
+ AsyncQueryResponse asyncQueryResponse =
+ _queryRouter.submitQuery(requestId, "testTable", BROKER_REQUEST,
_routingTable, null, null, 600_000L);
+ Map<ServerRoutingInstance, ServerResponse> responses =
asyncQueryResponse.getFinalResponses();
+ assertEquals(responses.size(), 1);
+ ServerResponse serverResponse =
responses.get(_offlineServerRoutingInstance);
+ assertNotNull(serverResponse);
+ assertEquals(serverResponse.getResponseSize(), response.size());
+ DataTable receivedDataTable = serverResponse.getDataTable();
+ assertNotNull(receivedDataTable);
+ assertEquals(receivedDataTable.getNumberOfRows(), numRows);
+ for (int rowId = 0; rowId < numRows; rowId++) {
+ byte[] expected = new byte[10_000];
+ Arrays.fill(expected, (byte) rowId);
+ assertEquals(receivedDataTable.getBytes(rowId, 0).getBytes(), expected);
+ }
+ _requestCount += 2;
+ waitForStatsUpdate(_requestCount);
+ }
+
@Test
public void testInvalidResponse()
throws Exception {
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutor.java
b/pinot-server/src/main/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutor.java
index 84a2706f254..3ea61e403ba 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutor.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutor.java
@@ -44,6 +44,7 @@ import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUt
import org.apache.pinot.core.query.scheduler.QueryScheduler;
import org.apache.pinot.segment.local.data.manager.SegmentDataManager;
import org.apache.pinot.segment.local.data.manager.TableDataManager;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
import org.apache.pinot.spi.config.table.PageCacheWarmupConfig;
import org.apache.pinot.spi.config.table.QuotaConfig;
import org.apache.pinot.spi.config.table.TableConfig;
@@ -273,8 +274,8 @@ public class PageCacheWarmupServerQueryExecutor {
_serverMetrics.addMeteredGlobalValue(ServerMeter.PAGE_CACHE_WARMUP_QUERIES, 1L);
// Execute query with timeout
- ListenableFuture<byte[]> listenableFuture =
_queryScheduler.submit(serverQueryRequest);
- CompletableFuture<byte[]> queryFuture =
toCompletableFuture(listenableFuture);
+ ListenableFuture<DataBuffer> listenableFuture =
_queryScheduler.submitResponse(serverQueryRequest);
+ CompletableFuture<DataBuffer> queryFuture =
toCompletableFuture(listenableFuture);
CompletableFuture<Void> trackedFuture =
queryFuture.orTimeout(remainingWarmupTimeMs, TimeUnit.MILLISECONDS)
.thenAccept(result -> LOGGER.info("Successfully executed warmup
query={}", query))
.exceptionally(ex -> {
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutorTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutorTest.java
index 9045e6ec347..f4c3d623562 100644
---
a/pinot-server/src/test/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutorTest.java
+++
b/pinot-server/src/test/java/org/apache/pinot/server/warmup/PageCacheWarmupServerQueryExecutorTest.java
@@ -30,6 +30,7 @@ import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUt
import org.apache.pinot.core.query.scheduler.QueryScheduler;
import org.apache.pinot.segment.local.data.manager.SegmentDataManager;
import org.apache.pinot.segment.local.data.manager.TableDataManager;
+import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.spi.config.table.PageCacheWarmupConfig;
import org.apache.pinot.spi.config.table.QuotaConfig;
import org.apache.pinot.spi.config.table.TableConfig;
@@ -123,8 +124,8 @@ public class PageCacheWarmupServerQueryExecutorTest {
String table = "testTable";
// Mock QueryScheduler to return an already‑completed future
- when(_queryScheduler.submit(any())).thenAnswer(inv ->
- com.google.common.util.concurrent.Futures.immediateFuture(new
byte[0]));
+ when(_queryScheduler.submitResponse(any())).thenAnswer(inv ->
+
com.google.common.util.concurrent.Futures.immediateFuture(PinotByteBuffer.wrap(new
byte[0])));
PageCacheWarmupConfig.Spec warmSpec = new PageCacheWarmupConfig.Spec(true,
2, null, null);
@@ -157,7 +158,7 @@ public class PageCacheWarmupServerQueryExecutorTest {
_warmupServerQueryExecutor.warmupTable(table, warmSpec, queries, null,
warmupQps);
// Submit should be called once per query
- verify(_queryScheduler, times(queries.size())).submit(any());
+ verify(_queryScheduler, times(queries.size())).submitResponse(any());
// No error metric should be recorded
verify(ServerMetrics.get(), never())
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]