Skip to content

Fix #1354 - Isolate variables in ForExecutor to avoid racing condition to overwrite loop variables#1363

Open
ricardozanini wants to merge 3 commits intoserverlessworkflow:mainfrom
ricardozanini:fix-forexecutor-async
Open

Fix #1354 - Isolate variables in ForExecutor to avoid racing condition to overwrite loop variables#1363
ricardozanini wants to merge 3 commits intoserverlessworkflow:mainfrom
ricardozanini:fix-forexecutor-async

Conversation

@ricardozanini
Copy link
Copy Markdown
Member

@ricardozanini ricardozanini commented May 2, 2026

Many thanks for submitting your Pull Request ❤️!

What this PR does / why we need it:
Fixes #1354

Special notes for reviewers:

  • I changed the InMemoryEvents class slightly to be inherit-friendly
  • Moved TranceExecutionListener to impl so it can easily be reused in other tests and in client code - it's quite useful for debugging/tracing, even in prod code.

Additional information (if needed):

Signed-off-by: Ricardo Zanini <ricardozanini@gmail.com>
…id racing condition to overwrite loop variables

Signed-off-by: Ricardo Zanini <ricardozanini@gmail.com>
Signed-off-by: Ricardo Zanini <ricardozanini@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses issue #1354 by changing how ForExecutor handles loop variables to prevent async tasks (e.g., emit) from observing overwritten $item/$index values. It also refactors supporting test utilities by making InMemoryEvents subclass-friendly, introducing a lagged event broker for deterministic async tests, and relocating TraceExecutionListener into impl code for reuse.

Changes:

  • Update ForExecutor to capture per-iteration loop values and reapply them at execution time in the async chain.
  • Make InMemoryEvents fields protected/final to support subclassing; add LaggedInMemoryEvents for regression testing async behavior.
  • Move TraceExecutionListener into io.serverlessworkflow.impl.lifecycle and update tests/tools to import it from the new package.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
impl/test/src/test/java/io/serverlessworkflow/impl/test/MvStorePersistenceTest.java Update import to new TraceExecutionListener package.
impl/test/src/test/java/io/serverlessworkflow/impl/test/DBGenerator.java Update import to new TraceExecutionListener package.
impl/core/src/main/java/io/serverlessworkflow/impl/lifecycle/TraceExecutionListener.java Change package to impl.lifecycle (and remove now-redundant imports).
impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java Attempt to fix foreach race by capturing item/index and re-setting variables in the async callback.
impl/core/src/main/java/io/serverlessworkflow/impl/events/InMemoryEvents.java Expose internals as protected to allow inheritance in tests/client code.
experimental/test/src/test/java/io/serverlessworkflow/fluent/test/LaggedInMemoryEvents.java New lagged publisher used to make async foreach behavior reproducible.
experimental/test/src/test/java/io/serverlessworkflow/fluent/test/ForEachFuncTest.java Switch to lagged broker and add listener to help validate the foreach/emit regression.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +83 to +87
final Object currentItem = iter.next();
final int currentIndex = i++;
taskContext.variables().put(task.getFor().getEach(), currentItem);
taskContext.variables().put(task.getFor().getAt(), currentIndex);

}
try {
Thread.sleep(10);
} catch (InterruptedException e) {

public class LaggedInMemoryEvents extends InMemoryEvents {

private static final Logger logger = LoggerFactory.getLogger(InMemoryEvents.class);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ForExecutor race condition: shared TaskContext causes wrong items to be processed in async loops

2 participants