Introduce builder for C++ Flight servers

This commit is contained in:
David Li committed 2019-05-20 10:27:19 -04:00
1 parent 4744196672
commit 675acc9bc2
18 files changed
+190 -82

No files matched your search

+12 -4
View File
@@ -231,17 +231,20 @@ class DoPutPayloadWriter : public ipc::internal::IpcPayloadWriter {
class FlightClient::FlightClientImpl {
public:
Status Connect(const Location& location) {
Status Connect(const Location& location, const FlightClientOptions& options) {
const std::string& scheme = location.scheme();
std::stringstream grpc_uri;
std::shared_ptr<grpc::ChannelCredentials> creds;
if (scheme == kSchemeGrpc || scheme == kSchemeGrpcTcp || scheme == kSchemeGrpcTls) {
// TODO(wesm): Support other kinds of GRPC ChannelCredentials
grpc_uri << location.uri_->host() << ":" << location.uri_->port_text();
if (scheme == "grpc+tls") {
creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
grpc::SslCredentialsOptions ssl_options;
if (!options.tls_root_certs.empty()) {
ssl_options.pem_root_certs = options.tls_root_certs;
}
creds = grpc::SslCredentials(ssl_options);
} else {
creds = grpc::InsecureChannelCredentials();
}
@@ -401,8 +404,13 @@ FlightClient::~FlightClient() {}
Status FlightClient::Connect(const Location& location,
std::unique_ptr<FlightClient>* client) {
return Connect(location, {}, client);
}
Status FlightClient::Connect(const Location& location, const FlightClientOptions& options,
std::unique_ptr<FlightClient>* client) {
client->reset(new FlightClient);
return (*client)->impl_->Connect(location);
return (*client)->impl_->Connect(location, options);
}
Status FlightClient::Authenticate(const FlightCallOptions& options,
+14
View File
@@ -57,6 +57,11 @@ class ARROW_EXPORT FlightCallOptions {
TimeoutDuration timeout;
};
class ARROW_EXPORT FlightClientOptions {
public:
std::string tls_root_certs;
};
/// \brief Client class for Arrow Flight RPC services (gRPC-based).
/// API experimental for now
class ARROW_EXPORT FlightClient {
@@ -70,6 +75,15 @@ class ARROW_EXPORT FlightClient {
/// successful
static Status Connect(const Location& location, std::unique_ptr<FlightClient>* client);
/// \brief Connect to an unauthenticated flight service
/// \param[in] location the URI
/// \param[in] options Other options for setting up the client
/// \param[out] client the created FlightClient
/// \return Status OK status may not indicate that the connection was
/// successful
static Status Connect(const Location& location, const FlightClientOptions& options,
std::unique_ptr<FlightClient>* client);
/// \brief Authenticate to the server using the given handler.
/// \param[in] options Per-RPC options
/// \param[in] auth_handler The authentication mechanism to use
+20 -16
View File
@@ -255,24 +255,25 @@ class DoPutTestServer : public FlightServerBase {
class TestAuthHandler : public ::testing::Test {
public:
void SetUp() {
port_ = 30000;
server_.reset(new InProcessTestServer(
std::unique_ptr<FlightServerBase>(new AuthTestServer), port_));
ASSERT_OK(server_->Start(std::unique_ptr<ServerAuthHandler>(
new TestServerAuthHandler("user", "p4ssw0rd"))));
Location location;
std::unique_ptr<FlightServerBase> server(new AuthTestServer);
ASSERT_OK(Location::ForGrpcTcp("localhost", 30000, &location));
FlightServerOptions options(location);
options.auth_handler =
std::unique_ptr<ServerAuthHandler>(new TestServerAuthHandler("user", "p4ssw0rd"));
ASSERT_OK(server->Init(options));
server_.reset(new InProcessTestServer(std::move(server), location));
ASSERT_OK(server_->Start());
ASSERT_OK(ConnectClient());
}
void TearDown() { server_->Stop(); }
Status ConnectClient() {
Location location;
RETURN_NOT_OK(Location::ForGrpcTcp("localhost", port_, &location));
return FlightClient::Connect(location, &client_);
}
Status ConnectClient() { return FlightClient::Connect(server_->location(), &client_); }
protected:
int port_;
std::unique_ptr<FlightClient> client_;
std::unique_ptr<InProcessTestServer> server_;
};
@@ -280,17 +281,21 @@ class TestAuthHandler : public ::testing::Test {
class TestDoPut : public ::testing::Test {
public:
void SetUp() {
port_ = 30000;
Location location;
ASSERT_OK(Location::ForGrpcTcp("localhost", 30000, &location));
do_put_server_ = new DoPutTestServer();
server_.reset(new InProcessTestServer(
std::unique_ptr<FlightServerBase>(do_put_server_), port_));
ASSERT_OK(server_->Start({}));
std::unique_ptr<FlightServerBase>(do_put_server_), location));
FlightServerOptions options(location);
ASSERT_OK(do_put_server_->Init(options));
ASSERT_OK(server_->Start());
ASSERT_OK(ConnectClient());
}
void TearDown() { server_->Stop(); }
Status ConnectClient() { return FlightClient::Connect("localhost", port_, &client_); }
Status ConnectClient() { return FlightClient::Connect(server_->location(), &client_); }
void CheckBatches(FlightDescriptor expected_descriptor,
const BatchVector& expected_batches) {
@@ -314,7 +319,6 @@ class TestDoPut : public ::testing::Test {
}
protected:
int port_;
std::unique_ptr<FlightClient> client_;
std::unique_ptr<InProcessTestServer> server_;
DoPutTestServer* do_put_server_;
+3 -2
View File
@@ -207,8 +207,9 @@ int main(int argc, char** argv) {
arrow::flight::Location location;
ARROW_CHECK_OK(arrow::flight::Location::ForGrpcTcp("0.0.0.0", FLAGS_port, &location));
ARROW_CHECK_OK(
g_server->Init(std::unique_ptr<arrow::flight::NoOpAuthHandler>(), location));
arrow::flight::FlightServerOptions options(location);
ARROW_CHECK_OK(g_server->Init(options));
// Exit with a clean error code (0) on SIGTERM
ARROW_CHECK_OK(g_server->SetShutdownOnSignals({SIGTERM}));
std::cout << "Server port: " << FLAGS_port << std::endl;
+20 -5
View File
@@ -21,6 +21,7 @@
#include <atomic>
#include <cstdint>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
@@ -438,24 +439,38 @@ void FlightServerBase::Impl::HandleSignal(int signum) {
}
}
FlightServerOptions::FlightServerOptions(const Location& location_)
: location(location_), auth_handler(nullptr) {}
FlightServerBase::FlightServerBase() { impl_.reset(new Impl); }
FlightServerBase::~FlightServerBase() {}
Status FlightServerBase::Init(std::unique_ptr<ServerAuthHandler> auth_handler,
const Location& location) {
std::shared_ptr<ServerAuthHandler> handler = std::move(auth_handler);
Status FlightServerBase::Init(FlightServerOptions& options) {
std::shared_ptr<ServerAuthHandler> handler = std::move(options.auth_handler);
impl_->service_.reset(new FlightServiceImpl(handler, this));
grpc::ServerBuilder builder;
// Allow uploading messages of any length
builder.SetMaxReceiveMessageSize(-1);
const Location& location = options.location;
const std::string scheme = location.scheme();
if (scheme == kSchemeGrpc || scheme == kSchemeGrpcTcp) {
if (scheme == kSchemeGrpc || scheme == kSchemeGrpcTcp || scheme == kSchemeGrpcTls) {
std::stringstream address;
address << location.uri_->host() << ':' << location.uri_->port_text();
builder.AddListeningPort(address.str(), grpc::InsecureServerCredentials());
std::shared_ptr<grpc::ServerCredentials> creds;
if (scheme == kSchemeGrpcTls) {
grpc::SslServerCredentialsOptions ssl_options;
ssl_options.pem_key_cert_pairs.push_back(
{options.tls_private_key, options.tls_cert_chain});
creds = grpc::SslServerCredentials(ssl_options);
} else {
creds = grpc::InsecureServerCredentials();
}
builder.AddListeningPort(address.str(), creds);
} else if (scheme == kSchemeGrpcUnix) {
std::stringstream address;
address << "unix:" << location.uri_->path();
+13 -6
View File
@@ -24,6 +24,7 @@
#include <string>
#include <vector>
#include "arrow/flight/server_auth.h"
#include "arrow/flight/types.h" // IWYU pragma: keep
#include "arrow/ipc/dictionary.h"
#include "arrow/memory_pool.h"
@@ -38,8 +39,6 @@ class Status;
namespace flight {
class ServerAuthHandler;
/// \brief Interface that produces a sequence of IPC payloads to be sent in
/// FlightData protobuf messages
class ARROW_EXPORT FlightDataStream {
@@ -89,6 +88,16 @@ class ARROW_EXPORT ServerCallContext {
virtual const std::string& peer_identity() const = 0;
};
class ARROW_EXPORT FlightServerOptions {
public:
explicit FlightServerOptions(const Location& location_);
Location location;
std::unique_ptr<ServerAuthHandler> auth_handler;
std::string tls_cert_chain;
std::string tls_private_key;
};
/// \brief Skeleton RPC server implementation which can be used to create
/// custom servers by implementing its abstract methods
class ARROW_EXPORT FlightServerBase {
@@ -100,10 +109,8 @@ class ARROW_EXPORT FlightServerBase {
/// \brief Initialize a Flight server listening at the given location.
/// This method must be called before any other method.
/// \param[in] auth_handler The authentication handler. May be
/// nullptr if no authentication is desired.
/// \param[in] location The location to serve on.
Status Init(std::unique_ptr<ServerAuthHandler> auth_handler, const Location& location);
/// \param[in] options The configuration for this server.
Status Init(FlightServerOptions& options);
/// \brief Set the server to stop when receiving any of the given signal
/// numbers.
@@ -126,7 +126,9 @@ int main(int argc, char** argv) {
g_server.reset(new arrow::flight::FlightIntegrationTestServer);
arrow::flight::Location location;
ARROW_CHECK_OK(arrow::flight::Location::ForGrpcTcp("0.0.0.0", FLAGS_port, &location));
ARROW_CHECK_OK(g_server->Init(nullptr, location));
arrow::flight::FlightServerOptions options(location);
ARROW_CHECK_OK(g_server->Init(options));
// Exit with a clean error code (0) on SIGTERM
ARROW_CHECK_OK(g_server->SetShutdownOnSignals({SIGTERM}));
+4 -2
View File
@@ -139,10 +139,12 @@ int main(int argc, char** argv) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
g_server.reset(new arrow::flight::FlightTestServer);
arrow::flight::Location location;
ARROW_CHECK_OK(arrow::flight::Location::ForGrpcTcp("0.0.0.0", FLAGS_port, &location));
ARROW_CHECK_OK(
g_server->Init(std::unique_ptr<arrow::flight::NoOpAuthHandler>(), location));
arrow::flight::FlightServerOptions options(location);
ARROW_CHECK_OK(g_server->Init(options));
// Exit with a clean error code (0) on SIGTERM
ARROW_CHECK_OK(g_server->SetShutdownOnSignals({SIGTERM}));
+2 -7
View File
@@ -118,22 +118,17 @@ bool TestServer::IsRunning() { return server_process_->running(); }
int TestServer::port() const { return port_; }
Status InProcessTestServer::Start(std::unique_ptr<ServerAuthHandler> auth_handler) {
Location location;
RETURN_NOT_OK(Location::ForGrpcTcp("localhost", port_, &location));
RETURN_NOT_OK(server_->Init(std::move(auth_handler), location));
Status InProcessTestServer::Start() {
thread_ = std::thread([this]() { ARROW_EXPECT_OK(server_->Serve()); });
return Status::OK();
}
Status InProcessTestServer::Start() { return Start({}); }
void InProcessTestServer::Stop() {
server_->Shutdown();
thread_.join();
}
int InProcessTestServer::port() const { return port_; }
const Location& InProcessTestServer::location() const { return location_; }
InProcessTestServer::~InProcessTestServer() {
// Make sure server shuts down properly
+5 -5
View File
@@ -63,17 +63,17 @@ class ARROW_EXPORT TestServer {
class ARROW_EXPORT InProcessTestServer {
public:
explicit InProcessTestServer(std::unique_ptr<FlightServerBase> server, int port)
: server_(std::move(server)), port_(port), thread_() {}
explicit InProcessTestServer(std::unique_ptr<FlightServerBase> server,
const Location& location)
: server_(std::move(server)), location_(location), thread_() {}
~InProcessTestServer();
Status Start();
Status Start(std::unique_ptr<ServerAuthHandler> auth_handler);
void Stop();
int port() const;
const Location& location() const;
private:
std::unique_ptr<FlightServerBase> server_;
int port_;
Location location_;
std::thread thread_;
};
+1 -1
View File
@@ -102,7 +102,7 @@ Status Location::ForGrpcUnix(const std::string& path, Location* location) {
return Location::Parse(uri_string.str(), location);
}
std::string Location::ToString() const { return uri_->to_string(); }
std::string Location::ToString() const { return uri_->ToString(); }
std::string Location::scheme() const {
std::string scheme = uri_->scheme();
if (scheme.empty()) {
+1 -1
View File
@@ -120,7 +120,7 @@ std::string Uri::path() const {
return ss.str();
}
const std::string& Uri::to_string() const { return impl_->string_rep_; }
const std::string& Uri::ToString() const { return impl_->string_rep_; }
Status Uri::Parse(const std::string& uri_string) {
impl_->Reset();
+2 -1
View File
@@ -54,8 +54,9 @@ class ARROW_EXPORT Uri {
int32_t port() const;
/// The URI path component.
std::string path() const;
/// Get the string representation of this URI.
const std::string& to_string() const;
const std::string& ToString() const;
/// Factory function to parse a URI from its string representation.
Status Parse(const std::string& uri_string);
+11 -1
View File
@@ -90,6 +90,8 @@ def get_flight(args, client):
def _add_common_arguments(parser):
parser.add_argument('--tls', action='store_true')
parser.add_argument('--tls-roots', default=None)
parser.add_argument('host', type=str,
help="The host to connect to.")
@@ -131,7 +133,15 @@ def main():
}
host, port = args.host.split(':')
port = int(port)
client = pyarrow.flight.FlightClient.connect(f"grpc://{host}:{port}")
scheme = "grpc+tcp"
connection_args = {}
if args.tls:
scheme = "grpc+tls"
if args.tls_roots:
with open(args.tls_roots, "rb") as root_certs:
connection_args["tls_root_certs"] = root_certs.read()
client = pyarrow.flight.FlightClient.connect(f"{scheme}://{host}:{port}",
**connection_args)
while True:
try:
action = pyarrow.flight.Action("healthcheck", b"")
+20 -2
View File
@@ -17,6 +17,7 @@
"""An example Flight Python server."""
import argparse
import ast
import threading
import time
@@ -76,7 +77,7 @@ class FlightServer(pyarrow.flight.FlightServerBase):
return None
return pyarrow.flight.RecordBatchStream(self.flights[key])
def list_actions(self, context):
def list_actions(self):
return [
("clear", "Clear the stored flights."),
("shutdown", "Shut down this server."),
@@ -100,8 +101,25 @@ class FlightServer(pyarrow.flight.FlightServerBase):
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=5005)
parser.add_argument("--tls", nargs=2, default=None)
args = parser.parse_args()
server = FlightServer()
server.run(5005)
kwargs = {}
scheme = "grpc+tcp"
if args.tls:
scheme = "grpc+tls"
with open(args.tls[0], "rb") as cert_file:
kwargs["tls_cert_chain"] = cert_file.read()
with open(args.tls[1], "rb") as key_file:
kwargs["tls_private_key"] = key_file.read()
location = "{}://0.0.0.0:{}".format(scheme, args.port)
print("Serving on", location)
server.run(location, **kwargs)
if __name__ == '__main__':
+36 -26
View File
@@ -72,7 +72,7 @@ cdef class Action:
return pyarrow_wrap_buffer(self.action.body)
@staticmethod
cdef CAction unwrap(action):
cdef CAction unwrap(action) except *:
if not isinstance(action, Action):
raise TypeError("Must provide Action, not '{}'".format(
type(action)))
@@ -168,11 +168,11 @@ cdef class FlightDescriptor:
return "<FlightDescriptor type: {!r}>".format(self.descriptor_type)
@staticmethod
cdef FlightDescriptor unwrap(descriptor):
cdef CFlightDescriptor unwrap(descriptor) except *:
if not isinstance(descriptor, FlightDescriptor):
raise TypeError("Must provide a FlightDescriptor, not '{}'".format(
type(descriptor)))
return <FlightDescriptor> descriptor
return (<FlightDescriptor> descriptor).descriptor
cdef class Ticket:
@@ -225,9 +225,9 @@ cdef class Location:
return result
@staticmethod
cdef CLocation unwrap(object location):
cdef CLocation unwrap(object location) except *:
cdef CLocation c_location
if isinstance(location, (six.text_type, six.binary_type)):
if isinstance(location, six.text_type):
check_status(CLocation.Parse(tobytes(location), &c_location))
return c_location
elif not isinstance(location, Location):
@@ -381,21 +381,20 @@ cdef class FlightClient:
.format(self.__class__.__name__))
@staticmethod
def connect(location):
def connect(location, **kwargs):
"""Connect to a Flight service on the given host and port."""
cdef:
FlightClient result = FlightClient.__new__(FlightClient)
int c_port = 0
CLocation c_location
CLocation c_location = Location.unwrap(location)
CFlightClientOptions c_options
if isinstance(location, Location):
c_location = (<Location> location).location
else:
c_location = CLocation()
check_status(CLocation.Parse(tobytes(location), &c_location))
if "tls_root_certs" in kwargs:
c_options.tls_root_certs = tobytes(kwargs["tls_root_certs"])
with nogil:
check_status(CFlightClient.Connect(c_location, &result.client))
check_status(CFlightClient.Connect(c_location, c_options,
&result.client))
return result
@@ -478,12 +477,12 @@ cdef class FlightClient:
cdef:
FlightInfo result = FlightInfo.__new__(FlightInfo)
CFlightCallOptions* c_options = FlightCallOptions.unwrap(options)
FlightDescriptor c_descriptor = \
CFlightDescriptor c_descriptor = \
FlightDescriptor.unwrap(descriptor)
with nogil:
check_status(self.client.get().GetFlightInfo(
deref(c_options), c_descriptor.descriptor, &result.info))
deref(c_options), c_descriptor, &result.info))
return result
@@ -494,7 +493,8 @@ cdef class FlightClient:
CFlightCallOptions* c_options = FlightCallOptions.unwrap(options)
with nogil:
check_status(self.client.get().DoGet(deref(c_options), ticket.ticket, &reader))
check_status(self.client.get().DoGet(
deref(c_options), ticket.ticket, &reader))
result = FlightRecordBatchReader()
result.reader.reset(reader.release())
return result
@@ -506,12 +506,12 @@ cdef class FlightClient:
shared_ptr[CSchema] c_schema = pyarrow_unwrap_schema(schema)
unique_ptr[CRecordBatchWriter] writer
CFlightCallOptions* c_options = FlightCallOptions.unwrap(options)
FlightDescriptor c_descriptor = \
CFlightDescriptor c_descriptor = \
FlightDescriptor.unwrap(descriptor)
with nogil:
check_status(self.client.get().DoPut(
deref(c_options), c_descriptor.descriptor, c_schema, &writer))
deref(c_options), c_descriptor, c_schema, &writer))
result = FlightRecordBatchWriter()
result.writer.reset(writer.release())
return result
@@ -520,7 +520,7 @@ cdef class FlightClient:
cdef class FlightDataStream:
"""Abstract base class for Flight data streams."""
cdef CFlightDataStream* to_stream(self):
cdef CFlightDataStream* to_stream(self) except *:
"""Create the C++ data stream for the backing Python object.
We don't expose the C++ object to Python, so we can manage its
@@ -547,7 +547,7 @@ cdef class RecordBatchStream(FlightDataStream):
"but got: {}".format(type(data_source)))
self.data_source = data_source
cdef CFlightDataStream* to_stream(self):
cdef CFlightDataStream* to_stream(self) except *:
cdef:
shared_ptr[CRecordBatchReader] reader
if isinstance(self.data_source, _CRecordBatchReader):
@@ -585,7 +585,7 @@ cdef class GeneratorStream(FlightDataStream):
self.schema = pyarrow_unwrap_schema(schema)
self.generator = iter(generator)
cdef CFlightDataStream* to_stream(self):
cdef CFlightDataStream* to_stream(self) except *:
cdef:
function[cb_data_stream_next] callback = &_data_stream_next
return new CPyGeneratorFlightDataStream(self, self.schema, callback)
@@ -985,20 +985,30 @@ cdef class FlightServerBase:
cdef:
unique_ptr[PyFlightServer] server
def run(self, location, auth_handler=None):
def run(self, location, auth_handler=None, **kwargs):
cdef:
PyFlightServerVtable vtable = PyFlightServerVtable()
PyFlightServer* c_server
CLocation c_location = Location.unwrap(location)
unique_ptr[CServerAuthHandler] c_auth_handler
unique_ptr[CFlightServerOptions] c_options
c_options.reset(new CFlightServerOptions(Location.unwrap(location)))
if auth_handler:
if not isinstance(auth_handler, ServerAuthHandler):
raise TypeError("auth_handler must be a ServerAuthHandler, "
"not a '{}'".format(type(auth_handler)))
c_auth_handler.reset(
c_options.get().auth_handler.reset(
(<ServerAuthHandler> auth_handler).to_handler())
if "tls_cert_chain" in kwargs:
if "tls_private_key" not in kwargs:
raise ValueError(
"Must provide both cert chain and private key")
c_options.get().tls_cert_chain = tobytes(
kwargs["tls_cert_chain"])
c_options.get().tls_private_key = tobytes(
kwargs["tls_private_key"])
vtable.list_flights = &_list_flights
vtable.get_flight_info = &_get_flight_info
vtable.do_put = &_do_put
@@ -1009,7 +1019,7 @@ cdef class FlightServerBase:
c_server = new PyFlightServer(self, vtable)
self.server.reset(c_server)
with nogil:
check_status(c_server.Init(move(c_auth_handler), c_location))
check_status(c_server.Init(deref(c_options)))
check_status(c_server.ServeWithSignals())
def list_flights(self, context, criteria):
+13 -2
View File
@@ -153,9 +153,21 @@ cdef extern from "arrow/flight/api.h" namespace "arrow" nogil:
CFlightCallOptions()
CTimeoutDuration timeout
cdef cppclass CFlightServerOptions" arrow::flight::FlightServerOptions":
CFlightServerOptions(const CLocation& location)
CLocation location
unique_ptr[CServerAuthHandler] auth_handler
c_string tls_cert_chain
c_string tls_private_key
cdef cppclass CFlightClientOptions" arrow::flight::FlightClientOptions":
CFlightClientOptions()
c_string tls_root_certs
cdef cppclass CFlightClient" arrow::flight::FlightClient":
@staticmethod
CStatus Connect(const CLocation& location,
const CFlightClientOptions& options,
unique_ptr[CFlightClient]* client)
CStatus Authenticate(CFlightCallOptions& options,
@@ -229,8 +241,7 @@ cdef extern from "arrow/python/flight.h" namespace "arrow::py::flight" nogil:
cdef cppclass PyFlightServer:
PyFlightServer(object server, PyFlightServerVtable vtable)
CStatus Init(unique_ptr[CServerAuthHandler] auth_handler,
const CLocation& location)
CStatus Init(CFlightServerOptions& options)
CStatus ServeWithSignals() except *
void Shutdown()
+10
View File
@@ -397,3 +397,13 @@ def test_token_auth_invalid():
client = flight.FlightClient.connect(server_location)
with pytest.raises(pa.ArrowException, match=".*unauthenticated.*"):
client.authenticate(TokenClientAuthHandler('test', 'wrong'))
def test_location_invalid():
"""Test constructing invalid URIs."""
with pytest.raises(pa.ArrowException, match=".*Cannot parse URI:.*"):
flight.FlightClient.connect("%")
server = ConstantFlightServer()
with pytest.raises(pa.ArrowException, match=".*Cannot parse URI:.*"):
server.run("%")