Skip to content

GH-55396 : [C++][FlightRPC] Add async gRPC server - #55419

Open
Alex-PLACET wants to merge 1 commit into
apache:mainfrom
Alex-PLACET:feature/async-grpc-generic-server
Open

Alex-PLACET wants to merge 1 commit into
apache:mainfrom
Alex-PLACET:feature/async-grpc-generic-server

Conversation

@Alex-PLACET

@Alex-PLACET Alex-PLACET commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

Address the issue: #55396

What changes are included in this PR?

Here we add an async gRPC Flight server whose handlers return arrow::Future instead of blocking and address the issue from #55396

The synchronous transport runs every RPC on a gRPC worker thread that the handler occupies until it returns. Throughput is bounded by the thread pool which grows at most about one thread per second rather than by the work in flight. With the async transport a handler returns a future immediately, and no gRPC thread is held while the answer is pending.

#include <arrow/flight/api.h>

class MyServer : public arrow::flight::AsyncGenericFlightServerBase {
  arrow::Future<std::shared_ptr<arrow::flight::FlightInfo>> GetFlightInfoAsync(
      const arrow::flight::ServerCallContext&,
      const arrow::flight::FlightDescriptor&) override;

  arrow::Future<std::shared_ptr<arrow::flight::AsyncFlightDataStream>> DoGetAsync(
      const arrow::flight::ServerCallContext&,
      const arrow::flight::Ticket&) override;
};

MyServer server;
ARROW_RETURN_NOT_OK(server.Init(options));
ARROW_RETURN_NOT_OK(server.Serve());

Deriving from AsyncGenericFlightServerBase is what selects the async transport. FlightServerBase is unchanged and still uses the synchronous path.

Every handler returns a future. Complete it from your own thread or pool when the answer is ready. Handlers that do run inline must still return promptly: blocking a gRPC callback thread starves new connections multiplexed onto it.

An unoverridden handler answers Status::NotImplemented.

Handler Returns
ListFlightsAsync Future<shared_ptr<AsyncFlightListing>>
GetFlightInfoAsync Future<shared_ptr<FlightInfo>>
PollFlightInfoAsync Future<shared_ptr<PollInfo>>
GetSchemaAsync Future<shared_ptr<SchemaResult>>
DoGetAsync Future<shared_ptr<AsyncFlightDataStream>>
DoActionAsync Future<shared_ptr<AsyncResultStream>>
ListActionsAsync Future<vector<ActionType>>
DoExchangeAsync Future<>
CreateDoPutListener shared_ptr<AsyncFlightDataListener> (plain, not a future)

Streams

Pull-based iterators. The transport requests one item at a time, so a producer never runs ahead of the consumer.

class AsyncFlightDataStream {
  virtual std::shared_ptr<Schema> schema() = 0;
  virtual Future<FlightPayload> GetSchemaPayloadAsync() = 0;
  virtual Future<std::optional<FlightPayload>> NextAsync() = 0;
  virtual Status Close();  // the RPC is over
};

class AsyncFlightListing {
  virtual Future<std::shared_ptr<FlightInfo>> NextAsync() = 0;
};

class AsyncResultStream {
  virtual Future<std::shared_ptr<Result>> NextAsync() = 0;
};

NextAsync() reports the end of a stream as std::nullopt.

DoExchange gets both halves:

class AsyncFlightMessageReader {
  virtual const FlightDescriptor& descriptor() const = 0;
  virtual Future<FlightStreamChunk> NextAsync() = 0;
};

class AsyncFlightMessageWriter {
  virtual Future<> BeginAsync(std::shared_ptr<Schema> schema) = 0;
  virtual Future<> WriteRecordBatchAsync(const RecordBatch& batch) = 0;
  virtual Future<> WriteMetadataAsync(std::shared_ptr<Buffer> app_metadata) = 0;
  virtual Future<> WriteWithMetadataAsync(std::shared_ptr<RecordBatch> batch, std::shared_ptr<Buffer> app_metadata) = 0;
  virtual Future<> CloseAsync() = 0;
};

DoPut uses listener

DoPut is not answered by a handler method. The transport hands each decoded
upload to a per RPC listener from CreateDoPutListener, returning nullptr
refuses the upload with UNIMPLEMENTED.

