Skip to content

[Dataflow Streaming] [Multi Key] MultiKey failure handling + Integration - #38919

Merged
scwhittle merged 38 commits into
apache:masterfrom
arunpandianp:multikey_failure
Jul 29, 2026
Merged

[Dataflow Streaming] [Multi Key] MultiKey failure handling + Integration #38919
scwhittle merged 38 commits into
apache:masterfrom
arunpandianp:multikey_failure

Conversation

@arunpandianp

Copy link
Copy Markdown
Contributor

The change connects adds failure handling for multi key commits.
Integrates StreamingWorkScheduler and multikey commit methods.
Updates StreamingModeExecutionContext::advance to pull in more items from BoundedWorkQueue

All changes are behind the experiment unstable_enable_multi_key_bundle and does not affect default logic.

…lients

- Add MultiKeyWorkItemCommitRequest to windmill.proto.
- Support MultiKey commits in Commit model and StreamingEngineWorkCommitter.
- Update GrpcCommitWorkStream to batch and stream MultiKey commit requests.
# Conflicts:
#	runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java
Resolved conflicts in StreamingModeExecutionContext.java and StreamingModeExecutionContextTest.java.
Fixed compilation error in Work.java by removing duplicate getComputationId() method.

TAG=agy
CONV=143daaa5-e902-4d26-820d-cf1af2babb84
@arunpandianp

Copy link
Copy Markdown
Contributor Author

R: @scwhittle This change is on top of #38814 and #38768 PTAL

@github-actions

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

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces multi-key commit support for Dataflow streaming, allowing multiple work items to be committed in a single transaction. It enhances failure handling by enabling re-execution of specific work items within a batch if a retryable failure occurs. Additionally, it optimizes the execution context to pull more work items from the BoundedWorkQueue, thereby increasing processing efficiency for multi-key bundles.

Highlights

  • Multi-Key Commit Failure Handling: Introduced robust failure handling for multi-key commits, allowing for partial re-execution of work items upon retryable failures.
  • Streaming Scheduler Integration: Integrated StreamingWorkScheduler with multi-key commit methods to enable efficient batching of work items.
  • StreamingModeExecutionContext Updates: Updated StreamingModeExecutionContext::advance to pull more items from the BoundedWorkQueue, improving throughput for multi-key bundles.
  • Experimental Feature Flag: All changes are guarded by the 'unstable_enable_multi_key_bundle' experiment flag, ensuring no impact on default logic.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces multi-key bundle support for streaming in the Google Cloud Dataflow Java worker, enabling batching and advancing through multiple keys within a key group, as well as transactionally committing multi-key work batches. Feedback suggests adding robust error handling when parsing experimental options to prevent worker crashes, and implementing defensive null and bounds checks when accessing read operation receivers to avoid potential runtime exceptions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +252 to +259
String batchSizeStr =
ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_SIZE);
this.maxKeyGroupBatchSize = batchSizeStr != null ? Integer.parseInt(batchSizeStr) : 100;

String batchTimeStr =
ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS);
this.maxKeyGroupBatchTimeNanos =
TimeUnit.MILLISECONDS.toNanos(batchTimeStr != null ? Long.parseLong(batchTimeStr) : 100);

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.

medium

Parsing user-provided experimental options directly using Integer.parseInt and Long.parseLong without error handling can cause the worker to crash if the values are malformed. It is safer to wrap these in a try-catch block and fall back to the default values with a warning log.

    String batchSizeStr =
        ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_SIZE);
    int batchSize = 100;
    if (batchSizeStr != null) {
      try {
        batchSize = Integer.parseInt(batchSizeStr);
      } catch (NumberFormatException e) {
        LOG.warn("Failed to parse {} as integer, using default of 100", WINDMILL_MAX_KEY_GROUP_BATCH_SIZE, e);
      }
    }
    this.maxKeyGroupBatchSize = batchSize;

    String batchTimeStr =
        ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS);
    long batchTimeMs = 100;
    if (batchTimeStr != null) {
      try {
        batchTimeMs = Long.parseLong(batchTimeStr);
      } catch (NumberFormatException e) {
        LOG.warn("Failed to parse {} as long, using default of 100", WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS, e);
      }
    }
    this.maxKeyGroupBatchTimeNanos = TimeUnit.MILLISECONDS.toNanos(batchTimeMs);

Comment on lines +714 to +718
HashMap<String, ElementCounter> counters =
((DataflowMapTaskExecutor) workExecutor)
.getReadOperation()
.receivers[0]
.getOutputCounters();

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.

medium

