Skip to content

Fix data race in UpdateWithStart: read ExecutionState.Status before releasing workflow lock - #11602

Merged
yiminc merged 3 commits into
temporalio:mainfrom
Lakshaymiddha:fix/multioperation-status-data-race
Aug 19, 2026
Merged

Fix data race in UpdateWithStart: read ExecutionState.Status before releasing workflow lock#11602
yiminc merged 3 commits into
temporalio:mainfrom
Lakshaymiddha:fix/multioperation-status-data-race

Conversation

@Lakshaymiddha

@Lakshaymiddha Lakshaymiddha commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What changed?

Captured ExecutionState.Status before calling GetReleaseFn()(nil) in updateWithStart.Invoke to avoid a data race with concurrent goroutines that may acquire the lock and modify ExecutionState after it is released.

Added a regression test that confirms the race under -race.

Fixes #11600

Why?

Invoke in service/history/api/multioperation/api.go releases the workflow lock at line 196 via workflowLease.GetReleaseFn()(nil), then reads workflowLease.GetMutableState().GetExecutionState().Status at line 201 — after the lock is released. Any concurrent goroutine waiting on Lock() for the same workflow (e.g. a signal, terminate, or another update) can acquire the lock and modify ExecutionState between lines 196 and 201, creating a data race.

This matches the pattern noted in the Updater struct itself:

WARNING: any references to mutable state data have to be copied to avoid data races when used outside the workflow lease.

How did you test it?

  • added new unit test(s)

The test spawns a concurrent writer that modifies ExecutionState.Status after the lock is released.

Before fix:

go test -race -tags test_dep -count=1 \
  -run TestUpdateWithStartSuite/TestInvoke_CompletedUpdate_StatusCapturedBeforeRelease \
  ./service/history/api/multioperation/

WARNING: DATA RACE
Read at ... api.go:201
--- FAIL

After fix:

ok  go.temporal.io/server/service/history/api/multioperation

Potential risks

Minimal - single line moved before the release call. No API or persistence behavior change.

@Lakshaymiddha
Lakshaymiddha requested a review from a team August 17, 2026 19:07
@Lakshaymiddha
Lakshaymiddha requested review from a team as code owners August 17, 2026 19:07
…eleasing workflow lock

Capture ExecutionState.Status before calling GetReleaseFn()(nil) to
avoid a data race with concurrent goroutines that may acquire the lock
and modify ExecutionState after it is released.

Add a regression test that confirms the race under -race.
@Lakshaymiddha
Lakshaymiddha force-pushed the fix/multioperation-status-data-race branch from 7a5f71e to 59fd185 Compare August 17, 2026 19:22
@yycptt
yycptt requested a review from a team August 18, 2026 23:25
@yycptt yycptt added the reliability-2026 Reliability related changes label Aug 18, 2026
@yiminc
yiminc merged commit 5e43259 into temporalio:main Aug 19, 2026
57 checks passed
@Lakshaymiddha
Lakshaymiddha deleted the fix/multioperation-status-data-race branch August 20, 2026 02:47
davidporter-id-au pushed a commit to davidporter-id-au/temporal that referenced this pull request Aug 24, 2026
…eleasing workflow lock (temporalio#11602)

## What changed?
Captured `ExecutionState.Status` before calling `GetReleaseFn()(nil)` in
`updateWithStart.Invoke` to avoid a data race with concurrent goroutines
that may acquire the lock and modify `ExecutionState` after it is
released.

Added a regression test that confirms the race under `-race`.

Fixes temporalio#11600

## Why?
`Invoke` in `service/history/api/multioperation/api.go` releases the
workflow lock at line 196 via `workflowLease.GetReleaseFn()(nil)`, then
reads `workflowLease.GetMutableState().GetExecutionState().Status` at
line 201 — after the lock is released. Any concurrent goroutine waiting
on `Lock()` for the same workflow (e.g. a signal, terminate, or another
update) can acquire the lock and modify `ExecutionState` between lines
196 and 201, creating a data race.

This matches the pattern noted in the `Updater` struct itself:

> WARNING: any references to mutable state data *have to* be copied to
avoid data races when used outside the workflow lease.

## How did you test it?
- [x] added new unit test(s)

The test spawns a concurrent writer that modifies
`ExecutionState.Status` after the lock is released.

**Before fix:**
```
go test -race -tags test_dep -count=1 \
  -run TestUpdateWithStartSuite/TestInvoke_CompletedUpdate_StatusCapturedBeforeRelease \
  ./service/history/api/multioperation/

WARNING: DATA RACE
Read at ... api.go:201
--- FAIL
```

**After fix:**
```
ok  go.temporal.io/server/service/history/api/multioperation
```

## Potential risks
Minimal - single line moved before the release call. No API or
persistence behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reliability-2026 Reliability related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Data race in UpdateWithStart: ExecutionState.Status read after workflow lock release

3 participants