class AsyncFlightDataListener : public ipc::Listener {
  virtual Future<> OnNext(FlightStreamChunk chunk) = 0;
  virtual Future<> OnDescriptor(const FlightDescriptor& descriptor);
  virtual Future<> OnFinish(Status status);
  Future<> Cancel(Status status);
  const std::shared_ptr<AsyncFlightDataWriter>& writer() const;
};
  • OnNext runs once per decoded message. chunk.data is the RecordBatch, or nullptr for a metadata-only message.
  • OnDescriptor returning non-OK rejects the upload; the client sees that status when it closes the writer.
  • OnFinish runs exactly once, however the upload ends. A cancel and a clean end do not both report.
  • Cancel is safe from any thread at any time. It returns OK when the cancel was handed to the transport, or Invalid when no upload is in flight.
  • writer()->WriteMetadata() may be called from any thread, including after the RPC has finished, the transport guards that race.

Authentication

Auth goes through hooks on the server class:

virtual Status Handshake(const ServerCallContext& context,
                         const std::string& request,
                         std::string* response);
virtual Status ValidateToken(const ServerCallContext& context,
                             const std::string& token,
                             std::string* peer_identity);

A blocking ServerAuthHandler in FlightServerOptions is not supported: Init() rejects it with NotImplemented rather than silently servicing unauthenticated calls.

Other new API

  • FlightStreamChunk(std::shared_ptr<RecordBatch>, std::shared_ptr<Buffer>): build a chunk directly (types.h).
  • AsyncFlightMessageDecoder: decode a FlightData message from a buffer or from an already-deserialized message (flight_data_decoder.h).

Requirements

The async server needs gRPC's generic callback API:

  • = 1.65: async server compiled

  • < 1.65: Init() fails with NotImplemented

Are these changes tested?

New files:

  • cpp/src/arrow/flight/transport/grpc/async_grpc_do_get_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_do_put_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_exchange_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_handshake_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_service_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_streaming_test.cc
  • cpp/src/arrow/flight/transport/grpc/async_grpc_test_fixtures.h
  • cpp/src/arrow/flight/transport/grpc/async_grpc_unary_test.cc

Examples:

  • cpp/examples/arrow/flight_async_generic_doget_example.cc: DoGet. Each batch is produced on the stream's own worker thread --batch_delay_ms after the transport asks, so the delay never occupies a gRPC thread.
  • cpp/examples/arrow/flight_async_generic_doput_example.cc: DoPut. OnDescriptor / OnSchemaDecoded / OnNext / OnFinish, and the PutResult metadata channel.

Are there any user-facing changes?

Was AI used for this PR?

In accordance to the AI generation guidelines, please disclose below whether and how AI was used in this PR.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

⚠️ GitHub issue #55396 has been automatically assigned in GitHub to PR creator.

@Alex-PLACET
Alex-PLACET force-pushed the feature/async-grpc-generic-server branch from a27b30f to fea00aa Compare November 6, 2026 07:43
@Alex-PLACET
Alex-PLACET marked this pull request as ready for review November 9, 2026 10:26
@Alex-PLACET
Alex-PLACET requested a review from lidavidm as a code owner November 9, 2026 10:26
Copilot AI balanced review requested due to automatic review settings November 9, 2026 10:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved lifetime and synchronization bugs can cause use-after-free, deadlocks, and cancellation failures, with additional defects breaking Windows builds and mutual-TLS peer identity propagation.

9 open issues
What changed in this PR

Adds a future-based C++ Flight server for GH-55396, using gRPC’s generic callback API to avoid holding worker threads while application results are pending. The existing synchronous server remains available.

Changes:

  • Introduces asynchronous handlers, streaming interfaces, upload listeners, and authentication hooks.
  • Adds callback reactors and shares server lifecycle and serialization utilities.
  • Integrates version-gated builds, RPC tests, decoder tests, and upload/download examples.
