Skip to content

[Bug] Nondeterminism replaying local activity markers recorded after a FORCE_CLOSE_COMMAND workflow task failure (1.34.0) #1976

Closed as not planned

Description

@kkdy21

What are you really trying to do?

A workflow fans out ~900 regular activities with asyncio.gather. A workflow outbound interceptor records progress with a local activity: at most one is in flight, and when it resolves a done callback starts the next one with the changes accumulated in the meantime. (We have replaced it with a regular activity as a workaround.)

Describe the bug

While results streamed in, workflow tasks took 2.5-6 s and the server failed them 12 times with WORKFLOW_TASK_FAILED_CAUSE_FORCE_CLOSE_COMMAND (51-139 activity completions arrived during each task). After one of those, the next workflow task (started 08:52:05.021Z, completed 08:52:08.319Z) recorded, in this order:

  • local activity marker seq 1296 (activation_index 1, original_schedule_time 08:52:04.597Z, complete_time 08:52:00.700Z)
  • 422 x ScheduleActivityTask (seq 1297-1718)
  • local activity marker seq 1719 (activation_index 1, original_schedule_time 08:52:04.637Z, complete_time 08:52:02.229Z)
  • 49 x ScheduleActivityTask
  • local activity markers seq 1769 (activation_index 2) and 1770 (activation_index 3)

The complete_time of 1296 and 1719 is before that task started, so both ran during the force-closed attempt (08:52:00.674Z-08:52:04.941Z) and their results were handed to the next attempt.

Any later full replay of this history fails with:

[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1719)

Replaying the downloaded history with Replayer, the same workflow code and temporalio 1.34.0 fails the same way, so the SDK cannot replay a history it wrote. On the live worker the workflow task kept failing until we terminated the workflow (114 attempts over 16 hours).

Our reading (may be wrong): in the original activation 1, the 1296 result was delivered after the 422 activity resolutions, so the next local activity got seq 1719. activation_index keeps 1296 in activation 1 but not its position among the other resolutions in that activation, so on replay the next local activity gets a different seq.

Minimal Reproduction

We don't have a minimal reproduction yet. The history contains customer data, so we can't attach it, but we can share sanitized event and marker metadata or run a patched build against it.

Shape: asyncio.gather over many activities, plus an outbound interceptor that chains one local activity at a time from done callbacks, plus enough concurrent completions to trigger FORCE_CLOSE_COMMAND (default history.maximumBufferedEventsBatch = 100).

Environment/Versions

Additional context

Workaround: send the recording call as a regular activity instead of a local activity.