Defensive programming: Accessing receivers[0] directly without checking if getReadOperation() is null, or if receivers is null or empty, can lead to NullPointerException or ArrayIndexOutOfBoundsException. Adding appropriate guards ensures robust execution.

    DataflowMapTaskExecutor mapTaskExecutor = (DataflowMapTaskExecutor) workExecutor;
    if (mapTaskExecutor.getReadOperation() == null
        || mapTaskExecutor.getReadOperation().receivers == null
        || mapTaskExecutor.getReadOperation().receivers.length == 0) {
      return 0L;
    }
    HashMap<String, ElementCounter> counters =
        mapTaskExecutor.getReadOperation().receivers[0].getOutputCounters();
    if (counters == null) {
      return 0L;
    }

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.44884% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.39%. Comparing base (1cf3545) to head (0df9f54).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
...e/beam/runners/dataflow/worker/streaming/Work.java 69.44% 10 Missing and 1 partial ⚠️
...dataflow/worker/StreamingModeExecutionContext.java 94.59% 8 Missing and 2 partials ⚠️
...nners/dataflow/worker/WindowingWindmillReader.java 81.08% 3 Missing and 4 partials ⚠️
...ers/dataflow/worker/util/BoundedQueueExecutor.java 75.00% 0 Missing and 3 partials ⚠️
...nners/dataflow/worker/StreamingDataflowWorker.java 60.00% 1 Missing and 1 partial ⚠️
...unners/dataflow/worker/WorkCancelingException.java 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #38919       +/-   ##
=============================================
- Coverage     59.33%   55.39%    -3.95%     
+ Complexity    16593     2243    -14350     
=============================================
  Files          2845     1104     -1741     
  Lines        291334   171355   -119979     
  Branches      14421     1437    -12984     
=============================================
- Hits         172859    94915    -77944     
+ Misses       111065    74000    -37065     
+ Partials       7410     2440     -4970     
Flag Coverage Δ
java 75.76% <88.44%> (+9.65%) ⬆️

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.

@derrickaw

Copy link
Copy Markdown
Collaborator

Hi @scwhittle, can you review this at your convenience? Thanks

@scwhittle scwhittle left a comment

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.

waiting on tests until previous change has been merged

@@ -88,6 +88,11 @@ static ActiveWorkState create(WindmillStateCache.ForComputation computationState
return new ActiveWorkState(new HashMap<>(), computationStateCache);
}

