Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit f3865b0

Browse files
authored
feat: in StreamWriterV2, supports new append, which takes rows and offset (#894)
* feat: StreamWriterV2 will attach stream name in the first request in the connection and remove stream name and schema in the following ones. * feat: StreamWriterV2 will attach stream name in the first request in the connection and remove stream name and schema in the following ones * feat: In StreamWriterV2, add a new append method accepting rows and offset * remove duplicated code * add unit test for append rows without schema
1 parent 74d20b2 commit f3865b0

File tree

2 files changed

+153
-39
lines changed

2 files changed

+153
-39
lines changed

google-cloud-bigquerystorage/src/main/java/com/google/cloud/bigquery/storage/v1beta2/StreamWriterV2.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import com.google.api.core.SettableApiFuture;
2020
import com.google.api.gax.core.CredentialsProvider;
2121
import com.google.api.gax.rpc.TransportChannelProvider;
22+
import com.google.cloud.bigquery.storage.v1beta2.AppendRowsRequest.ProtoData;
2223
import com.google.cloud.bigquery.storage.v1beta2.StreamConnection.DoneCallback;
2324
import com.google.cloud.bigquery.storage.v1beta2.StreamConnection.RequestCallback;
2425
import com.google.common.base.Preconditions;
2526
import com.google.common.util.concurrent.Uninterruptibles;
27+
import com.google.protobuf.Int64Value;
2628
import io.grpc.Status;
2729
import io.grpc.Status.Code;
2830
import io.grpc.StatusRuntimeException;
@@ -39,8 +41,6 @@
3941
/**
4042
* A BigQuery Stream Writer that can be used to write data into BigQuery Table.
4143
*
42-
* <p>TODO: Attach schema.
43-
*
4444
* <p>TODO: Attach traceId.
4545
*
4646
* <p>TODO: Support batching.
@@ -59,6 +59,11 @@ public class StreamWriterV2 implements AutoCloseable {
5959
*/
6060
private final String streamName;
6161

62+
/*
63+
* The proto schema of rows to write.
64+
*/
65+
private final ProtoSchema writerSchema;
66+
6267
/*
6368
* Max allowed inflight requests in the stream. Method append is blocked at this.
6469
*/
@@ -135,6 +140,7 @@ private StreamWriterV2(Builder builder) throws IOException {
135140
this.hasMessageInWaitingQueue = lock.newCondition();
136141
this.inflightReduced = lock.newCondition();
137142
this.streamName = builder.streamName;
143+
this.writerSchema = builder.writerSchema;
138144
this.maxInflightRequests = builder.maxInflightRequest;
139145
this.maxInflightBytes = builder.maxInflightBytes;
140146
this.waitingRequestQueue = new LinkedList<AppendRequestAndResponse>();
@@ -188,10 +194,52 @@ public void run() {
188194
* ApiFuture<AppendRowsResponse> messageIdFuture = writer.append(message);
189195
* ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<AppendRowsResponse>() {
190196
* public void onSuccess(AppendRowsResponse response) {
191-
* if (response.hasOffset()) {
192-
* System.out.println("written with offset: " + response.getOffset());
197+
* if (!response.hasError()) {
198+
* System.out.println("written with offset: " + response.getAppendResult().getOffset());
199+
* } else {
200+
* System.out.println("received an in stream error: " + response.getError().toString());
201+
* }
202+
* }
203+
*
204+
* public void onFailure(Throwable t) {
205+
* System.out.println("failed to write: " + t);
206+
* }
207+
* }, MoreExecutors.directExecutor());
208+
* }</pre>
209+
*
210+
* @param rows the rows in serialized format to write to BigQuery.
211+
* @param offset the offset of the first row.
212+
* @return the append response wrapped in a future.
213+
*/
214+
public ApiFuture<AppendRowsResponse> append(ProtoRows rows, long offset) {
215+
// TODO: Move this check to builder after the other append is removed.
216+
if (this.writerSchema == null) {
217+
throw new StatusRuntimeException(
218+
Status.fromCode(Code.INVALID_ARGUMENT)
219+
.withDescription("Writer schema must be provided when building this writer."));
220+
}
221+
AppendRowsRequest.Builder requestBuilder = AppendRowsRequest.newBuilder();
222+
requestBuilder.setProtoRows(ProtoData.newBuilder().setRows(rows).build());
223+
if (offset >= 0) {
224+
requestBuilder.setOffset(Int64Value.of(offset));
225+
}
226+
return append(requestBuilder.build());
227+
}
228+
229+
/**
230+
* Schedules the writing of a message.
231+
*
232+
* <p>Example of writing a message.
233+
*
234+
* <pre>{@code
235+
* AppendRowsRequest message;
236+
* ApiFuture<AppendRowsResponse> messageIdFuture = writer.append(message);
237+
* ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<AppendRowsResponse>() {
238+
* public void onSuccess(AppendRowsResponse response) {
239+
* if (!response.hasError()) {
240+
* System.out.println("written with offset: " + response.getAppendResult().getOffset());
193241
* } else {
194-
* System.out.println("received an in stream error: " + response.error().toString());
242+
* System.out.println("received an in stream error: " + response.getError().toString());
195243
* }
196244
* }
197245
*
@@ -202,8 +250,9 @@ public void run() {
202250
* }</pre>
203251
*
204252
* @param message the message in serialized format to write to BigQuery.
205-
* @return the message ID wrapped in a future.
253+
* @return the append response wrapped in a future.
206254
*/
255+
@Deprecated
207256
public ApiFuture<AppendRowsResponse> append(AppendRowsRequest message) {
208257
AppendRequestAndResponse requestWrapper = new AppendRequestAndResponse(message);
209258
if (requestWrapper.messageSize > getApiMaxRequestBytes()) {
@@ -380,6 +429,9 @@ private AppendRowsRequest prepareRequestBasedOnPosition(
380429
AppendRowsRequest original, boolean isFirstRequest) {
381430
AppendRowsRequest.Builder requestBuilder = original.toBuilder();
382431
if (isFirstRequest) {
432+
if (this.writerSchema != null) {
433+
requestBuilder.getProtoRowsBuilder().setWriterSchema(this.writerSchema);
434+
}
383435
requestBuilder.setWriteStream(this.streamName);
384436
} else {
385437
requestBuilder.clearWriteStream();
@@ -473,6 +525,8 @@ public static final class Builder {
473525

474526
private BigQueryWriteClient client;
475527

528+
private ProtoSchema writerSchema = null;
529+
476530
private long maxInflightRequest = DEFAULT_MAX_INFLIGHT_REQUESTS;
477531

478532
private long maxInflightBytes = DEFAULT_MAX_INFLIGHT_BYTES;
@@ -495,6 +549,12 @@ private Builder(String streamName, BigQueryWriteClient client) {
495549
this.client = Preconditions.checkNotNull(client);
496550
}
497551

552+
/** Sets the proto schema of the rows. */
553+
public Builder setWriterSchema(ProtoSchema writerSchema) {
554+
this.writerSchema = writerSchema;
555+
return this;
556+
}
557+
498558
public Builder setMaxInflightRequests(long value) {
499559
this.maxInflightRequest = value;
500560
return this;

google-cloud-bigquerystorage/src/test/java/com/google/cloud/bigquery/storage/v1beta2/StreamWriterV2Test.java

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -87,31 +87,39 @@ private StreamWriterV2 getTestStreamWriterV2() throws IOException {
8787
return StreamWriterV2.newBuilder(TEST_STREAM, client).build();
8888
}
8989

90-
private AppendRowsRequest createAppendRequest(String[] messages, long offset) {
91-
AppendRowsRequest.Builder requestBuilder = AppendRowsRequest.newBuilder();
92-
AppendRowsRequest.ProtoData.Builder dataBuilder = AppendRowsRequest.ProtoData.newBuilder();
93-
dataBuilder.setWriterSchema(
94-
ProtoSchema.newBuilder()
95-
.setProtoDescriptor(
96-
DescriptorProtos.DescriptorProto.newBuilder()
97-
.setName("Message")
98-
.addField(
99-
DescriptorProtos.FieldDescriptorProto.newBuilder()
100-
.setName("foo")
101-
.setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_STRING)
102-
.setNumber(1)
103-
.build())
104-
.build()));
105-
ProtoRows.Builder rows = ProtoRows.newBuilder();
90+
private ProtoSchema createProtoSchema() {
91+
return ProtoSchema.newBuilder()
92+
.setProtoDescriptor(
93+
DescriptorProtos.DescriptorProto.newBuilder()
94+
.setName("Message")
95+
.addField(
96+
DescriptorProtos.FieldDescriptorProto.newBuilder()
97+
.setName("foo")
98+
.setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_STRING)
99+
.setNumber(1)
100+
.build())
101+
.build())
102+
.build();
103+
}
104+
105+
private ProtoRows createProtoRows(String[] messages) {
106+
ProtoRows.Builder rowsBuilder = ProtoRows.newBuilder();
106107
for (String message : messages) {
107108
FooType foo = FooType.newBuilder().setFoo(message).build();
108-
rows.addSerializedRows(foo.toByteString());
109+
rowsBuilder.addSerializedRows(foo.toByteString());
109110
}
111+
return rowsBuilder.build();
112+
}
113+
114+
private AppendRowsRequest createAppendRequest(String[] messages, long offset) {
115+
AppendRowsRequest.Builder requestBuilder = AppendRowsRequest.newBuilder();
116+
AppendRowsRequest.ProtoData.Builder dataBuilder = AppendRowsRequest.ProtoData.newBuilder();
117+
dataBuilder.setWriterSchema(createProtoSchema());
110118
if (offset > 0) {
111119
requestBuilder.setOffset(Int64Value.of(offset));
112120
}
113121
return requestBuilder
114-
.setProtoRows(dataBuilder.setRows(rows.build()).build())
122+
.setProtoRows(dataBuilder.setRows(createProtoRows(messages)).build())
115123
.setWriteStream(TEST_STREAM)
116124
.build();
117125
}
@@ -166,6 +174,24 @@ public void run() {
166174
appendThread.interrupt();
167175
}
168176

177+
private void verifyAppendRequests(long appendCount) {
178+
assertEquals(appendCount, testBigQueryWrite.getAppendRequests().size());
179+
for (int i = 0; i < appendCount; i++) {
180+
AppendRowsRequest serverRequest = testBigQueryWrite.getAppendRequests().get(i);
181+
assertTrue(serverRequest.getProtoRows().getRows().getSerializedRowsCount() > 0);
182+
assertEquals(i, serverRequest.getOffset().getValue());
183+
if (i == 0) {
184+
// First request received by server should have schema and stream name.
185+
assertTrue(serverRequest.getProtoRows().hasWriterSchema());
186+
assertEquals(serverRequest.getWriteStream(), TEST_STREAM);
187+
} else {
188+
// Following request should not have schema and stream name.
189+
assertFalse(serverRequest.getProtoRows().hasWriterSchema());
190+
assertEquals(serverRequest.getWriteStream(), "");
191+
}
192+
}
193+
}
194+
169195
@Test
170196
public void testBuildBigQueryWriteClientInWriter() throws Exception {
171197
StreamWriterV2 writer =
@@ -181,40 +207,68 @@ public void testBuildBigQueryWriteClientInWriter() throws Exception {
181207
}
182208

183209
@Test
184-
public void testAppendSuccess() throws Exception {
185-
StreamWriterV2 writer = getTestStreamWriterV2();
210+
public void testAppendWithRowsSuccess() throws Exception {
211+
StreamWriterV2 writer =
212+
StreamWriterV2.newBuilder(TEST_STREAM, client).setWriterSchema(createProtoSchema()).build();
186213

187-
long appendCount = 1000;
214+
long appendCount = 100;
188215
for (int i = 0; i < appendCount; i++) {
189216
testBigQueryWrite.addResponse(createAppendResponse(i));
190217
}
191218

192219
List<ApiFuture<AppendRowsResponse>> futures = new ArrayList<>();
193220
for (int i = 0; i < appendCount; i++) {
194-
futures.add(sendTestMessage(writer, new String[] {String.valueOf(i)}));
221+
futures.add(writer.append(createProtoRows(new String[] {String.valueOf(i)}), i));
195222
}
196223

197224
for (int i = 0; i < appendCount; i++) {
198225
assertEquals(i, futures.get(i).get().getAppendResult().getOffset().getValue());
199226
}
200-
assertEquals(appendCount, testBigQueryWrite.getAppendRequests().size());
201227

228+
verifyAppendRequests(appendCount);
229+
230+
writer.close();
231+
}
232+
233+
@Test
234+
public void testAppendWithMessageSuccess() throws Exception {
235+
StreamWriterV2 writer = getTestStreamWriterV2();
236+
237+
long appendCount = 1000;
202238
for (int i = 0; i < appendCount; i++) {
203-
AppendRowsRequest serverRequest = testBigQueryWrite.getAppendRequests().get(i);
204-
if (i == 0) {
205-
// First request received by server should have schema and stream name.
206-
assertTrue(serverRequest.getProtoRows().hasWriterSchema());
207-
assertEquals(serverRequest.getWriteStream(), TEST_STREAM);
208-
} else {
209-
// Following request should not have schema and stream name.
210-
assertFalse(serverRequest.getProtoRows().hasWriterSchema());
211-
assertEquals(serverRequest.getWriteStream(), "");
212-
}
239+
testBigQueryWrite.addResponse(createAppendResponse(i));
213240
}
214241

242+
List<ApiFuture<AppendRowsResponse>> futures = new ArrayList<>();
243+
for (int i = 0; i < appendCount; i++) {
244+
futures.add(writer.append(createAppendRequest(new String[] {String.valueOf(i)}, i)));
245+
}
246+
247+
for (int i = 0; i < appendCount; i++) {
248+
assertEquals(i, futures.get(i).get().getAppendResult().getOffset().getValue());
249+
}
250+
251+
verifyAppendRequests(appendCount);
252+
215253
writer.close();
216254
}
217255

256+
@Test
257+
public void testAppendWithRowsNoSchema() throws Exception {
258+
final StreamWriterV2 writer = getTestStreamWriterV2();
259+
StatusRuntimeException ex =
260+
assertThrows(
261+
StatusRuntimeException.class,
262+
new ThrowingRunnable() {
263+
@Override
264+
public void run() throws Throwable {
265+
writer.append(createProtoRows(new String[] {"A"}), -1);
266+
}
267+
});
268+
assertEquals(ex.getStatus().getCode(), Status.INVALID_ARGUMENT.getCode());
269+
assertTrue(ex.getStatus().getDescription().contains("Writer schema must be provided"));
270+
}
271+
218272
@Test
219273
public void testAppendSuccessAndConnectionError() throws Exception {
220274
StreamWriterV2 writer = getTestStreamWriterV2();

0 commit comments

Comments
 (0)