File Description
cpp/​src/​arrow/​util/​windows_fixup.h Stops undefining Windows IN and OUT macros.
cpp/​src/​arrow/​flight/​types.h Declares a chunk constructor accepting data and metadata.
cpp/​src/​arrow/​flight/​types.cc Implements the new chunk constructor.
cpp/​src/​arrow/​flight/​transport/​grpc/​util_internal.h Declares callback-context status conversion.
cpp/​src/​arrow/​flight/​transport/​grpc/​util_internal.cc Shares status conversion across gRPC context types.
cpp/​src/​arrow/​flight/​transport/​grpc/​serialization_internal.h Exposes buffer-conversion helpers.
cpp/​src/​arrow/​flight/​transport/​grpc/​serialization_internal.cc Implements shared gRPC buffer helpers.
cpp/​src/​arrow/​flight/​transport/​grpc/​grpc_server.cc Registers and initializes the async transport.
cpp/​src/​arrow/​flight/​transport/​grpc/​grpc_server_internal.h Adds authentication hooks to shared context handling.
cpp/​src/​arrow/​flight/​transport/​grpc/​customize_grpc.h Exports Flight payload serialization.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_unary.cc Implements future-based unary RPC reactors.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_unary_test.cc Tests unary responses, errors, and deadlines.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_streaming.cc Implements listing and action-stream reactors.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_streaming_test.cc Tests streaming responses and failure paths.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_service.h Declares the generic callback service.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_service.cc Dispatches Flight methods to callback reactors.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_service_test.cc Tests dispatch, middleware, and server options.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_service_internal.h Adds shared reactor lifetime and termination support.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_handshake.cc Implements the handshake reactor.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_exchange.cc Coordinates bidirectional exchange RPCs.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_exchange_writer.cc Implements queued asynchronous exchange writes.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_exchange_test.cc Tests exchanges, metadata, and cancellation.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_exchange_reader.cc Implements demand-driven exchange reads.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_exchange_internal.h Declares exchange reader, writer, and reactor state.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_put.cc Implements listener-driven uploads and metadata responses.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_get.cc Implements asynchronous payload streaming.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_get_test.cc Tests downloads and stream cleanup.
cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_compat.h Gates callback-server support on gRPC compatibility.
cpp/​src/​arrow/​flight/​transport.h Declares asynchronous transport factories.
cpp/​src/​arrow/​flight/​transport.cc Implements asynchronous transport registration and lookup.
cpp/​src/​arrow/​flight/​transport_server_internal.h Declares shared server lifecycle management.
cpp/​src/​arrow/​flight/​transport_server_internal.cc Implements shared lifecycle operations.
cpp/​src/​arrow/​flight/​server.cc Delegates synchronous lifecycle operations to the shared implementation.
cpp/​src/​arrow/​flight/​server_async.h Defines public asynchronous server and stream interfaces.
cpp/​src/​arrow/​flight/​server_async.cc Implements async server lifecycle and default handlers.
cpp/​src/​arrow/​flight/​serialization_internal.h Exports descriptor-to-payload conversion.
cpp/​src/​arrow/​flight/​README.md Makes an end-of-file formatting-only change.
cpp/​src/​arrow/​flight/​meson.build Integrates async sources, headers, and tests into Meson.
cpp/​src/​arrow/​flight/​flight_internals_test.cc Adds decoder, buffer, and availability tests.
cpp/​src/​arrow/​flight/​flight_data_decoder.h Declares the decoder, upload listener, and metadata writer.
cpp/​src/​arrow/​flight/​flight_data_decoder.cc Implements decoding and upload-listener transport state.
cpp/​src/​arrow/​flight/​CMakeLists.txt Integrates version-gated async sources and tests into CMake.
cpp/​src/​arrow/​flight/​api.h Includes the asynchronous server API.
cpp/​examples/​arrow/​flight_async_generic_doput_example.cc Demonstrates listener-based uploads.
cpp/​examples/​arrow/​flight_async_generic_doget_example.cc Demonstrates delayed asynchronous downloads.
cpp/​examples/​arrow/​CMakeLists.txt Registers both asynchronous server examples.

Review effort: Balanced

"${CMAKE_CURRENT_BINARY_DIR}/helloworld.pb.cc"
"${CMAKE_CURRENT_BINARY_DIR}/helloworld.grpc.pb.cc")

