Skip to content

Commit a7c3d51

Browse files
committed
[FnApi Java] Add support for separate named data streams to provide bundle isolation.
This is advertised to the runner via a new NAMED_DATA_STREAMS protocol capability. The runner is then free to assign bundles to named data streams as it chooses to isolate bundle processing from each other. Instead of single data stream from the sdk, the sdk will create a data stream for each name. The benefit of doing so is that the multiplexing currently performed on data stream messages being received allows a slow bundle to fill up buffers and block the shared stream. With separate named streams, bundles on other data streams have separate grpc flow control from the blocked stream and are not affected.
1 parent b3a62ee commit a7c3d51

19 files changed

Lines changed: 377 additions & 266 deletions

File tree

model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ message RemoteGrpcPort {
120120
service BeamFnControl {
121121
// Instructions sent by the runner to the SDK requesting different types
122122
// of work.
123+
//
124+
// Header metadata has the specified keys pairs:
125+
// - "worker_id": the id of the sdk
123126
rpc Control(
124127
// A stream of responses to instructions the SDK was asked to be
125128
// performed.
@@ -130,6 +133,9 @@ service BeamFnControl {
130133

131134
// Used to get the full process bundle descriptors for bundles one
132135
// is asked to process.
136+
//
137+
// Header metadata has the specified keys pairs:
138+
// - "worker_id": the id of the sdk
133139
rpc GetProcessBundleDescriptor(GetProcessBundleDescriptorRequest) returns (
134140
ProcessBundleDescriptor) {}
135141
}
@@ -416,14 +422,21 @@ message ProcessBundleRequest {
416422
// at https://s.apache.org/beam-fn-api-control-data-embedding.
417423
Elements elements = 3;
418424

419-
// indicates that the runner has no stare for the keys in this bundle
425+
// Indicates that the runner has no state for the keys in this bundle
420426
// so SDk can safely begin stateful processing with a locally-generated
421-
// initial empty state
427+
// initial empty state.
422428
bool has_no_state = 4;
423429

424-
// indicates that the runner will never process another bundle for the keys
430+
// Indicates that the runner will never process another bundle for the keys
425431
// in this bundle so state need not be included in the bundle commit.
426432
bool only_bundle_for_keys = 5;
433+
434+
// (Optional) If non-empty, the ID of the data stream to use for this bundle.
435+
// See comments at BeamFnData.Data for more details.
436+
//
437+
// The runner should only populate this field if the sdk advertises the
438+
// beam:protocol:named_data_streams:v1 capability.
439+
string data_stream_id = 6;
427440
}
428441

429442
message ProcessBundleResponse {
@@ -835,6 +848,11 @@ message Elements {
835848
// Stable
836849
service BeamFnData {
837850
// Used to send data between harnesses.
851+
//
852+
// Header metadata has the specified keys pairs:
853+
// - "worker_id": value is the id of the sdk
854+
// - "data_stream_id": value is the id of the data stream, distinguishing it from other data streams from the same
855+
// sdk. This field should only be populated if requested in a received ProcessBundleRequest from the runner.
838856
rpc Data(
839857
// A stream of data representing input.
840858
stream Elements)
@@ -900,6 +918,9 @@ message StateResponse {
900918

901919
service BeamFnState {
902920
// Used to get/append/clear state stored by the runner on behalf of the SDK.
921+
//
922+
// Header metadata has the specified keys pairs:
923+
// - "worker_id": the id of the sdk
903924
rpc State(
904925
// A stream of state instructions requested of the runner.
905926
stream StateRequest)
@@ -1295,6 +1316,9 @@ message LogControl {}
12951316
service BeamFnLogging {
12961317
// Allows for the SDK to emit log entries which the runner can
12971318
// associate with the active job.
1319+
//
1320+
// Header metadata has the specified keys pairs:
1321+
// - "worker_id": the id of the sdk
12981322
rpc Logging(
12991323
// A stream of log entries batched into lists emitted by the SDK harness.
13001324
stream LogEntry.List)
@@ -1356,6 +1380,8 @@ message WorkerStatusResponse {
13561380

13571381
// API for SDKs to report debug-related statuses to runner during pipeline execution.
13581382
service BeamFnWorkerStatus {
1383+
// Header metadata has the specified keys pairs:
1384+
// - "worker_id": the id of the sdk
13591385
rpc WorkerStatus (stream WorkerStatusResponse)
13601386
returns (stream WorkerStatusRequest) {}
13611387
}

model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,10 @@ message StandardProtocols {
16891689
// Indicates whether the SDK supports multimap state.
16901690
MULTIMAP_STATE = 12
16911691
[(beam_urn) = "beam:protocol:multimap_state:v1"];
1692+
1693+
// Indicates whether the SDK supports data stream ids being requested by the runner in
1694+
// ProcessBundleRequests.
1695+
NAMED_DATA_STREAMS = 13 [(beam_urn) = "beam:protocol:named_data_streams:v1"];
16921696
}
16931697
}
16941698

runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ public ActiveBundle newBundle(
298298
ImmutableMap.Builder<LogicalEndpoint, FnDataReceiver<?>> receiverBuilder =
299299
ImmutableMap.builder();
300300
BeamFnDataOutboundAggregator beamFnDataOutboundAggregator =
301-
fnApiDataService.createOutboundAggregator(() -> bundleId, false);
301+
fnApiDataService.createOutboundAggregator(bundleId, false);
302302
for (RemoteInputDestination remoteInput : remoteInputs) {
303303
LogicalEndpoint endpoint = LogicalEndpoint.data(bundleId, remoteInput.getPTransformId());
304304
receiverBuilder.put(

runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package org.apache.beam.runners.fnexecution.data;
1919

20-
import java.util.function.Supplier;
2120
import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements;
2221
import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator;
2322
import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver;
@@ -69,5 +68,5 @@ public interface FnDataService {
6968
* <p>The returned aggregator is not thread safe.
7069
*/
7170
BeamFnDataOutboundAggregator createOutboundAggregator(
72-
Supplier<String> processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes);
71+
String processBundleId, boolean collectElementsIfNoFlushes);
7372
}

runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.concurrent.LinkedBlockingQueue;
2424
import java.util.concurrent.TimeUnit;
2525
import java.util.concurrent.TimeoutException;
26-
import java.util.function.Supplier;
2726
import org.apache.beam.model.fnexecution.v1.BeamFnApi;
2827
import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements;
2928
import org.apache.beam.model.fnexecution.v1.BeamFnDataGrpc;
@@ -175,13 +174,13 @@ public void unregisterReceiver(String instructionId) {
175174

176175
@Override
177176
public BeamFnDataOutboundAggregator createOutboundAggregator(
178-
Supplier<String> processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes) {
177+
String instructionId, boolean collectElementsIfNoFlushes) {
179178
try {
180-
return new BeamFnDataOutboundAggregator(
181-
options,
182-
processBundleRequestIdSupplier,
183-
connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver(),
184-
collectElementsIfNoFlushes);
179+
BeamFnDataOutboundAggregator aggregator =
180+
new BeamFnDataOutboundAggregator(options, collectElementsIfNoFlushes);
181+
aggregator.prepareForInstruction(
182+
instructionId, connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver());
183+
return aggregator;
185184
} catch (InterruptedException e) {
186185
Thread.currentThread().interrupt();
187186
throw new RuntimeException(e);

runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public void testMessageReceivedBySingleClientWhenThereAreMultipleClients() throw
102102
for (int i = 0; i < 3; ++i) {
103103
final String instructionId = Integer.toString(i);
104104
BeamFnDataOutboundAggregator aggregator =
105-
service.createOutboundAggregator(() -> instructionId, false);
105+
service.createOutboundAggregator(instructionId, false);
106106
aggregator.start();
107107
FnDataReceiver<WindowedValue<String>> consumer =
108108
aggregator.registerOutputDataLocation(TRANSFORM_ID, CODER);

sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataGrpcMultiplexer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class BeamFnDataGrpcMultiplexer implements AutoCloseable {
6363
private final Cache</*instructionId=*/ String, /*unused=*/ Boolean> poisonedInstructionIds;
6464

6565
private static class PoisonedException extends RuntimeException {
66-
public PoisonedException() {
66+
private PoisonedException() {
6767
super("Instruction poisoned");
6868
}
6969
};

sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
*/
1818
package org.apache.beam.sdk.fn.data;
1919

20+
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
21+
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
22+
2023
import java.io.IOException;
2124
import java.util.Collections;
2225
import java.util.HashMap;
@@ -28,7 +31,6 @@
2831
import java.util.concurrent.Executors;
2932
import java.util.concurrent.ScheduledFuture;
3033
import java.util.concurrent.TimeUnit;
31-
import java.util.function.Supplier;
3234
import javax.annotation.Nullable;
3335
import javax.annotation.concurrent.NotThreadSafe;
3436
import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements;
@@ -56,7 +58,7 @@
5658
@SuppressWarnings({
5759
"nullness" // TODO(https://github.com/apache/beam/issues/20497)
5860
})
59-
// The calling thread that invokes sendBufferedDataAndFinishOutboundStreams synchronizes on
61+
// The calling thread that invokes sendOrCollectBufferedDataAndFinishOutboundStreams synchronizes on
6062
// flushLock effectively making the periodic flushing no longer read or mutate hasFlushedForBundle
6163
// and allowing the calling thread to read and mutate hasFlushedForBundle safely without needing to
6264
// create another memory barrier. Also note that flush is always invoked when synchronizing on
@@ -72,33 +74,56 @@ public class BeamFnDataOutboundAggregator {
7274
private static final Logger LOG = LoggerFactory.getLogger(BeamFnDataOutboundAggregator.class);
7375
private final int sizeLimit;
7476
private final long timeLimit;
75-
private final Supplier<String> processBundleRequestIdSupplier;
77+
private String instructionId;
7678
@VisibleForTesting final Map<String, Receiver<?>> outputDataReceivers;
7779
@VisibleForTesting final Map<TimerEndpoint, Receiver<?>> outputTimersReceivers;
78-
private final StreamObserver<Elements> outboundObserver;
80+
@Nullable private StreamObserver<Elements> outboundObserver;
7981
@Nullable @VisibleForTesting ScheduledFuture<?> flushFuture;
8082
private long bytesWrittenSinceFlush;
8183
private final Object flushLock;
8284
private final boolean collectElementsIfNoFlushes;
8385
private boolean hasFlushedForBundle;
8486

85-
public BeamFnDataOutboundAggregator(
86-
PipelineOptions options,
87-
Supplier<String> processBundleRequestIdSupplier,
88-
StreamObserver<Elements> outboundObserver,
89-
boolean collectElementsIfNoFlushes) {
87+
public BeamFnDataOutboundAggregator(PipelineOptions options, boolean collectElementsIfNoFlushes) {
9088
this.sizeLimit = getSizeLimit(options);
9189
this.timeLimit = getTimeLimit(options);
9290
this.collectElementsIfNoFlushes = collectElementsIfNoFlushes;
9391
this.outputDataReceivers = new HashMap<>();
9492
this.outputTimersReceivers = new HashMap<>();
95-
this.outboundObserver = outboundObserver;
96-
this.processBundleRequestIdSupplier = processBundleRequestIdSupplier;
9793
this.bytesWrittenSinceFlush = 0L;
9894
this.flushLock = new Object();
9995
this.hasFlushedForBundle = false;
10096
}
10197

98+
public void prepareForInstruction(
99+
String instructionId, StreamObserver<Elements> outboundObserver) {
100+
if (timeLimit > 0) {
101+
synchronized (flushLock) {
102+
checkState(this.instructionId == null && this.outboundObserver == null);
103+
this.instructionId = instructionId;
104+
this.outboundObserver = outboundObserver;
105+
}
106+
} else {
107+
checkState(this.instructionId == null && this.outboundObserver == null);
108+
this.instructionId = instructionId;
109+
this.outboundObserver = outboundObserver;
110+
}
111+
}
112+
113+
public void finishInstruction() {
114+
if (timeLimit > 0) {
115+
synchronized (flushLock) {
116+
checkState(this.instructionId != null && this.outboundObserver != null);
117+
this.instructionId = null;
118+
this.outboundObserver = null;
119+
}
120+
} else {
121+
checkState(this.instructionId != null && this.outboundObserver != null);
122+
this.instructionId = null;
123+
this.outboundObserver = null;
124+
}
125+
}
126+
102127
/** Starts the flushing daemon thread if data_buffer_time_limit_ms is set. */
103128
public void start() {
104129
if (timeLimit > 0 && this.flushFuture == null) {
@@ -166,7 +191,7 @@ private void flushInternal() {
166191
}
167192
Elements.Builder elements = convertBufferForTransmission();
168193
if (elements.getDataCount() > 0 || elements.getTimersCount() > 0) {
169-
outboundObserver.onNext(elements.build());
194+
checkNotNull(outboundObserver).onNext(elements.build());
170195
}
171196
hasFlushedForBundle = true;
172197
}
@@ -177,6 +202,7 @@ private void flushInternal() {
177202
* collectElementsIfNoFlushes=true, and there was no previous flush in this bundle, otherwise
178203
* returns null.
179204
*/
205+
@Nullable
180206
public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() {
181207
if (outputTimersReceivers.isEmpty() && outputDataReceivers.isEmpty()) {
182208
return null;
@@ -191,14 +217,14 @@ public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() {
191217
}
192218
LOG.debug(
193219
"Closing streams for instruction {} and outbound data {} and timers {}.",
194-
processBundleRequestIdSupplier.get(),
220+
instructionId,
195221
outputDataReceivers,
196222
outputTimersReceivers);
197223
for (Map.Entry<String, Receiver<?>> entry : outputDataReceivers.entrySet()) {
198224
String pTransformId = entry.getKey();
199225
bufferedElements
200226
.addDataBuilder()
201-
.setInstructionId(processBundleRequestIdSupplier.get())
227+
.setInstructionId(instructionId)
202228
.setTransformId(pTransformId)
203229
.setIsLast(true);
204230
entry.getValue().resetStats();
@@ -207,34 +233,43 @@ public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() {
207233
TimerEndpoint timerKey = entry.getKey();
208234
bufferedElements
209235
.addTimersBuilder()
210-
.setInstructionId(processBundleRequestIdSupplier.get())
236+
.setInstructionId(instructionId)
211237
.setTransformId(timerKey.pTransformId)
212238
.setTimerFamilyId(timerKey.timerFamilyId)
213239
.setIsLast(true);
214240
entry.getValue().resetStats();
215241
}
242+
// This is the end of the bundle so we reset state to prepare for future bundles.
216243
if (collectElementsIfNoFlushes && !hasFlushedForBundle) {
217244
return bufferedElements.build();
218245
}
219-
outboundObserver.onNext(bufferedElements.build());
220-
// This is now at the end of a bundle, so we reset hasFlushedForBundle to prepare for new
221-
// bundles.
246+
checkNotNull(outboundObserver).onNext(bufferedElements.build());
222247
hasFlushedForBundle = false;
223248
return null;
224249
}
225250

226251
// Send the elements to the StreamObserver associated with this aggregator.
227252
public void sendElements(Elements elements) {
228-
outboundObserver.onNext(elements);
253+
checkNotNull(outboundObserver).onNext(elements);
229254
}
230255

256+
// Prepares for discarding the aggregator without preserving its output or
257+
// preparing it for reuse.
231258
public void discard() {
232259
if (flushFuture != null) {
233-
flushFuture.cancel(true);
260+
// Short-circuit the possibly concurrently running flush.
261+
synchronized (flushLock) {
262+
bytesWrittenSinceFlush = 0L;
263+
finishInstruction();
264+
}
265+
flushFuture.cancel(false);
266+
} else {
267+
finishInstruction();
234268
}
235269
}
236270

237271
private Elements.Builder convertBufferForTransmission() {
272+
bytesWrittenSinceFlush = 0L;
238273
Elements.Builder bufferedElements = Elements.newBuilder();
239274
for (Map.Entry<String, Receiver<?>> entry : outputDataReceivers.entrySet()) {
240275
if (!entry.getValue().hasBufferedOutput()) {
@@ -243,7 +278,7 @@ private Elements.Builder convertBufferForTransmission() {
243278
ByteString bytes = entry.getValue().toByteStringAndResetBuffer();
244279
bufferedElements
245280
.addDataBuilder()
246-
.setInstructionId(processBundleRequestIdSupplier.get())
281+
.setInstructionId(instructionId)
247282
.setTransformId(entry.getKey())
248283
.setData(bytes);
249284
}
@@ -254,12 +289,11 @@ private Elements.Builder convertBufferForTransmission() {
254289
ByteString bytes = entry.getValue().toByteStringAndResetBuffer();
255290
bufferedElements
256291
.addTimersBuilder()
257-
.setInstructionId(processBundleRequestIdSupplier.get())
292+
.setInstructionId(instructionId)
258293
.setTransformId(entry.getKey().pTransformId)
259294
.setTimerFamilyId(entry.getKey().timerFamilyId)
260295
.setTimers(bytes);
261296
}
262-
bytesWrittenSinceFlush = 0L;
263297
return bufferedElements;
264298
}
265299

@@ -353,10 +387,12 @@ public void accept(T input) throws Exception {
353387
}
354388
}
355389

390+
@VisibleForTesting
356391
public long getByteCount() {
357392
return perBundleByteCount;
358393
}
359394

395+
@VisibleForTesting
360396
public long getElementCount() {
361397
return perBundleElementCount;
362398
}

sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/Environments.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ public static Set<String> getJavaCapabilities() {
522522
capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.SDK_CONSUMING_RECEIVED_DATA));
523523
capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.ORDERED_LIST_STATE));
524524
capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.MULTIMAP_STATE));
525+
capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.NAMED_DATA_STREAMS));
525526
return capabilities.build();
526527
}
527528

0 commit comments

Comments
 (0)