Activity

  1. mjameswh commented on Oct 16, 2026

    @mjameswh
    Contributor

    Thanks for reporting. A history file (even sanitized) and/or repro code would definitely help investigation. If you are a Temporal Cloud customer, I'd also recommend opening a support ticket on our portal, and provide information through that channel.

    Also, can please answer these questions?

    • Was this workflow/interceptor/local activity working fine before, or is this a new workload?
    • If that looks like a regression, are you saying it appeared in 1.32.0? What other versions of the SDK have you tried?
    • Do workflow executions that fail with NDE ran exclusively on SDK 1.32.0, or did they start executing with a different version and then transitioned to a newer SDK version?
  2. deleted a comment from shaikn6 on Oct 19, 2026
  3. tconley1428 commented on Oct 22, 2026

    @tconley1428
    Contributor

    Feel free to reopen if more information becomes available.

  4. lucafranco1994pot-cloud commented on Nov 5, 2026

    @lucafranco1994pot-cloud

    repro_1976_frozen.zip

    Hi, I was able to reproduce this failure locally with a fully synthetic workload (no customer data). The attached zip contains the reproducer scripts, the histories referenced below and SHA-256 checksums.

    Setup: Temporal CLI 1.10.0 (dev server, Server 1.32.2, default history.maximumBufferedEventsBatch=100), Python 3.13. Each run used exactly one SDK version from start to finish.
    Workload (repro_peritem.py): 900 parallel chains of 2 regular activities each (1800 total). A workflow interceptor schedules one local activity per completed regular activity, strictly serially: the done callback of the running local activity starts the next one.

    Live runs:

    • SDK 1.34.0, runs that hit WORKFLOW_TASK_FAILED_CAUSE_FORCE_CLOSE_COMMAND: 2 of 2 failed replay with "[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1209)" and "(1724)". The live workflow also stopped progressing (timed out after 300 s).
    • SDK 1.34.0, run without force close: 1 of 1 replayed fine.
    • SDK 1.35.0, runs with force close: 4 of 4 completed live and replayed fine. One further run without force close also replayed fine.
    • A variant that batches all pending results into one local activity (only about 8 local activities in total) replayed fine on both versions, including runs with force close.

    Cross-version replay of the same frozen histories:

    • Failing 1.34.0 histories replayed with 1.34.0: same error.
    • Failing 1.34.0 histories replayed with 1.35.0: same error (LocalActivity(1209) / LocalActivity(1724)).
    • Passing 1.35.0 history replayed with 1.34.0 and 1.35.0: OK.

    For these two failing histories, upgrading the replayer from 1.34.0 to 1.35.0 does not resolve the replay failure. In contrast, the tested passing history generated under 1.35.0 replays successfully with either version.

    This suggests that the observed difference involves how the histories are produced during execution, rather than a replay-only fix in 1.35.0. However, these tests do not distinguish between a history-generation issue, a replay limitation shared by both versions, or an interaction between the two. The root cause remains unverified.

    Limits: small number of runs; I have not identified the change between the two versions; the controls "no interceptor" and "regular instead of local logging activities" never reached force close, so they are not valid controls yet. This is a synthetic reproduction of the reported pattern, not a reproduction of the original history.

    Happy to run additional variants if that helps.

  5. lucafranco1994pot-cloud commented on Nov 5, 2026

    @lucafranco1994pot-cloud

    Follow-up with captured worker logs. The attached repro_1976_frozen_v2.zip contains the synthetic reproducer, worker logs, histories and checksums.

    I re-ran the same synthetic workload with Core log output captured. Each run is a separate worker process running exactly one workflow, and the error lines carry the same run_id as the exported history.

    • Python SDK 1.34.0, run 1: FORCE_CLOSE_COMMAND occurred. The log shows 2x "Trying to send a new WFT for a run which already has one!" (14:44:58 and 14:44:59 UTC), then about 9 s later the live worker fails the workflow task with "[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1502)". Replay of the exported history fails with the same error class.
    • Python SDK 1.34.0, run 2: same pattern. 5x the same log line between 14:50:00 and 14:50:17 UTC, then at 14:50:28 the live failure "Missing associated machine for LocalActivity(1781)". Replay fails as well.
    • Python SDK 1.35.0, one comparison run: FORCE_CLOSE_COMMAND occurred, the log line was not observed, and replay passed.

    The log line matches the task-replacement scenario documented in temporalio/sdk-rust#1640 ("Buffer a new workflow task while the run already has one"). That PR buffers an incoming workflow task while Core still holds the current task, including while waiting for a local activity. The Python SDK 1.35.0 release notes list this fix (Core commit 212e8150).

    This makes #1640 my leading hypothesis for the synthetic reproduction. It is not a confirmed diagnosis of the original production case in this issue: #1640 documents a different downstream nondeterminism error, and my comparison changes the full SDK version rather than isolating that fix (1.35.0 also contains other Core changes, e.g. #1620). I have not tested a 1.35.0-based build with only the #1640 buffering change reverted.

    Separately, the two failing 1.34.0 histories from my previous comment also failed replay under 1.35.0. I keep that result distinct from the successful replay of histories newly generated under 1.35.0.

    Could you help confirm whether this reproducer exercises the #1640 path and whether that task replacement explains the LocalActivity marker replay failure?

    repro_1976_frozen_v2.zip

  6. kkdy21 commented on Nov 5, 2026

    @kkdy21
    Author

    Sorry for the late reply, and thanks @lucafranco1994pot-cloud for the reproducer.

    Answers to @mjameswh's questions first:

    1. Working before, or a new workload? A fairly new workload. The interceptor that chains local activities was added to our code on 2026-09-14, about a month before the failure. Our dependency was unpinned (temporalio>=1.29,<2), so that code only ever ran on 1.32.0 through 1.34.0, and we did not notice this error before 2026-10-15. So we can't say it worked on an older SDK; we never ran it on one.
    2. Regression in 1.32.0? Which versions did you try? The production failure was on 1.34.0 only, and I replayed that history with 1.34.0 (same error). I could not share that history, so I used Luca's synthetic reproducer to compare versions: 1.31.0, 1.32.0, 1.34.0 and 1.35.0 (not 1.33.0). The results below point to a regression that starts in 1.32.0 and is fixed in 1.35.0.
    3. One SDK version, or a version transition? One version. The whole execution, from start to the first NDE, ran on a single worker build with 1.34.0, so this is not a history from a mixed-version run. The same holds for every reproducer run below: each used one SDK version from start to finish.

    I ran Luca's repro_peritem.py (case A, n=900) against a local dev server (CLI 1.8.1, Server 1.31.2), two runs per SDK version. For each history I counted local activity markers whose complete_time is earlier than the start of the workflow task that recorded them. Every run had FORCE_CLOSE_COMMAND failures (6 to 14 per run).

    SDK "already has one" log lines (run 1 / run 2) markers completed before their WFT started replay
    1.31.0 0 / 0 0 / 0 ok / ok
    1.32.0 7 / 8 122 / 321 ok / NDE
    1.34.0 4 / 14 48 / 672 NDE / ok
    1.35.0 0 / 0 0 / 0 ok / ok

    These markers look the same as the ones in our production history. Markers 1296 and 1719 had a complete_time before the recording WFT started, and an original_schedule_time after their complete_time. In other words, local activities that ran during one workflow task were recorded by the next one.

    That fits temporalio/sdk-rust#1640. temporalio/sdk-rust#1442 (first shipped in 1.32.0) made a run that waits on a local activity look idle to Core, so a new workflow task arriving at that point replaced the outstanding one. #1640 (shipped in 1.35.0) buffers it instead. In our case the replacing task is the server's retry of the force-closed workflow task, not a query.

    My reading:

    • Affected: 1.32.0 through 1.34.0, for workflows that are waiting on a local activity when another workflow task arrives (here, the retry after FORCE_CLOSE_COMMAND).
    • Fixed in 1.35.0 for new executions.
    • Histories already written by an affected version can still fail to replay on 1.35.0. I confirmed Luca's result with the two failing histories above (one from 1.32.0, one from 1.34.0). We terminated our stuck execution.
    • Not every affected history fails replay: one 1.32.0 run and one 1.34.0 run replayed fine even with misplaced markers. That is probably why this depends so much on load and timing.

    If it helps, I can open a PR with a regression test for this path (a force-closed workflow task while a local activity is running), in whichever repo you prefer, sdk-python or sdk-rust.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions