[Dataflow Streaming] [Multi Key] Flush streaming sinks at key boundaries and bundle completion - #39961
[Dataflow Streaming] [Multi Key] Flush streaming sinks at key boundaries and bundle completion#39961arunpandianp wants to merge 9 commits into
Conversation
…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.
|
R: @scwhittle |
|
Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment |
| /** 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. */ |
There was a problem hiding this comment.
Should we note this is optional and only currently done for multi-key streaming execution?
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
just pass multiKeyBundleEnabled() as param?
| @Override | ||
| public void close() throws IOException { | ||
| outputBuilders.clear(); | ||
| if (context.multiKeyBundleEnabled()) { |
There was a problem hiding this comment.
just pass multiKeyBundleEnabled() as param to flush
| @Override | ||
| public void finishKey(@Nullable Object key) throws IOException { | ||
| if (context.multiKeyBundleEnabled()) { | ||
| flush(/* bundleLevel= */ false); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Added logic to merge outputs till they reach 1MB.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Changed to flush at the bundle level.
| context.getOutputBuilder().addOutputMessages(outputBuilder.build()); | ||
| Windmill.OutputMessageBundle bundle = outputBuilder.build(); | ||
| if (bundleLevel) { | ||
| context.addBundleOutputMessages(bundle); |
There was a problem hiding this comment.
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?
f8fd69b to
624b1ab
Compare
624b1ab to
2b2be9e
Compare
| } | ||
|
|
||
| @Override | ||
| public void finishKey(@Nullable Object key) throws IOException {} |
There was a problem hiding this comment.
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.
| if (pubsubMessages.getMessagesCount() > 0) { | ||
| context.getOutputBuilder().addPubsubMessages(pubsubMessages); | ||
| public void finishKey(@Nullable Object key) throws IOException { | ||
| if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) { |
There was a problem hiding this comment.
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?
| @@ -321,6 +323,8 @@ public void reset() { | |||
| // these lists and maps are returned to callers after processing | |||
There was a problem hiding this comment.
see other comment, maybe we can avoid setting to emptyList and then reallocating if unused
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
good idea! done.
|
|
||
| @Override | ||
| public void finishKey(@Nullable Object key) throws IOException { | ||
| if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) { |
There was a problem hiding this comment.
ditto about ordered keys
| @Override | ||
| public void finishKey(@Nullable Object key) throws IOException { | ||
| if (context.multiKeyBundleEnabled() && bufferedBytes >= MAX_PUBSUB_BUNDLE_BYTES) { | ||
| flush(/* bundleLevel= */ false); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
ack. Changed to flush at bundle level.
| @Override | ||
| public void finishKey(@Nullable Object key) throws IOException { | ||
| if (context.multiKeyBundleEnabled()) { | ||
| flush(/* bundleLevel= */ false); |
There was a problem hiding this comment.
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 Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| return this.bundlePubsubMessages; | ||
| } | ||
|
|
||
| private List<Windmill.WorkItemCommitRequest> getOrCreateWorkItemCommits() { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
could also inline the pubsub ones since they also just have single usage
| if (bundleLevel) { | ||
| context.addBundleOutputMessages(bundle); | ||
| } else { | ||
| context.getOutputBuilder().addOutputMessages(bundle); |
There was a problem hiding this comment.
maybe we should change getOutputBuilder to be getKeyOutputBuilder? could do separately.
| } | ||
| } | ||
| } finally { | ||
| outputBuilder = createOutputBuilder(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
assert there is a single commit
| ExecuteWorkResult result = executionContext.flushStateAndReset(); | ||
|
|
||
| assertEquals(1234, executionContext.getOutputBuilder().getSourceBacklogBytes()); | ||
| assertEquals(1234, result.workItemCommits().get(0).getSourceBacklogBytes()); |
There was a problem hiding this comment.
assert there is a single commit
No description provided.