Skip to content

Commit 1282e6c

Browse files
authored
[Dataflow Streaming] Fix race in GetDataStream that could leave get data requests orphaned on stream errors after half-closing. (#36401)
This caused test flakiness of GetDataStreamTest.testRequestKeyedData_reconnectOnStreamErrorAfterHalfClose Fixes #36347
1 parent 7542aea commit 1282e6c

2 files changed

Lines changed: 27 additions & 13 deletions

File tree

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcGetDataStream.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ final class GrpcGetDataStream
9191
@GuardedBy("this")
9292
private final Deque<QueuedBatch> batches;
9393

94-
private final Supplier<Integer> batchesDebugSizeSupplier;
94+
// Size of the batches that may be read without synchronization. If it is under synchronized
95+
// block it is guaranteed to be correct.
96+
private final Supplier<Integer> batchesSizeSupplier;
9597

9698
private final AtomicLong idGenerator;
9799
private final JobHeader jobHeader;
@@ -133,7 +135,7 @@ private GrpcGetDataStream(
133135
// Otherwise the deque is accessed via batches which has a guardedby annotation.
134136
ConcurrentLinkedDeque<QueuedBatch> batches = new ConcurrentLinkedDeque<>();
135137
this.batches = batches;
136-
this.batchesDebugSizeSupplier = batches::size;
138+
this.batchesSizeSupplier = batches::size;
137139
this.sendKeyedGetDataRequests = sendKeyedGetDataRequests;
138140
this.processHeartbeatResponses = processHeartbeatResponses;
139141
}
@@ -224,7 +226,7 @@ public void onResponse(StreamingGetDataResponse chunk) {
224226

225227
@Override
226228
public boolean hasPendingRequests() {
227-
return !pending.isEmpty();
229+
return !pending.isEmpty() || batchesSizeSupplier.get() > 0;
228230
}
229231

230232
@Override
@@ -276,7 +278,9 @@ protected synchronized void onFlushPending(boolean isNewStream)
276278
while (!batches.isEmpty()) {
277279
QueuedBatch batch = checkNotNull(batches.peekFirst());
278280
verify(!batch.isEmpty());
279-
if (!batch.isFinalized()) break;
281+
if (!batch.isFinalized()) {
282+
break;
283+
}
280284
try {
281285
verify(
282286
batch == batches.pollFirst(),
@@ -419,7 +423,7 @@ protected synchronized void shutdownInternal() {
419423

420424
@Override
421425
public void appendSpecificHtml(PrintWriter writer) {
422-
int batches = batchesDebugSizeSupplier.get();
426+
int batches = batchesSizeSupplier.get();
423427
if (batches > 0) {
424428
writer.format("GetDataStream: %d queued batches ", batches);
425429
} else {
@@ -516,7 +520,7 @@ private synchronized void trySendBatch(QueuedBatch batch) throws WindmillStreamS
516520
}
517521
final @Nullable GetDataPhysicalStreamHandler currentGetDataPhysicalStream =
518522
(GetDataPhysicalStreamHandler) currentPhysicalStream;
519-
if (currentGetDataPhysicalStream == null) {
523+
if (currentGetDataPhysicalStream == null || clientClosed) {
520524
// Leave the batch finalized but in the batches queue. Finalized batches will be sent on a
521525
// new stream in onFlushPending.
522526
return;

runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcGetDataStreamTest.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -316,16 +316,26 @@ public void testRequestKeyedData_reconnectOnStreamErrorAfterHalfClose()
316316
assertNull(streamInfo.onDone.get());
317317

318318
// Simulate an error on the grpc stream, this should trigger retrying the requests on a new
319-
// stream
320-
// which is half-closed.
319+
// stream which is half-closed.
321320
streamInfo.responseObserver.onError(new IOException("test error"));
322321

323-
FakeWindmillGrpcService.GetDataStreamInfo streamInfo2 = waitForConnectionAndConsumeHeader();
324-
Windmill.StreamingGetDataRequest request2 = streamInfo2.requests.take();
325-
assertThat(request2.getRequestIdList()).containsExactly(1L);
326-
assertEquals(keyedGetDataRequest, request2.getStateRequest(0).getRequests(0));
327-
assertNull(streamInfo2.onDone.get());
328322
Windmill.KeyedGetDataResponse keyedGetDataResponse = createTestResponse(1);
323+
FakeWindmillGrpcService.GetDataStreamInfo streamInfo2;
324+
while (true) {
325+
streamInfo2 = waitForConnectionAndConsumeHeader();
326+
streamInfo2.onDone.get();
327+
Windmill.StreamingGetDataRequest request2 = streamInfo2.requests.poll(5, TimeUnit.SECONDS);
328+
if (request2 == null) {
329+
// Client half-closed but didn't send the request, this can happen due to race but
330+
// should recover by resending stream with requests.
331+
streamInfo2.responseObserver.onCompleted();
332+
continue;
333+
}
334+
assertThat(request2.getRequestIdList()).containsExactly(1L);
335+
assertEquals(keyedGetDataRequest, request2.getStateRequest(0).getRequests(0));
336+
break;
337+
}
338+
329339
streamInfo2.responseObserver.onNext(
330340
Windmill.StreamingGetDataResponse.newBuilder()
331341
.addRequestId(1)

0 commit comments

Comments
 (0)