Skip to content

Commit 8f4a816

Browse files
committed
[Java IO] Harden ArrowFlightIO writes
1 parent 7252108 commit 8f4a816

2 files changed

Lines changed: 240 additions & 83 deletions

File tree

sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIO.java

Lines changed: 144 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
import java.net.URI;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.ArrayList;
29+
import java.util.Arrays;
2930
import java.util.Collections;
3031
import java.util.Iterator;
3132
import java.util.List;
33+
import org.apache.arrow.flight.AsyncPutListener;
34+
import org.apache.arrow.flight.CallOption;
3235
import org.apache.arrow.flight.FlightCallHeaders;
3336
import org.apache.arrow.flight.FlightClient;
3437
import org.apache.arrow.flight.FlightDescriptor;
@@ -37,7 +40,6 @@
3740
import org.apache.arrow.flight.FlightStream;
3841
import org.apache.arrow.flight.HeaderCallOption;
3942
import org.apache.arrow.flight.Location;
40-
import org.apache.arrow.flight.PutResult;
4143
import org.apache.arrow.flight.Ticket;
4244
import org.apache.arrow.memory.BufferAllocator;
4345
import org.apache.arrow.memory.RootAllocator;
@@ -120,6 +122,42 @@ public class ArrowFlightIO {
120122

121123
private ArrowFlightIO() {}
122124

125+
private static byte[] copyToken(byte[] token) {
126+
return Arrays.copyOf(checkNotNull(token, "token"), token.length);
127+
}
128+
129+
private static CallOption[] callOptions(byte @Nullable [] token) {
130+
if (token == null) {
131+
return new CallOption[0];
132+
}
133+
FlightCallHeaders headers = new FlightCallHeaders();
134+
headers.insert("authorization", "Bearer " + new String(token, StandardCharsets.UTF_8));
135+
return new CallOption[] {new HeaderCallOption(headers)};
136+
}
137+
138+
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+
}
158+
}
159+
}
160+
123161
public static Read read() {
124162
return new AutoValue_ArrowFlightIO_Read.Builder().setPort(47470).setUseTls(false).build();
125163
}
@@ -242,7 +280,7 @@ public Read withCommand(String command) {
242280

243281
/** Sets a bearer token for authentication. */
244282
public Read withToken(byte[] token) {
245-
return builder().setToken(token).build();
283+
return builder().setToken(copyToken(token)).build();
246284
}
247285

248286
@Override
@@ -270,15 +308,8 @@ public PCollection<Row> expand(PBegin input) {
270308
.setRowSchema(beamSchema);
271309
}
272310

273-
HeaderCallOption[] callOptions() {
274-
if (token() != null) {
275-
FlightCallHeaders headers = new FlightCallHeaders();
276-
headers.insert(
277-
"authorization",
278-
"Bearer " + new String(checkNotNull(token(), "token"), StandardCharsets.UTF_8));
279-
return new HeaderCallOption[] {new HeaderCallOption(headers)};
280-
}
281-
return new HeaderCallOption[0];
311+
CallOption[] callOptions() {
312+
return ArrowFlightIO.callOptions(token());
282313
}
283314

284315
@Override
@@ -547,15 +578,21 @@ public Write withBatchSize(int batchSize) {
547578

548579
/** Sets a bearer token for authentication. */
549580
public Write withToken(byte[] token) {
550-
return builder().setToken(token).build();
581+
return builder().setToken(copyToken(token)).build();
582+
}
583+
584+
CallOption[] callOptions() {
585+
return ArrowFlightIO.callOptions(token());
551586
}
552587

553588
@Override
554589
public PDone expand(PCollection<Row> input) {
555590
checkArgument(host() != null, "withHost() is required");
556591
checkArgument(descriptor() != null, "withDescriptor() is required");
592+
Schema inputSchema = checkNotNull(input.getSchema(), "input schema");
593+
validateWriteSchema(inputSchema);
557594

558-
input.apply(ParDo.of(new FlightWriteFn(this)));
595+
input.apply(ParDo.of(new FlightWriteFn(this, inputSchema)));
559596
return PDone.in(input.getPipeline());
560597
}
561598

@@ -579,16 +616,17 @@ static class FlightWriteFn extends DoFn<Row, Void> {
579616
Metrics.counter(ArrowFlightIO.class, "batchesWritten");
580617

581618
private final Write spec;
582-
private transient BufferAllocator allocator;
583-
private transient FlightClient client;
584-
private transient FlightClient.ClientStreamListener listener;
585-
private transient VectorSchemaRoot root;
619+
private final Schema beamSchema;
620+
private transient @Nullable BufferAllocator allocator;
621+
private transient @Nullable FlightClient client;
622+
private transient FlightClient.@Nullable ClientStreamListener listener;
623+
private transient @Nullable VectorSchemaRoot root;
586624
private transient org.apache.arrow.vector.types.pojo.Schema arrowSchema;
587625
private transient List<Row> batch;
588-
private transient @Nullable Schema beamSchema;
589626

590-
FlightWriteFn(Write spec) {
627+
FlightWriteFn(Write spec, Schema beamSchema) {
591628
this.spec = spec;
629+
this.beamSchema = beamSchema;
592630
}
593631

594632
@StartBundle
@@ -598,9 +636,9 @@ public void startBundle() {
598636

599637
@ProcessElement
600638
public void processElement(@Element Row row) {
601-
if (beamSchema == null) {
602-
beamSchema = row.getSchema();
603-
}
639+
checkArgument(
640+
row.getSchema().equivalent(beamSchema),
641+
"ArrowFlightIO.write() requires all rows to use the same schema.");
604642
batch.add(row);
605643
if (batch.size() >= spec.batchSize()) {
606644
flush();
@@ -609,30 +647,59 @@ public void processElement(@Element Row row) {
609647

610648
@FinishBundle
611649
public void finishBundle() {
612-
flush();
613-
closeConnection();
650+
RuntimeException failure = null;
651+
try {
652+
flush();
653+
} catch (RuntimeException e) {
654+
failure = e;
655+
}
656+
657+
try {
658+
closeConnection();
659+
} catch (RuntimeException e) {
660+
if (failure == null) {
661+
failure = e;
662+
} else {
663+
failure.addSuppressed(e);
664+
}
665+
}
666+
667+
if (failure != null) {
668+
throw failure;
669+
}
614670
}
615671

616672
@Teardown
617673
public void teardown() {
618-
closeConnection();
674+
try {
675+
closeConnection();
676+
} catch (RuntimeException e) {
677+
LOG.warn("Error closing Flight write connection during teardown", e);
678+
}
619679
}
620680

621-
@SuppressWarnings("nullness")
622681
private void ensureConnection() {
623682
if (client == null) {
624-
allocator = new RootAllocator(Long.MAX_VALUE);
625-
client = createClient(allocator, spec.host(), spec.port(), spec.useTls());
683+
BufferAllocator currentAllocator = new RootAllocator(Long.MAX_VALUE);
684+
allocator = currentAllocator;
685+
FlightClient currentClient =
686+
createClient(
687+
currentAllocator, checkNotNull(spec.host(), "host"), spec.port(), spec.useTls());
688+
client = currentClient;
626689

627690
List<Field> arrowFields = new ArrayList<>();
628691
for (Schema.Field beamField : beamSchema.getFields()) {
629692
arrowFields.add(toArrowField(beamField));
630693
}
631694
arrowSchema = new org.apache.arrow.vector.types.pojo.Schema(arrowFields);
632-
root = VectorSchemaRoot.create(arrowSchema, allocator);
633-
634-
FlightDescriptor descriptor = FlightDescriptor.path(spec.descriptor());
635-
listener = client.startPut(descriptor, root, new AsyncPutListener());
695+
VectorSchemaRoot currentRoot = VectorSchemaRoot.create(arrowSchema, currentAllocator);
696+
root = currentRoot;
697+
698+
FlightDescriptor descriptor =
699+
FlightDescriptor.path(checkNotNull(spec.descriptor(), "descriptor"));
700+
listener =
701+
currentClient.startPut(
702+
descriptor, currentRoot, new AsyncPutListener(), spec.callOptions());
636703
}
637704
}
638705

@@ -668,8 +735,8 @@ private ArrowType beamTypeToArrowType(Schema.FieldType beamType) {
668735
case DATETIME:
669736
return new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC");
670737
default:
671-
LOG.warn("Unsupported Beam type {}, falling back to Utf8", beamType.getTypeName());
672-
return ArrowType.Utf8.INSTANCE;
738+
throw new IllegalArgumentException(
739+
"Unsupported Beam type for ArrowFlightIO.write(): " + beamType.getTypeName());
673740
}
674741
}
675742

@@ -745,76 +812,70 @@ private void setVectorValue(
745812
((TimeStampMilliTZVector) vector).setSafe(index, millis);
746813
break;
747814
default:
748-
((VarCharVector) vector)
749-
.setSafe(index, value.toString().getBytes(StandardCharsets.UTF_8));
750-
break;
815+
throw new IllegalArgumentException(
816+
"Unsupported Beam type for ArrowFlightIO.write(): " + type.getTypeName());
751817
}
752818
}
753819

754-
@SuppressWarnings("nullness")
755820
private void closeConnection() {
821+
RuntimeException failure = null;
822+
FlightClient.ClientStreamListener currentListener = listener;
823+
listener = null;
756824
try {
757-
if (listener != null) {
758-
listener.completed();
759-
listener.getResult();
760-
listener = null;
825+
if (currentListener != null) {
826+
currentListener.completed();
827+
currentListener.getResult();
761828
}
762-
} catch (Exception e) {
763-
LOG.warn("Error completing Flight put", e);
829+
} catch (RuntimeException e) {
830+
failure = e;
764831
}
832+
833+
VectorSchemaRoot currentRoot = root;
834+
root = null;
765835
try {
766-
if (root != null) {
767-
root.close();
768-
root = null;
836+
if (currentRoot != null) {
837+
currentRoot.close();
769838
}
770839
} catch (Exception e) {
771-
LOG.warn("Error closing VectorSchemaRoot", e);
840+
if (failure == null) {
841+
failure = new RuntimeException("Error closing VectorSchemaRoot", e);
842+
} else {
843+
failure.addSuppressed(e);
844+
}
772845
}
846+
847+
FlightClient currentClient = client;
848+
client = null;
773849
try {
774-
if (client != null) {
775-
client.close();
776-
client = null;
850+
if (currentClient != null) {
851+
currentClient.close();
777852
}
778853
} catch (InterruptedException e) {
779854
Thread.currentThread().interrupt();
780-
LOG.warn("Interrupted closing FlightClient", e);
855+
RuntimeException closeFailure = new RuntimeException("Interrupted closing FlightClient", e);
856+
if (failure == null) {
857+
failure = closeFailure;
858+
} else {
859+
failure.addSuppressed(closeFailure);
860+
}
781861
}
862+
863+
BufferAllocator currentAllocator = allocator;
864+
allocator = null;
782865
try {
783-
if (allocator != null) {
784-
allocator.close();
785-
allocator = null;
866+
if (currentAllocator != null) {
867+
currentAllocator.close();
786868
}
787869
} catch (Exception e) {
788-
LOG.warn("Error closing BufferAllocator", e);
870+
if (failure == null) {
871+
failure = new RuntimeException("Error closing BufferAllocator", e);
872+
} else {
873+
failure.addSuppressed(e);
874+
}
789875
}
790-
}
791-
}
792-
793-
/** A no-op listener for async put operations. */
794-
static class AsyncPutListener implements FlightClient.PutListener {
795-
private volatile @Nullable Throwable error;
796-
797-
@Override
798-
public void onNext(PutResult val) {}
799876

800-
@Override
801-
public void onError(Throwable t) {
802-
this.error = t;
803-
}
804-
805-
@Override
806-
public void onCompleted() {}
807-
808-
@Override
809-
public boolean isCancelled() {
810-
return false;
811-
}
812-
813-
@Override
814-
public void getResult() {
815-
Throwable t = error;
816-
if (t != null) {
817-
throw new RuntimeException("Error during Flight put", t);
877+
if (failure != null) {
878+
throw failure;
818879
}
819880
}
820881
}

0 commit comments

Comments
 (0)