5555import org .apache .arrow .vector .VarBinaryVector ;
5656import org .apache .arrow .vector .VarCharVector ;
5757import org .apache .arrow .vector .VectorSchemaRoot ;
58- import org .apache .arrow .vector .types .FloatingPointPrecision ;
59- import org .apache .arrow .vector .types .TimeUnit ;
60- import org .apache .arrow .vector .types .pojo .ArrowType ;
61- import org .apache .arrow .vector .types .pojo .Field ;
62- import org .apache .arrow .vector .types .pojo .FieldType ;
6358import org .apache .beam .sdk .coders .Coder ;
6459import org .apache .beam .sdk .coders .RowCoder ;
6560import org .apache .beam .sdk .extensions .arrow .ArrowConversion ;
8479 * IO to read and write data using <a href="https://arrow.apache.org/docs/format/Flight.html">Apache
8580 * Arrow Flight</a>.
8681 *
87- * <p>Arrow Flight is a high-performance RPC framework for transferring Arrow-formatted data over
88- * gRPC. It enables 10-50x faster data transfer compared to JDBC/ODBC by avoiding
89- * serialization/deserialization overhead.
82+ * <p>Arrow Flight is an RPC framework for transferring Arrow-formatted data over gRPC.
9083 *
9184 * <h3>Reading from an Arrow Flight server</h3>
9285 *
115108 * .withDescriptor("my_table")
116109 * .withBatchSize(1024));
117110 * }</pre>
111+ *
112+ * <h3>Java runtime configuration</h3>
113+ *
114+ * <p>On Java 17 or later, Arrow memory access requires {@code
115+ * --add-opens=java.base/java.nio=ALL-UNNAMED} on JVMs executing this connector. Beam SDK containers
116+ * enable it by default. For other environments, add it to the local or runner JVM and pass {@code
117+ * --JdkAddOpenModules=java.base/java.nio=ALL-UNNAMED} to configure SDK harness JVMs. See the <a
118+ * href="https://arrow.apache.org/docs/java/install.html">Arrow Java installation guide</a>.
118119 */
119120public class ArrowFlightIO {
120121
@@ -136,25 +137,10 @@ private static CallOption[] callOptions(byte @Nullable [] token) {
136137 }
137138
138139 private static void validateWriteSchema (Schema schema ) {
139- for (Schema .Field field : schema .getFields ()) {
140- switch (field .getType ().getTypeName ()) {
141- case BYTE :
142- case INT16 :
143- case INT32 :
144- case INT64 :
145- case FLOAT :
146- case DOUBLE :
147- case STRING :
148- case BOOLEAN :
149- case BYTES :
150- case DATETIME :
151- break ;
152- default :
153- throw new IllegalArgumentException (
154- String .format (
155- "ArrowFlightIO.write() does not support Beam type '%s' for field '%s'." ,
156- field .getType ().getTypeName (), field .getName ()));
157- }
140+ try {
141+ ArrowConversion .ArrowSchemaTranslator .toArrowSchema (schema );
142+ } catch (IllegalArgumentException e ) {
143+ throw new IllegalArgumentException ("ArrowFlightIO.write(): " + e .getMessage (), e );
158144 }
159145 }
160146
@@ -415,36 +401,48 @@ static class FlightBoundedReader extends BoundedSource.BoundedReader<Row> {
415401 private transient FlightStream stream ;
416402 private transient Iterator <Row > currentBatchIterator ;
417403 private transient Row current ;
404+ private FlightBoundedSource currentSource ;
418405
419406 FlightBoundedReader (FlightBoundedSource source ) {
420407 this .source = source ;
408+ this .currentSource = source ;
421409 }
422410
423411 @ Override
424412 public boolean start () throws IOException {
425413 allocator = new RootAllocator (Long .MAX_VALUE );
426414 Read spec = source .spec ;
427415
428- String hostName = checkNotNull (spec .host (), "host" );
429- if (source .endpoint != null ) {
430- String host = source .endpoint .getHost (hostName );
431- int port = source .endpoint .getPort (spec .port ());
432- client = createClient (allocator , host , port , spec .useTls ());
433- stream = client .getStream (source .endpoint .getTicket (), spec .callOptions ());
434- } else {
435- client = createClient (allocator , hostName , spec .port (), spec .useTls ());
436- FlightInfo info =
437- client .getInfo (
438- FlightDescriptor .command (
439- checkNotNull (spec .command (), "command" ).getBytes (StandardCharsets .UTF_8 )),
440- spec .callOptions ());
441- List <FlightEndpoint > endpoints = info .getEndpoints ();
442- if (endpoints .isEmpty ()) {
443- return false ;
416+ String defaultHost = checkNotNull (spec .host (), "host" );
417+ SerializableEndpoint endpoint = source .endpoint ;
418+ if (endpoint == null ) {
419+ try (FlightClient discoveryClient =
420+ createClient (allocator , defaultHost , spec .port (), spec .useTls ())) {
421+ FlightInfo info =
422+ discoveryClient .getInfo (
423+ FlightDescriptor .command (
424+ checkNotNull (spec .command (), "command" ).getBytes (StandardCharsets .UTF_8 )),
425+ spec .callOptions ());
426+ List <FlightEndpoint > endpoints = info .getEndpoints ();
427+ if (endpoints .isEmpty ()) {
428+ return false ;
429+ }
430+ endpoint =
431+ SerializableEndpoint .fromFlightEndpoint (endpoints .get (0 ), defaultHost , spec .port ());
432+ } catch (InterruptedException e ) {
433+ Thread .currentThread ().interrupt ();
434+ throw new IOException ("Interrupted while discovering Flight endpoints" , e );
444435 }
445- stream = client . getStream ( endpoints . get ( 0 ). getTicket (), spec . callOptions () );
436+ currentSource = new FlightBoundedSource ( spec , source . beamSchema , endpoint );
446437 }
447438
439+ client =
440+ createClient (
441+ allocator ,
442+ endpoint .getHost (defaultHost ),
443+ endpoint .getPort (spec .port ()),
444+ spec .useTls ());
445+ stream = client .getStream (endpoint .getTicket (), spec .callOptions ());
448446 currentBatchIterator = Collections .emptyIterator ();
449447 return advance ();
450448 }
@@ -509,12 +507,13 @@ public void close() throws IOException {
509507
510508 @ Override
511509 public BoundedSource <Row > getCurrentSource () {
512- return source ;
510+ return currentSource ;
513511 }
514512 }
515513
516514 // ======================== WRITE ========================
517515
516+ // TODO: Return a write result with failed rows for dead-letter handling instead of PDone.
518517 @ AutoValue
519518 public abstract static class Write extends PTransform <PCollection <Row >, PDone > {
520519
@@ -621,7 +620,6 @@ static class FlightWriteFn extends DoFn<Row, Void> {
621620 private transient @ Nullable FlightClient client ;
622621 private transient FlightClient .@ Nullable ClientStreamListener listener ;
623622 private transient @ Nullable VectorSchemaRoot root ;
624- private transient org .apache .arrow .vector .types .pojo .Schema arrowSchema ;
625623 private transient List <Row > batch ;
626624
627625 FlightWriteFn (Write spec , Schema beamSchema ) {
@@ -687,11 +685,8 @@ private void ensureConnection() {
687685 currentAllocator , checkNotNull (spec .host (), "host" ), spec .port (), spec .useTls ());
688686 client = currentClient ;
689687
690- List <Field > arrowFields = new ArrayList <>();
691- for (Schema .Field beamField : beamSchema .getFields ()) {
692- arrowFields .add (toArrowField (beamField ));
693- }
694- arrowSchema = new org .apache .arrow .vector .types .pojo .Schema (arrowFields );
688+ org .apache .arrow .vector .types .pojo .Schema arrowSchema =
689+ ArrowConversion .ArrowSchemaTranslator .toArrowSchema (beamSchema );
695690 VectorSchemaRoot currentRoot = VectorSchemaRoot .create (arrowSchema , currentAllocator );
696691 root = currentRoot ;
697692
@@ -703,43 +698,6 @@ private void ensureConnection() {
703698 }
704699 }
705700
706- private Field toArrowField (Schema .Field beamField ) {
707- ArrowType arrowType = beamTypeToArrowType (beamField .getType ());
708- FieldType fieldType =
709- beamField .getType ().getNullable ()
710- ? FieldType .nullable (arrowType )
711- : FieldType .notNullable (arrowType );
712- return new Field (beamField .getName (), fieldType , Collections .emptyList ());
713- }
714-
715- private ArrowType beamTypeToArrowType (Schema .FieldType beamType ) {
716- switch (beamType .getTypeName ()) {
717- case BYTE :
718- return new ArrowType .Int (8 , true );
719- case INT16 :
720- return new ArrowType .Int (16 , true );
721- case INT32 :
722- return new ArrowType .Int (32 , true );
723- case INT64 :
724- return new ArrowType .Int (64 , true );
725- case FLOAT :
726- return new ArrowType .FloatingPoint (FloatingPointPrecision .SINGLE );
727- case DOUBLE :
728- return new ArrowType .FloatingPoint (FloatingPointPrecision .DOUBLE );
729- case STRING :
730- return ArrowType .Utf8 .INSTANCE ;
731- case BOOLEAN :
732- return ArrowType .Bool .INSTANCE ;
733- case BYTES :
734- return ArrowType .Binary .INSTANCE ;
735- case DATETIME :
736- return new ArrowType .Timestamp (TimeUnit .MILLISECOND , "UTC" );
737- default :
738- throw new IllegalArgumentException (
739- "Unsupported Beam type for ArrowFlightIO.write(): " + beamType .getTypeName ());
740- }
741- }
742-
743701 @ SuppressWarnings ("nullness" )
744702 private void flush () {
745703 if (batch == null || batch .isEmpty ()) {
0 commit comments