feat: Native Arrow transport path for VectorSchemaRoot in Flight plugin - #21240
feat: Native Arrow transport path for VectorSchemaRoot in Flight plugin#21240vamsimanohar wants to merge 2 commits into
Conversation
e9fc477 to
c530fec
Compare
PR Reviewer Guide 🔍(Review updated until commit e5dc448)Here are some key observations to aid the review process:
|
c530fec to
57d5693
Compare
PR Code Suggestions ✨Latest suggestions up to e5dc448 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 6b7b55a
Suggestions up to commit ae33c2b
Suggestions up to commit 57d5693
Suggestions up to commit c530fec
|
|
Persistent review updated to latest commit 57d5693 |
|
❌ Gradle check result for 57d5693: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit ae33c2b |
Responses to review suggestions1. Allocator leak in test ( This is test-only code. The core issue is that In a real production consumer (e.g., the lakehouse distributed query engine), DataFusion owns the 2. Cache Fixed in 3. Thread safety of The class Javadoc states: "This implementation is not thread safe; consumer must ensure to invoke sendBatch serially and call completeStream() at the end." 4. Mixed byte/arrow path ( A single stream is either all native Arrow or all byte path. Mixing Additional detail on why we're not fixing this:
5. Schema mismatch in subsequent batches This is the caller's responsibility, same as the existing byte path ( 6. Document root reuse contract in The 7. Gradle check failure The failure is |
|
❌ Gradle check result for ae33c2b: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
ae33c2b to
b187be6
Compare
|
@rishabhmaurya @reta @finnegancarroll — requesting review. |
When a TransportResponse implements the new ArrowBatchResponse interface, FlightOutboundHandler sends the VectorSchemaRoot directly via putNext() instead of byte-serializing through VectorStreamOutput. On the client side, handlers implementing ArrowStreamHandler receive the VectorSchemaRoot directly instead of deserializing through VectorStreamInput. Server side: - ArrowBatchResponse marker interface for responses carrying native Arrow data - FlightServerChannel.sendArrowBatch() sends caller's VectorSchemaRoot directly - externalRoot flag prevents close() from freeing externally-owned roots Client side: - ArrowStreamHandler interface for handlers consuming native VectorSchemaRoot - resolveArrowStreamHandler() walks decorator chain via getDelegate() - TransportResponseHandler.getDelegate() enables generic chain introspection - Cached handler resolution to avoid per-batch decorator chain walk Integration tests: - NativeArrowTransportIT with single-batch and multi-batch tests verifying typed Arrow data (VarChar, Int columns) arrives intact over Flight transport Signed-off-by: Vamsi Manohar <reddyvam@amazon.com>
b187be6 to
6b7b55a
Compare
|
Persistent review updated to latest commit 6b7b55a |
|
❌ Gradle check result for 6b7b55a: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
thanks @vamsimanohar for working on it.
I won't call it unnecessary as the very first use case we onboarded was streaming aggregations, where we have to do ser/de to/from StreamOutput/Input. Maybe oneday we will start making use of Arrow format within opensearch aggregation directly and call it unnecessary :) The main question here is - who is doing buffer management? Java or DF? I think the changes can be simplified a lot as it was always designed to support this primary use case. I will add details in my next comment in a while (running the approach with kiro to implement and check). |
| * | ||
| * @opensearch.experimental | ||
| */ | ||
| public interface ArrowStreamHandler<T extends TransportResponse> { |
There was a problem hiding this comment.
nitpick - this is an alternate to Writeable's handler.read(StreamInput) - lets call it something like ArrowStreamReader to not confuse with transport response handlers.
Ideally, the buffer management should be java side with all allocation a child of the same root. Right now DataFusionService is creating its own root allocator java side. Not an expert here, but I'm assuming your concern @rishabhmaurya is the |
My bad. It slipped into description from the context of my POC and the discussion with agent. Anyways edited it. 👍 I know we can't avoid it for existing usecases. |
Thanks for the feedback @rishabhmaurya I could see few problems with the current implementation particularly on sending side. Server-side(Sending Arrow Batch): Either we provide a completion callback so the caller can close it after consumption, or FlightServerChannel takes ownership and closes it. I think FlightServerChannel closing it is fine, unless the caller is using the batch for further computation. Depends on the use case. I think once we send a batch from one node to another there won't be further computation. I should maybe revert the externalRoot change. Let me know if your idea is to introduce new interfaces altogether. ============================================================================== Client-side: ============================================================================== Multiple allocator pools: Will give it a thought on these problems. |
|
@vamsimanohar Yes - I like the way you're going about it, however to avoid back n forth, I created some rough changes on how I think this should addressed, please take a look as a reference while you work on it - #21253 ; it have few tests and passes the checks but there might be issues. Concerns with the current approach (which you're mostly aware of, still listing them here) 1. Transport-level branching
2. Core server changes for handler introspection The PR adds 3. Buffer ownership confusion
4. No pipelining support The PR passes the Proposed alternativeKey tenet
How it works Send side: API developer extends Receive side: Allocator access: |
|
Persistent review updated to latest commit e5dc448 |
|
❌ Gradle check result for e5dc448: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Closing this in favour of #21253 |
Context
While building a query engine POC using Apache DataFusion, I found that query results are already in native Arrow format (
VectorSchemaRoot) on both the sending and receiving sides. However, the current Flight transport path forces serialization for native arrow format.The data starts as Arrow, gets serialized to bytes, wrapped in a VarBinary vector, sent over Flight, unwrapped, deserialized back to bytes, and then reconstructed — all redundant when both ends already speak Arrow natively.
This PR adds a zero-serialization path so Arrow data can flow directly:
Summary
Server side (sending)
ArrowBatchResponse— marker interface forTransportResponseinstances carrying native Arrow data. WhenFlightOutboundHandlerdetects this, it sends theVectorSchemaRootdirectly viaputNext(), bypassingVectorStreamOutputserialization.FlightServerChannel.sendArrowBatch()— sends the caller'sVectorSchemaRootdirectly. Tracks ownership viaexternalRootflag soclose()doesn't free externally-owned roots.Client side (receiving)
ArrowStreamHandler— interface for response handlers that can consume nativeVectorSchemaRootdata viareadArrow(). Handlers implementing this receive typed Arrow data instead of byte streams.FlightTransportResponse.resolveArrowStreamHandler()— walks the decorator chain (MetricsTrackingResponseHandler → ContextRestoreResponseHandler → TraceableTransportResponseHandler → original handler) to find theArrowStreamHandler. Result is cached after first resolution.Handler chain walking
TransportResponseHandler.getDelegate()default method to enable generic decorator chain introspection. Implemented inContextRestoreResponseHandler,TraceableTransportResponseHandler, andMetricsTrackingResponseHandler.Key design decisions
VectorSchemaRooton the send side — Flight reads from it but does not close it.FlightStreamreuses its root on the receive side —readArrow()consumers must deep-copy if they need to hold data acrossnext()calls, or process inline for zero-copy.ArrowBatchResponseresponses continue to use the existing byte serialization path. Non-ArrowStreamHandlerhandlers continue to useVectorStreamInputdeserialization.Test plan
NativeArrowTransportIT.testSingleBatchNativeArrow— 1 batch, 3 rows, verifies typed columns (VarChar, Int) and actual data values end-to-endNativeArrowTransportIT.testMultipleBatchesNativeArrow— 3 batches, 2 rows each, verifies multi-batch streaming with data verificationArrowBatchResponseTests— unit tests for ownership, serialization bypass, and error handlingFlightTransportIT,ClientSideChaosIT,SubAggregationIT, etc.)