synchronized Optional<ExecutableWork> getActiveWork(ShardedKey shardedKey, WorkId workId) {

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 return nullable ExecutableWork instead? With annotations that seems preferrable to me to avoid the optional allocation

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.

@@ -131,6 +131,10 @@ public void completeWorkAndScheduleNextWorkForKey(ShardedKey shardedKey, WorkId
.ifPresent(this::forceExecute);
}

public void reExecuteActiveWork(ShardedKey shardedKey, WorkId workId) {

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.

nit: I would uncapitalized E since reexecute is one word

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.

this.latencyTrackingId =
Long.toHexString(workItem.getShardingKey())
+ '-'
+ Long.toHexString(workItem.getWorkToken());
this.currentState = TimedState.initialState(startTime);
this.isFailed = false;
this.getWorkStreamLatencies = getWorkStreamLatencies;
}

public static Work create(

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.

if we don't use this often, perhaps we should just add the ImmutableList.of() to the call-site

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.

for (LatencyAttribution latency : getWorkStreamLatencies) {
totalDurationPerState.put(
latency.getState(), Duration.millis(latency.getTotalDurationMillis()));
public ImmutableList<LatencyAttribution> getWorkStreamLatencies() {

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.

I don't see usage of this, can we just get rid of this.getWorkStreamLatencies and instead do the recordGetWorkStreamLatencies logic in the constructor?

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.

removed getWorkStreamLatencies().

recordGetWorkStreamLatencies was initially inside the constructor, moved it out of the constructor to reduce load on the Getworkstream thread. recordGetWorkStreamLatencies runs in the processing thread.

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.

Not sure if you sent comments, but I was also suggesting removing the member variable and public recordGetWorkStreamLatencies method as well. Seems it could just be done in constructor?

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.

Sorry forgot to publish comments. Published now.

this.elements = elements;
private BoundedQueueExecutorWorkHandleImpl(Work work, long bytes) {
checkArgument(bytes >= 0);
this.workBatch = new ArrayList<>();

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 workBatch be an ImmutableList.Builder?

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.

changing it to ImmutableList.Builder is adding more copies in merge(). ArrayList seems to be better.

Windmill.MultiKeyWorkItemCommitRequest.Builder multiKeyBuilder =
Windmill.MultiKeyWorkItemCommitRequest.newBuilder();

Work primaryWork = workBatch.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.

checkState that workBatch is non-empty

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.


for (int i = 0; i < workBatch.size(); i++) {
// TODO: Add commit size validation
Windmill.WorkItemCommitRequest commit = workItemCommits.get(i);

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.

precondition that workitemcommits and workBatch are same size

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.

boolean hotKeyLoggingEnabled,
String stepName,
String sourceBytesProcessCounterName,
PipelineOptions options,

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.

instead of passing in full options, could pass some internal multikeyoptions struct

benefits is that the parsing doesn't have to be here and can be shared across all the contexts for now. But in the future we may want to configure differently for different fused stages based upon other information and that would allow us to do so as it would be separate from the single experiment value.

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 use MultiKeyBundleOptions.

return false;
}
if (workIsFailed()) {
throw new WorkItemCancelledException(checkStateNotNull(work).getWorkItem().getShardingKey());

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.

move activeWork definition above and use it here instead of separate check

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.

* Indicates that the work is no longer valid and should be canceled. It is thrown as a signal for
* upper layers to mark the work as failed.
*/
public class WorkCancelingException extends RuntimeException {

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.

nit: can we use Cancelling with two ls to match other usage in beam

should we just use WorkItemCancelledException.java? If not can you explain the differences here?

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.

Renamed and added differences. once is thrown before marking work as failed and the other is thrown after noticing that a work is failed.

@arunpandianp
arunpandianp requested a review from scwhittle July 24, 2026 07:42

@scwhittle scwhittle left a comment

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.

Code looks mostly good, still going through tests

+ Long.toHexString(workItem.getWorkToken());
this.currentState = TimedState.initialState(startTime);
this.isFailed = false;
this.getWorkStreamLatencies = getWorkStreamLatencies;

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.

instead of saving just populate totalDurationPerState here

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 was intentionally moved out of the submission loop in #33736

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.

can you add a comment here?
// We defer recordGetWorkStreamLatencies() to be called during bundle processing
// as these are constructed on the hot GetWork thread

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.

Thanks, done.

switch (evaluateRetry(computationId, executableWork.work(), t)) {
case DO_NOT_RETRY:
// Consider the item invalid. It will eventually be retried by Windmill if it still needs
// to

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.

nit, wrap comment

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 (workBatch.isEmpty()) {
return;
}
if (workBatch.size() > 1 || multiKeyBundleOptions.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.

is there a benefit to sending single keys in multi-key format? Otherwise seems likely more overhead in protos etc

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.

The benefit is eventually we can remove the single key code path and have only the multikey path. Having one code path on a job also will make debugging easier.

@scwhittle scwhittle left a comment

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.

finished looking at all tests, so think this is likely final round

+ Long.toHexString(workItem.getWorkToken());
this.currentState = TimedState.initialState(startTime);
this.isFailed = false;
this.getWorkStreamLatencies = getWorkStreamLatencies;

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.

can you add a comment here?
// We defer recordGetWorkStreamLatencies() to be called during bundle processing
// as these are constructed on the hot GetWork thread

Windmill.MultiKeyWorkItemCommitRequest.newBuilder();

Work primaryWork = workBatch.get(0);
Work.KeyGroup keyGroup = primaryWork.getKeyGroup();

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.

since we are using this for single keys also that likely don't have a group, should we avoid setting the key group nested field if 0?

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.

Windmill.Uint128Proto.newBuilder().setHigh(keyGroup.high()).setLow(keyGroup.low()).build());

for (int i = 0; i < workBatch.size(); i++) {
// TODO: Retry on commit truncations

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 throw an exception for now?

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.

// Key switch listener to delegate MDC logging context and thread name updates
public interface KeyTransitionListener {
void onKeyTransition(Work oldWork, Work newWork);
void onKeyTransition(@Nullable Work oldWork, Work newWork);

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.

// oldWork is null when newWork is the first work for the bundle.

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.

currentBuilder.clear();
currentBuilder.mergeFrom(truncationBuilder.build());

// TODO: throw and retry when truncation is not on a single key 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.

can we throw an exception if multikey bundles are enabled to make sure we don't lose data if we forget to address this?

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.

makeWorker(
defaultWorkerParams(
"--experiments=unstable_enable_multi_key_bundle,windmill_max_key_group_batch_time_ms=50000",
"--numberOfWorkerHarnessThreads=1")

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.

do we need this harness threads because otherwise we start new threads to process the same group in parallel?

If so I wonder if that something we might want to try to prevent in the future? If we have more threads than # of CPU, it is likely better to not parallelize a key group.

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.

Yes, there is room to improve the batching. Planning to tackle it separately.

.setKey(keyRequest.getKey())
.setShardingKey(keyRequest.getShardingKey());
if (keyRequest.getWorkToken() == 2) {
keyBuilder.setFailed(true);

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.

if we do add support for the generic response to the server, we could also add the ability to set work tokens to fail get data requests.

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 StreamingDataflowWorkerTest::emptyDataResponderWithFailedWorkTokens that take the failed work tokens.

options = PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class);
options
.as(ExperimentalOptions.class)
.setExperiments(Arrays.asList("unstable_enable_multi_key_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.

List.of

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.

work1, workExecutor, mockExecutor, mockHandle, null, (oldWork, newWork) -> {});

assertTrue(executionContext.advance());
assertEquals("key2", executionContext.getSerializedKey().toStringUtf8());

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.

advance again?

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.


work1.setFailed();

assertThrows(WorkItemCancelledException.class, () -> executionContext.advance());

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 no interactions on executor

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.

@arunpandianp
arunpandianp requested a review from scwhittle July 29, 2026 13:49
@scwhittle
scwhittle merged commit 3c9f9ce into apache:master Jul 29, 2026
16 checks passed
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.

3 participants