add_arrow_example(flight_async_generic_doget_example
Comment on lines +90 to +91
reader_->OnCancelled();
writer_->OnCancelled();
Comment on lines +118 to +119
if (finished_.compare_exchange_strong(expected, true)) {
Finish(flight_context_.FinishRequest(status));
Copilot AI balanced review requested due to automatic review settings November 9, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical concurrency and lifetime defects can cause invalid futures, data races, use-after-free, and I/O on torn-down gRPC calls.

11 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Decode dictionary messages incrementally

cpp/​src/​arrow/​flight/​flight_data_decoder.cc:189

Dictionary messages remain queued until a record batch arrives. A stream sending many dictionary deltas or replacements before its next batch retains every message body, and HasRecordBatch() rescans the growing queue on each Consume(), producing unbounded retained memory and quadratic work. Decode dictionary messages incrementally instead of waiting for a record batch to drain them, while retaining only the dictionary state needed for subsequent batches.

Medium severity Queue PutResults or expose completion backpressure

cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_put.cc:63

A listener that acknowledges each batch can receive the next OnNext() before the preceding PutResult's OnWriteDone(), so ordinary consecutive uploads can hit this branch and lose acknowledgements or fail the upload. WriteMetadata() returns only a Status, so callers have no completion future to await as this error requests. Queue and serialize PutResults, or expose an asynchronous completion/backpressure API so the next acknowledgement can be issued reliably.

5 resolved since last review

Review effort: Balanced

if (!serialize.ok()) {
return MakeFlightError(FlightStatusCode::Internal, serialize.error_message());
}
StartWrite(&write_buf_);
Comment on lines +30 to +34
if (reader_->pending_.is_valid() && !reader_->pending_.is_finished()) {
reader_->ResolvePending(std::move(chunk));
} else {
reader_->buffered_ = std::move(chunk);
}
"the exchange is over: the client disconnected or cancelled"));
return;
case Action::kWrite: {
const auto status = reactor->WritePayload(std::move(payload));
Copilot AI balanced review requested due to automatic review settings November 9, 2026 15:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved concurrency and lifecycle defects can deadlock uploads, report success despite commit failures, deliver callbacks after termination, and retain unbounded buffers or resources.

6 open issues
7 previously missed issues

In code that hasn't changed since last review

High severity Prevent decoder callbacks after upload termination

cpp/​src/​arrow/​flight/​flight_data_decoder.cc:144

If the client cancels while OnDescriptor() is pending, DoPut reports OnFinish(), but completing the descriptor future with OK still delivers OnSchemaDecoded() or OnNext() afterward. The listener can therefore receive data after releasing resources or rolling back in its terminal callback. Coordinate decoder callbacks with upload termination so this continuation cannot deliver data after OnFinish().

High severity Process dictionary messages incrementally

cpp/​src/​arrow/​flight/​flight_data_decoder.cc:189

Dictionary messages remain in queue_ until a record batch arrives, so a client sending dictionary replacements without record batches causes unbounded buffer retention and repeated scans of the growing queue. Messages without application metadata also bypass OnNext, so listener backpressure cannot bound this growth. Process dictionaries incrementally into the IPC dictionary state instead of retaining every message until the next record batch.

High severity Propagate OnFinish failures before completing the RPC

cpp/​src/​arrow/​flight/​flight_data_decoder.cc:318

On a clean upload, FinishUpload calls this and immediately finishes the RPC with OK, regardless of the OnFinish future's result. A listener that commits its upload asynchronously in OnFinish can therefore fail—even return an already-failed future—while the client's Close still succeeds. Chain successful RPC completion to this future and propagate its failure, without blocking the gRPC callback thread.

High severity Release transport lock before invoking OnFinish

cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_put.cc:91

This calls the listener's OnFinish while holding write_mutex(). If another thread is in writer()->WriteMetadata(), it can hold writer_mutex_ while waiting for write_mutex(); an OnFinish that writes metadata then waits for writer_mutex_, deadlocking both threads and preventing RPC shutdown. Release the transport lock before invoking FinishUpload here, as OnWriteDone already does.

High severity Break pending-future ownership cycles during teardown

cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_streaming.cc:77

If a listing or result stream retains its pending NextAsync() future, the reactor, iterator, continuation, and HoldToken form an ownership cycle that cancellation does not break, retaining the reactor and stream after shutdown. Break the cycle during teardown while keeping late callbacks safe; simply removing the hold would introduce a use-after-free. The same issue affects DoPut listeners that retain a pending OnNext() future in async_grpc_do_put.cc:131–132.

Medium severity Serialize PutResults or await write completion

cpp/​src/​arrow/​flight/​transport/​grpc/​async_grpc_do_put.cc:67

Sequential OnNext callbacks that acknowledge each batch can reach this guard before the previous PutResult's OnWriteDone, rejecting an acknowledgement or failing an otherwise valid upload. WriteMetadata returns only a Status, so the listener has no completion signal to await before the next write. Queue and serialize PutResults, or expose an awaitable write completion so listeners can apply backpressure.

Medium severity Honor the configured memory manager in async decoders

cpp/​src/​arrow/​flight/​transport/​grpc/​grpc_server.cc:499

The async service never receives the configured memory manager, so DoPut and DoExchange decode host-backed buffers even when FlightServerOptions::memory_manager requests another allocation destination. This ignores the incoming-allocation contract; the synchronous reader applies Buffer::ViewOrCopy() in transport_server_internal.cc:46. Pass the manager through to both async decoders and apply it to message bodies, or explicitly reject unsupported managers during Init().

5 resolved since last review

Review effort: Balanced

@Alex-PLACET
Alex-PLACET force-pushed the feature/async-grpc-generic-server branch from af283d8 to 6714d7d Compare November 10, 2026 12:08
@Alex-PLACET
Alex-PLACET force-pushed the feature/async-grpc-generic-server branch from 6714d7d to 6262113 Compare November 10, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants