Skip to content

[Dataflow Streaming] [Multi Key] Flush streaming sinks at key boundaries and bundle completion - #39961

Open
arunpandianp wants to merge 9 commits into
apache:masterfrom
arunpandianp:flushWindmillSink
Open

arunpandianp wants to merge 9 commits into
apache:masterfrom
arunpandianp:flushWindmillSink

Conversation

@arunpandianp

Copy link
Copy Markdown
Contributor

No description provided.

…ies and bundle completion

When multi-key bundling is enabled in Dataflow Streaming Engine,
outputs (productions) emitted during a key's processElement lifecycle
must be attributed to that specific key's WorkItemCommitRequest, while
outputs emitted during DoFn finishBundle must be attributed to the
bundle-level MultiKeyWorkItemCommitRequest.

This change:
1. Adds finishKey(@nullable Object key) to the SinkWriter interface
   and implements it across streaming sinks (WindmillSink, PubsubSink,
   PubsubDynamicSink) and non-streaming sinks.
2. In WindmillSink, PubsubSink, and PubsubDynamicSink:
   - When multi-key bundles are enabled: flushes to the active key's commit builder
     in finishKey; flushes remaining finishBundle outputs to bundle-level
     outputs (bundleOutputMessages / bundlePubsubMessages) in close().
   - When multi-key bundles are disabled: finishKey does not flush; close() flushes
     all outputs directly into the single key's commit request.
   - Updates abort() to cleanly discard buffered messages and reset
     internal streams without flushing.
3. In StreamingModeExecutionContext and StreamingWorkScheduler, tracks
   bundleOutputMessages and bundlePubsubMessages and attaches them to
   MultiKeyWorkItemCommitRequest.
@arunpandianp
arunpandianp marked this pull request as ready for review September 3, 2026 12:27
@arunpandianp

Copy link
Copy Markdown
Contributor Author

R: @scwhittle

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment assign set of reviewers

/** Adds a value to the sink. Returns the size in bytes of the data written. */
public long add(ElemT value) throws IOException;

/** Called when all elements for a specific key have been processed. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we note this is optional and only currently done for multi-key streaming execution?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called in streaming for all jobs. Batch does not call. Updated comment to say this is called only for streaming.

Windmill.PubSubMessageBundle pubsubMessages = outputBuilder.build();
if (pubsubMessages.getMessagesCount() > 0) {
context.getOutputBuilder().addPubsubMessages(pubsubMessages);
if (context.multiKeyBundleEnabled()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just pass multiKeyBundleEnabled() as param?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@Override
public void close() throws IOException {
outputBuilders.clear();
if (context.multiKeyBundleEnabled()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just pass multiKeyBundleEnabled() as param to flush

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@Override
public void finishKey(@Nullable Object key) throws IOException {
if (context.multiKeyBundleEnabled()) {
flush(/* bundleLevel= */ false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above comment, is there a benefit to producing at the key level and not the bundle level? With bundle level we can reduce duplicating topic info and proto overhead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a good idea to merge the pubsub outputs. I'm thinking we can flush every 1MB or so to the current key. Do we need to worry about how it'll affect publishing with ordering keys (if/when we add support for that).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added logic to merge outputs till they reach 1MB.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not flush at bundle level? seems odd to attach outputs made from Key A to key B if limit is only exceeded when reaching key B.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to flush at the bundle level.

context.getOutputBuilder().addOutputMessages(outputBuilder.build());
Windmill.OutputMessageBundle bundle = outputBuilder.build();
if (bundleLevel) {
context.addBundleOutputMessages(bundle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same merging comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In WindmillSink, attaching the outputs with the keys that produced them might be useful since doing so will preserve the key-key ordering as long as there are no buffered outputs. I'm thinking to keep it without cross key merging, till we figure out the story around ordering. wdyt?

}

@Override
public void finishKey(@Nullable Object key) throws IOException {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a default no-op impl to the Sink base class? Since it's not called in batch anyway, forcing a lot of these batch sinks to implement it seems like unneeded clutter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

if (pubsubMessages.getMessagesCount() > 0) {
context.getOutputBuilder().addPubsubMessages(pubsubMessages);
public void finishKey(@Nullable Object key) throws IOException {
if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will need to be per-key if we change windmill pubsub to support ordering keys. Maybe you could put a comment here and in the add method so we don't forget?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@@ -321,6 +323,8 @@ public void reset() {
// these lists and maps are returned to callers after processing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see other comment, maybe we can avoid setting to emptyList and then reallocating if unused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about changing context.flushState() to return ExecuteWorkResult and reset as needed?

Then we can get rid of the accessors just used for the flushing and we can be smarter about how we reset to avoid allocations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea! done.


@Override
public void finishKey(@Nullable Object key) throws IOException {
if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto about ordered keys

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@Override
public void finishKey(@Nullable Object key) throws IOException {
if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) {
flush(/* bundleLevel= */ false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not flush at bundle level? It seems odd to attach output made from processing key A to key B later if the buffered size was not exceeded on A but later was on B.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not flush at bundle level?

Wanted to spread the load across many keys. The bundle level productions eventually get attached to an arbitrary key in the backend and having lots of productions on a single key could run into limits or cause load skew.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just change that implementation? If we do add key-ordering, putting these on a key may affect performance if we try to respect that they are attached to a particular key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack. Changed to flush at bundle level.

@Override
public void finishKey(@Nullable Object key) throws IOException {
if (context.multiKeyBundleEnabled()) {
flush(/* bundleLevel= */ false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not flush at bundle level? seems odd to attach outputs made from Key A to key B if limit is only exceeded when reaching key B.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.39496% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.98%. Comparing base (0b6057d) to head (36d3c71).
⚠️ Report is 142 commits behind head on master.

Files with missing lines Patch % Lines
...dataflow/worker/StreamingModeExecutionContext.java 79.36% 9 Missing and 4 partials ⚠️
...eam/runners/dataflow/worker/PubsubDynamicSink.java 94.44% 0 Missing and 1 partial ⚠️
...flow/worker/util/common/worker/WriteOperation.java 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #39961      +/-   ##
============================================
+ Coverage     55.06%   55.98%   +0.92%     
- Complexity     1722     2290     +568     
============================================
  Files          1075     1116      +41     
  Lines        171582   175894    +4312     
  Branches       1262     1486     +224     
============================================
+ Hits          94476    98471    +3995     
- Misses        74836    74911      +75     
- Partials       2270     2512     +242     
Flag Coverage Δ
java 75.72% <87.39%> (+8.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

return this.bundlePubsubMessages;
}

private List<Windmill.WorkItemCommitRequest> getOrCreateWorkItemCommits() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about inlining this? it has a single usage and we can create a single-item list (like Immutable list which has specialized SingletonImmutableList) if not using multikeybundles and enforce only one key is present

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also inline the pubsub ones since they also just have single usage

if (bundleLevel) {
context.addBundleOutputMessages(bundle);
} else {
context.getOutputBuilder().addOutputMessages(bundle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should change getOutputBuilder to be getKeyOutputBuilder? could do separately.

}
}
} finally {
outputBuilder = createOutputBuilder();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previously we cleared the builder (though see other comment that seems buggy).

However should we consider just clearing the messages from the bundle so we don't have to recreate builder, and it can cache message array etc?

if (pubsubMessages.getMessagesCount() > 0) {
context.getOutputBuilder().addPubsubMessages(pubsubMessages);
}
outputBuilder.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how did this work previously? It seems if we ever closed and then reused this object, the outputBuilder would not have the topic etc set. Are these sinks not actually reused in streaming?

context.finishKey();
WindmillComputationKey computationKey = context.getComputationKey();
ExecuteWorkResult result = context.flushStateAndReset();
Windmill.WorkItemCommitRequest commitRequest = result.workItemCommits().get(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert there is a single commit

ExecuteWorkResult result = executionContext.flushStateAndReset();

assertEquals(1234, executionContext.getOutputBuilder().getSourceBacklogBytes());
assertEquals(1234, result.workItemCommits().get(0).getSourceBacklogBytes());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert there is a single commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants