Skip to content

Commit 854c81e

Browse files
authored
feat: Support inflight control in StreamWriterV2 (#875)
* Add a new StreamWriterV2. Compared to existing StreamWriter, its locking mechanism is much simpler. * Stop using Java8 features as we still need to support Java7 * Do not hold lock while sending requests, and some minor refactoring. * Support inflight control with blocking behavior as desired by dataflow sink. * Verify request ordering in unit test * Move inflight quota wait to separate method.
1 parent 0261af4 commit 854c81e

File tree

2 files changed

+158
-4
lines changed

2 files changed

+158
-4
lines changed

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

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
*
4141
* <p>TODO: Attach schema.
4242
*
43-
* <p>TODO: Add inflight control.
44-
*
4543
* <p>TODO: Attach traceId.
4644
*
4745
* <p>TODO: Support batching.
@@ -53,12 +51,35 @@ public class StreamWriterV2 implements AutoCloseable {
5351

5452
private Lock lock;
5553
private Condition hasMessageInWaitingQueue;
54+
private Condition inflightReduced;
5655

5756
/*
5857
* The identifier of stream to write to.
5958
*/
6059
private final String streamName;
6160

61+
/*
62+
* Max allowed inflight requests in the stream. Method append is blocked at this.
63+
*/
64+
private final long maxInflightRequests;
65+
66+
/*
67+
* Max allowed inflight bytes in the stream. Method append is blocked at this.
68+
*/
69+
private final long maxInflightBytes;
70+
71+
/*
72+
* Tracks current inflight requests in the stream.
73+
*/
74+
@GuardedBy("lock")
75+
private long inflightRequests = 0;
76+
77+
/*
78+
* Tracks current inflight bytes in the stream.
79+
*/
80+
@GuardedBy("lock")
81+
private long inflightBytes = 0;
82+
6283
/*
6384
* Indicates whether user has called Close() or not.
6485
*/
@@ -101,7 +122,10 @@ public static long getApiMaxRequestBytes() {
101122
private StreamWriterV2(Builder builder) {
102123
this.lock = new ReentrantLock();
103124
this.hasMessageInWaitingQueue = lock.newCondition();
125+
this.inflightReduced = lock.newCondition();
104126
this.streamName = builder.streamName;
127+
this.maxInflightRequests = builder.maxInflightRequest;
128+
this.maxInflightBytes = builder.maxInflightBytes;
105129
this.waitingRequestQueue = new LinkedList<AppendRequestAndResponse>();
106130
this.inflightRequestQueue = new LinkedList<AppendRequestAndResponse>();
107131
this.streamConnection =
@@ -186,14 +210,38 @@ public ApiFuture<AppendRowsResponse> append(AppendRowsRequest message) {
186210
"Stream is closed due to " + connectionFinalStatus.toString())));
187211
return requestWrapper.appendResult;
188212
}
213+
214+
++this.inflightRequests;
215+
this.inflightBytes += requestWrapper.messageSize;
189216
waitingRequestQueue.addLast(requestWrapper);
190217
hasMessageInWaitingQueue.signal();
218+
maybeWaitForInflightQuota();
191219
return requestWrapper.appendResult;
192220
} finally {
193221
this.lock.unlock();
194222
}
195223
}
196224

