Skip to content

Commit 09f8172

Browse files
committed
[Dataflow Streaming] Enforce that get data requests for the same work item are not batched. Such batches are rejected by the backend so we should prevent them to avoid stuckness. Parallel requests for the same work item are unexpected but could be caused by bugs in the harness or by incorrect parallel state fetching from a single bundle.
1 parent 8566b2d commit 09f8172

4 files changed

Lines changed: 298 additions & 122 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: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ class GetDataPhysicalStreamHandler extends PhysicalStreamHandler {
193193
public void sendBatch(QueuedBatch batch) throws WindmillStreamShutdownException {
194194
// Synchronization of pending inserts is necessary with send to ensure duplicates are not
195195
// sent on stream reconnect.
196-
for (QueuedRequest request : batch.requestsReadOnly()) {
196+
for (QueuedRequest request : batch.requestsView()) {
197197
boolean alreadyPresent = pending.put(request.id(), request.getResponseStream()) != null;
198198
verify(!alreadyPresent, "Request already sent, id: %s", request.id());
199199
}
@@ -277,7 +277,7 @@ protected synchronized void onFlushPending(boolean isNewStream)
277277
}
278278
while (!batches.isEmpty()) {
279279
QueuedBatch batch = checkNotNull(batches.peekFirst());
280-
verify(!batch.isEmpty());
280+
verify(batch.requestsCount() > 0);
281281
if (!batch.isFinalized()) {
282282
break;
283283
}
@@ -481,17 +481,15 @@ private void queueRequestAndWait(QueuedRequest request)
481481

482482
batch = batches.isEmpty() ? null : batches.getLast();
483483
if (batch == null
484-
|| batch.isFinalized()
485-
|| batch.requestsCount() >= streamingRpcBatchLimit
486-
|| batch.byteSize() + request.byteSize() > AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) {
487-
if (batch != null) {
488-
prevBatch = batch;
489-
}
484+
|| !batch.tryAddRequest(
485+
request, streamingRpcBatchLimit, AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE)) {
486+
// We need a new batch.
487+
prevBatch = batch; // may be null
490488
batch = new QueuedBatch();
491489
batches.addLast(batch);
492490
responsibleForSend = true;
491+
verify(batch.tryAddRequest(request, Integer.MAX_VALUE, Long.MAX_VALUE));
493492
}
494-
batch.addRequest(request);
495493
}
496494
if (responsibleForSend) {
497495
if (prevBatch == null) {
@@ -531,7 +529,7 @@ private synchronized void trySendBatch(QueuedBatch batch) throws WindmillStreamS
531529
// an error and will
532530
// resend requests (possibly with new batching).
533531
verify(batch == batches.pollFirst());
534-
verify(!batch.isEmpty());
532+
verify(batch.requestsCount() > 0);
535533
currentGetDataPhysicalStream.sendBatch(batch);
536534
// Notify all waiters with requests in this batch as well as the sender
537535
// of the next batch (if one exists).

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

Lines changed: 104 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@
1919

2020
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList.toImmutableList;
2121

22-
import com.google.auto.value.AutoOneOf;
2322
import java.util.ArrayList;
2423
import java.util.Collections;
2524
import java.util.Comparator;
25+
import java.util.HashSet;
2626
import java.util.List;
2727
import java.util.concurrent.CountDownLatch;
28-
import java.util.stream.Stream;
28+
import javax.annotation.Nullable;
2929
import org.apache.beam.runners.dataflow.worker.windmill.Windmill;
30-
import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationGetDataRequest;
3130
import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataRequest;
3231
import org.apache.beam.runners.dataflow.worker.windmill.Windmill.KeyedGetDataRequest;
3332
import org.apache.beam.runners.dataflow.worker.windmill.client.WindmillStreamShutdownException;
33+
import org.apache.beam.sdk.util.Preconditions;
3434
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
3535
import org.slf4j.Logger;
3636
import org.slf4j.LoggerFactory;
@@ -46,15 +46,38 @@ private static String debugFormat(long value) {
4646
return String.format("%016x", value);
4747
}
4848

49+
static class ComputationAndKeyRequest {
50+
private final String computation;
51+
private final KeyedGetDataRequest request;
52+
53+
ComputationAndKeyRequest(String computation, KeyedGetDataRequest request) {
54+
this.computation = computation;
55+
this.request = request;
56+
}
57+
58+
String getComputation() {
59+
return computation;
60+
}
61+
62+
KeyedGetDataRequest getKeyedGetDataRequest() {
63+
return request;
64+
}
65+
}
66+
4967
static class QueuedRequest {
5068
private final long id;
51-
private final ComputationOrGlobalDataRequest dataRequest;
69+
private final @Nullable ComputationAndKeyRequest computationAndKeyRequest;
70+
private final @Nullable GlobalDataRequest globalDataRequest;
5271
private AppendableInputStream responseStream;
5372

5473
private QueuedRequest(
55-
long id, ComputationOrGlobalDataRequest dataRequest, long deadlineSeconds) {
74+
long id,
75+
@Nullable ComputationAndKeyRequest computationAndKeyRequest,
76+
@Nullable GlobalDataRequest globalDataRequest,
77+
long deadlineSeconds) {
5678
this.id = id;
57-
this.dataRequest = dataRequest;
79+
this.computationAndKeyRequest = computationAndKeyRequest;
80+
this.globalDataRequest = globalDataRequest;
5881
responseStream = new AppendableInputStream(deadlineSeconds);
5982
}
6083

@@ -63,27 +86,22 @@ static QueuedRequest forComputation(
6386
String computation,
6487
KeyedGetDataRequest keyedGetDataRequest,
6588
long deadlineSeconds) {
66-
ComputationGetDataRequest computationGetDataRequest =
67-
ComputationGetDataRequest.newBuilder()
68-
.setComputationId(computation)
69-
.addRequests(keyedGetDataRequest)
70-
.build();
7189
return new QueuedRequest(
7290
id,
73-
ComputationOrGlobalDataRequest.computation(computationGetDataRequest),
91+
new ComputationAndKeyRequest(computation, keyedGetDataRequest),
92+
null,
7493
deadlineSeconds);
7594
}
7695

7796
static QueuedRequest global(
7897
long id, GlobalDataRequest globalDataRequest, long deadlineSeconds) {
79-
return new QueuedRequest(
80-
id, ComputationOrGlobalDataRequest.global(globalDataRequest), deadlineSeconds);
98+
return new QueuedRequest(id, null, globalDataRequest, deadlineSeconds);
8199
}
82100

83101
static Comparator<QueuedRequest> globalRequestsFirst() {
84102
return (QueuedRequest r1, QueuedRequest r2) -> {
85-
boolean r1gd = r1.dataRequest.isGlobal();
86-
boolean r2gd = r2.dataRequest.isGlobal();
103+
boolean r1gd = r1.getKind() == Kind.GLOBAL;
104+
boolean r2gd = r2.getKind() == Kind.GLOBAL;
87105
return r1gd == r2gd ? 0 : (r1gd ? -1 : 1);
88106
};
89107
}
@@ -93,7 +111,13 @@ long id() {
93111
}
94112

95113
long byteSize() {
96-
return dataRequest.serializedSize();
114+
if (globalDataRequest != null) {
115+
return globalDataRequest.getSerializedSize();
116+
}
117+
Preconditions.checkStateNotNull(computationAndKeyRequest);
118+
return 10L
119+
+ computationAndKeyRequest.request.getSerializedSize()
120+
+ computationAndKeyRequest.getComputation().length();
97121
}
98122

99123
AppendableInputStream getResponseStream() {
@@ -104,22 +128,56 @@ void resetResponseStream() {
104128
this.responseStream = new AppendableInputStream(responseStream.getDeadlineSeconds());
105129
}
106130

107-
public ComputationOrGlobalDataRequest getDataRequest() {
108-
return dataRequest;
131+
enum Kind {
132+
COMPUTATION_AND_KEY_REQUEST,
133+
GLOBAL
134+
}
135+
136+
Kind getKind() {
137+
return computationAndKeyRequest != null ? Kind.COMPUTATION_AND_KEY_REQUEST : Kind.GLOBAL;
138+
}
139+
140+
ComputationAndKeyRequest getComputationAndKeyRequest() {
141+
return Preconditions.checkStateNotNull(computationAndKeyRequest);
142+
}
143+
144+
GlobalDataRequest getGlobalDataRequest() {
145+
return Preconditions.checkStateNotNull(globalDataRequest);
109146
}
110147

111148
void addToStreamingGetDataRequest(Windmill.StreamingGetDataRequest.Builder builder) {
112149
builder.addRequestId(id);
113-
if (dataRequest.isForComputation()) {
114-
builder.addStateRequest(dataRequest.computation());
115-
} else {
116-
builder.addGlobalDataRequest(dataRequest.global());
150+
switch (getKind()) {
151+
case COMPUTATION_AND_KEY_REQUEST:
152+
ComputationAndKeyRequest request = getComputationAndKeyRequest();
153+
builder
154+
.addStateRequestBuilder()
155+
.setComputationId(request.getComputation())
156+
.addRequests(request.request);
157+
break;
158+
case GLOBAL:
159+
builder.addGlobalDataRequest(getGlobalDataRequest());
160+
break;
117161
}
118162
}
119163

120164
@Override
121165
public final String toString() {
122-
return "QueuedRequest{" + "dataRequest=" + dataRequest + ", id=" + id + '}';
166+
StringBuilder result = new StringBuilder("QueuedRequest{id=").append(id).append(", ");
167+
if (getKind() == Kind.GLOBAL) {
168+
result.append("GetSideInput=").append(getGlobalDataRequest());
169+
} else {
170+
KeyedGetDataRequest key = getComputationAndKeyRequest().request;
171+
result
172+
.append("KeyedGetState=[shardingKey=")
173+
.append(debugFormat(key.getShardingKey()))
174+
.append("cacheToken=")
175+
.append(debugFormat(key.getCacheToken()))
176+
.append("workToken")
177+
.append(debugFormat(key.getWorkToken()))
178+
.append("]");
179+
}
180+
return result.append('}').toString();
123181
}
124182
}
125183

@@ -128,13 +186,14 @@ public final String toString() {
128186
*/
129187
static class QueuedBatch {
130188
private final List<QueuedRequest> requests = new ArrayList<>();
189+
private final HashSet<Long> workTokens = new HashSet<>();
131190
private final CountDownLatch sent = new CountDownLatch(1);
132191
private long byteSize = 0;
133192
private volatile boolean finalized = false;
134193
private volatile boolean failed = false;
135194

136195
/** Returns a read-only view of requests. */
137-
List<QueuedRequest> requestsReadOnly() {
196+
List<QueuedRequest> requestsView() {
138197
return Collections.unmodifiableList(requests);
139198
}
140199

@@ -155,18 +214,10 @@ Windmill.StreamingGetDataRequest asGetDataRequest() {
155214
return builder.build();
156215
}
157216

158-
boolean isEmpty() {
159-
return requests.isEmpty();
160-
}
161-
162217
int requestsCount() {
163218
return requests.size();
164219
}
165220

166-
long byteSize() {
167-
return byteSize;
168-
}
169-
170221
boolean isFinalized() {
171222
return finalized;
172223
}
@@ -176,9 +227,26 @@ void markFinalized() {
176227
}
177228

178229
/** Adds a request to the batch. */
179-
void addRequest(QueuedRequest request) {
230+
boolean tryAddRequest(QueuedRequest request, int countLimit, long byteLimit) {
231+
if (finalized) {
232+
return false;
233+
}
234+
if (requests.size() >= countLimit) {
235+
return false;
236+
}
237+
long estimatedBytes = request.byteSize();
238+
if (byteSize + estimatedBytes >= byteLimit) {
239+
return false;
240+
}
241+
242+
if (request.getKind() == QueuedRequest.Kind.COMPUTATION_AND_KEY_REQUEST
243+
&& !workTokens.add(request.getComputationAndKeyRequest().request.getWorkToken())) {
244+
return false;
245+
}
246+
// At this point we have added to work items so we must accept the item.
180247
requests.add(request);
181-
byteSize += request.byteSize();
248+
byteSize += estimatedBytes;
249+
return true;
182250
}
183251

184252
/**
@@ -227,75 +295,9 @@ void waitForSendOrFailNotification()
227295

228296
private ImmutableList<String> createStreamCancelledErrorMessages() {
229297
return requests.stream()
230-
.flatMap(
231-
request -> {
232-
switch (request.getDataRequest().getKind()) {
233-
case GLOBAL:
234-
return Stream.of("GetSideInput=" + request.getDataRequest().global());
235-
case COMPUTATION:
236-
return request.getDataRequest().computation().getRequestsList().stream()
237-
.map(
238-
keyedRequest ->
239-
"KeyedGetState=["
240-
+ "shardingKey="
241-
+ debugFormat(keyedRequest.getShardingKey())
242-
+ "cacheToken="
243-
+ debugFormat(keyedRequest.getCacheToken())
244-
+ "workToken"
245-
+ debugFormat(keyedRequest.getWorkToken())
246-
+ "]");
247-
default:
248-
// Will never happen switch is exhaustive.
249-
throw new IllegalStateException();
250-
}
251-
})
298+
.map(QueuedRequest::toString)
252299
.limit(STREAM_CANCELLED_ERROR_LOG_LIMIT)
253300
.collect(toImmutableList());
254301
}
255302
}
256-
257-
@AutoOneOf(ComputationOrGlobalDataRequest.Kind.class)
258-
abstract static class ComputationOrGlobalDataRequest {
259-
static ComputationOrGlobalDataRequest computation(
260-
ComputationGetDataRequest computationGetDataRequest) {
261-
return AutoOneOf_GrpcGetDataStreamRequests_ComputationOrGlobalDataRequest.computation(
262-
computationGetDataRequest);
263-
}
264-
265-
static ComputationOrGlobalDataRequest global(GlobalDataRequest globalDataRequest) {
266-
return AutoOneOf_GrpcGetDataStreamRequests_ComputationOrGlobalDataRequest.global(
267-
globalDataRequest);
268-
}
269-
270-
abstract Kind getKind();
271-
272-
abstract ComputationGetDataRequest computation();
273-
274-
abstract GlobalDataRequest global();
275-
276-
boolean isGlobal() {
277-
return getKind() == Kind.GLOBAL;
278-
}
279-
280-
boolean isForComputation() {
281-
return getKind() == Kind.COMPUTATION;
282-
}
283-
284-
long serializedSize() {
285-
switch (getKind()) {
286-
case GLOBAL:
287-
return global().getSerializedSize();
288-
case COMPUTATION:
289-
return computation().getSerializedSize();
290-
// this will never happen since the switch is exhaustive.
291-
default:
292-
throw new UnsupportedOperationException("unknown dataRequest type.");
293-
}
294-
}
295-
296-
enum Kind {
297-
COMPUTATION,
298-
GLOBAL
299-
}
300-
}
301303
}

0 commit comments

Comments
 (0)