225+
@GuardedBy("lock")
226+
private void maybeWaitForInflightQuota() {
227+
while (this.inflightRequests >= this.maxInflightRequests
228+
|| this.inflightBytes >= this.maxInflightBytes) {
229+
try {
230+
inflightReduced.await(100, TimeUnit.MILLISECONDS);
231+
} catch (InterruptedException e) {
232+
log.warning(
233+
"Interrupted while waiting for inflight quota. Stream: "
234+
+ streamName
235+
+ " Error: "
236+
+ e.toString());
237+
throw new StatusRuntimeException(
238+
Status.fromCode(Code.CANCELLED)
239+
.withCause(e)
240+
.withDescription("Interrupted while waiting for quota."));
241+
}
242+
}
243+
}
244+
197245
/** Close the stream writer. Shut down all resources. */
198246
@Override
199247
public void close() {
@@ -303,7 +351,7 @@ private void cleanupInflightRequests() {
303351
try {
304352
finalStatus = this.connectionFinalStatus;
305353
while (!this.inflightRequestQueue.isEmpty()) {
306-
localQueue.addLast(this.inflightRequestQueue.pollFirst());
354+
localQueue.addLast(pollInflightRequestQueue());
307355
}
308356
} finally {
309357
this.lock.unlock();
@@ -322,7 +370,7 @@ private void requestCallback(AppendRowsResponse response) {
322370
AppendRequestAndResponse requestWrapper;
323371
this.lock.lock();
324372
try {
325-
requestWrapper = this.inflightRequestQueue.pollFirst();
373+
requestWrapper = pollInflightRequestQueue();
326374
} finally {
327375
this.lock.unlock();
328376
}
@@ -343,6 +391,15 @@ private void doneCallback(Throwable finalStatus) {
343391
}
344392
}
345393

394+
@GuardedBy("lock")
395+
private AppendRequestAndResponse pollInflightRequestQueue() {
396+
AppendRequestAndResponse requestWrapper = this.inflightRequestQueue.pollFirst();
397+
--this.inflightRequests;
398+
this.inflightBytes -= requestWrapper.messageSize;
399+
this.inflightReduced.signal();
400+
return requestWrapper;
401+
}
402+
346403
/** Constructs a new {@link StreamWriterV2.Builder} using the given stream and client. */
347404
public static StreamWriterV2.Builder newBuilder(String streamName, BigQueryWriteClient client) {
348405
return new StreamWriterV2.Builder(streamName, client);
@@ -351,15 +408,33 @@ public static StreamWriterV2.Builder newBuilder(String streamName, BigQueryWrite
351408
/** A builder of {@link StreamWriterV2}s. */
352409
public static final class Builder {
353410

411+
private static final long DEFAULT_MAX_INFLIGHT_REQUESTS = 1000L;
412+
413+
private static final long DEFAULT_MAX_INFLIGHT_BYTES = 100 * 1024 * 1024; // 100Mb.
414+
354415
private String streamName;
355416

356417
private BigQueryWriteClient client;
357418

419+
private long maxInflightRequest = DEFAULT_MAX_INFLIGHT_REQUESTS;
420+
421+
private long maxInflightBytes = DEFAULT_MAX_INFLIGHT_BYTES;
422+
358423
private Builder(String streamName, BigQueryWriteClient client) {
359424
this.streamName = Preconditions.checkNotNull(streamName);
360425
this.client = Preconditions.checkNotNull(client);
361426
}
362427

428+
public Builder setMaxInflightRequests(long value) {
429+
this.maxInflightRequest = value;
430+
return this;
431+
}
432+
433+
public Builder setMaxInflightBytes(long value) {
434+
this.maxInflightBytes = value;
435+
return this;
436+
}
437+
363438
/** Builds the {@code StreamWriterV2}. */
364439
public StreamWriterV2 build() {
365440
return new StreamWriterV2(this);

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@ public void run() throws Throwable {
142142
});
143143
}
144144

145+
private void verifyAppendIsBlocked(final StreamWriterV2 writer) throws Exception {
146+
Thread appendThread =
147+
new Thread(
148+
new Runnable() {
149+
@Override
150+
public void run() {
151+
sendTestMessage(writer, new String[] {"A"});
152+
}
153+
});
154+
// Start a separate thread to append and verify that it is still alive after 2 seoncds.
155+
appendThread.start();
156+
TimeUnit.SECONDS.sleep(2);
157+
assertTrue(appendThread.isAlive());
158+
appendThread.interrupt();
159+
}
160+
145161
@Test
146162
public void testAppendSuccess() throws Exception {
147163
StreamWriterV2 writer = getTestStreamWriterV2();
@@ -291,6 +307,69 @@ public void serverCloseWhileRequestsInflight() throws Exception {
291307
writer.close();
292308
}
293309

310+
@Test
311+
public void testZeroMaxInflightRequests() throws Exception {
312+
StreamWriterV2 writer =
313+
StreamWriterV2.newBuilder(TEST_STREAM, client).setMaxInflightRequests(0).build();
314+
testBigQueryWrite.addResponse(createAppendResponse(0));
315+
verifyAppendIsBlocked(writer);
316+
writer.close();
317+
}
318+
319+
@Test
320+
public void testZeroMaxInflightBytes() throws Exception {
321+
StreamWriterV2 writer =
322+
StreamWriterV2.newBuilder(TEST_STREAM, client).setMaxInflightBytes(0).build();
323+
testBigQueryWrite.addResponse(createAppendResponse(0));
324+
verifyAppendIsBlocked(writer);
325+
writer.close();
326+
}
327+
328+
@Test
329+
public void testOneMaxInflightRequests() throws Exception {
330+
StreamWriterV2 writer =
331+
StreamWriterV2.newBuilder(TEST_STREAM, client).setMaxInflightRequests(1).build();
332+
// Server will sleep 1 second before every response.
333+
testBigQueryWrite.setResponseSleep(Duration.ofSeconds(1));
334+
testBigQueryWrite.addResponse(createAppendResponse(0));
335+
336+
long appendStartTimeMs = System.currentTimeMillis();
337+
ApiFuture<AppendRowsResponse> appendFuture1 = sendTestMessage(writer, new String[] {"A"});
338+
long appendElapsedMs = System.currentTimeMillis() - appendStartTimeMs;
339+
assertTrue(appendElapsedMs >= 1000);
340+
assertEquals(0, appendFuture1.get().getAppendResult().getOffset().getValue());
341+
writer.close();
342+
}
343+
344+
@Test
345+
public void testAppendsWithTinyMaxInflightBytes() throws Exception {
346+
StreamWriterV2 writer =
347+
StreamWriterV2.newBuilder(TEST_STREAM, client).setMaxInflightBytes(1).build();
348+
// Server will sleep 100ms before every response.
349+
testBigQueryWrite.setResponseSleep(Duration.ofMillis(100));
350+
long appendCount = 10;
351+
for (int i = 0; i < appendCount; i++) {
352+
testBigQueryWrite.addResponse(createAppendResponse(i));
353+
}
354+
355+
List<ApiFuture<AppendRowsResponse>> futures = new ArrayList<>();
356+
long appendStartTimeMs = System.currentTimeMillis();
357+
for (int i = 0; i < appendCount; i++) {
358+
futures.add(writer.append(createAppendRequest(new String[] {String.valueOf(i)}, i)));
359+
}
360+
long appendElapsedMs = System.currentTimeMillis() - appendStartTimeMs;
361+
assertTrue(appendElapsedMs >= 1000);
362+
363+
for (int i = 0; i < appendCount; i++) {
364+
assertEquals(i, futures.get(i).get().getAppendResult().getOffset().getValue());
365+
}
366+
assertEquals(appendCount, testBigQueryWrite.getAppendRequests().size());
367+
for (int i = 0; i < appendCount; i++) {
368+
assertEquals(i, testBigQueryWrite.getAppendRequests().get(i).getOffset().getValue());
369+
}
370+
writer.close();
371+
}
372+
294373
@Test
295374
public void testMessageTooLarge() {
296375
StreamWriterV2 writer = getTestStreamWriterV2();

0 commit comments

Comments
 (0)