[Bug] Local activity resolutions regrouped on replay since 1.32.0, delivering the wrong payload
I maintainer di solito rispondono entro 1 giorno
@Sushisource ci sta già lavorando.
Dal 28/9/2026.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 52/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- python
- Ambito
- backend, distributed-systems, testing
Direzione di ricerca
Inizia dai percorsi di risoluzione e pianificazione indicati in temporalio/worker/_workflow_instance.py, in particolare _apply_resolve_activity e _outbound_schedule_activity, quindi esegui la riproduzione minima con le versioni da 1.31.0 a 1.33.0. Traccia il raggruppamento delle activation nell'esecuzione live e nel replay e verifica che ogni attività locale riceva il payload registrato e che gli history vengano riprodotti correttamente.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What are you really trying to do?
Replaying histories from a workflow that starts several local activities, waits with return_when=FIRST_COMPLETED, and runs a further local activity while others are still pending. This replayed clean on 1.31.0 and stopped on 1.32.0.
Describe the bug
On 1.32.0 and 1.33.0, a history fails to replay under the same SDK version that recorded it. A local activity's recorded result is delivered to a different local activity's handle, and the payload converter raises on the type mismatch:
RuntimeError: Failed decoding arguments
caused by TypeError: Expected value to be str, was <class 'bool'>
temporalio/worker/_workflow_instance.py:900 _apply_resolve_activity
temporalio/worker/_workflow_instance.py:2358 _convert_payloads
temporalio/converter/_payload_converter.py:871 value_to_type
Live and replay group the resolutions into different activations. Tracing activate, _outbound_schedule_activity and _apply_resolve_activity on 1.33.0:
live ACTIVATE [('resolve_activity', 1)]
live RESOLVE 1 work b'"0"'
live SCHEDULE 4 expired
live ACTIVATE [('resolve_activity', 2), ('resolve_activity', 3)]
live RESOLVE 2 work b'"1"'
live RESOLVE 3 work b'"2"'
live ACTIVATE [('resolve_activity', 4)]
live RESOLVE 4 expired b'false'
live SCHEDULE 5 finish
replay ACTIVATE [('resolve_activity', 1), ('resolve_activity', 2), ('resolve_activity', 3)]
replay RESOLVE 1 work b'"0"'
replay RESOLVE 2 work b'"1"'
replay RESOLVE 3 work b'"2"'
replay SCHEDULE 4 finish
replay RESOLVE 4 finish b'false'
Live resolves seq 1 on its own, so the expired activity takes seq 4 and finish takes seq 5. Replay resolves 1, 2 and 3 in a single activation, so finish takes seq 4 and consumes the bool that was recorded for expired.
There is no custom payload converter, no interceptor, no sandbox and no data converter involved. value_to_type is byte-identical between 1.31.0 and 1.33.0 (sha256 32dfc9701260bb70a15f3a83442e065aaacffde874a0335487b0d2e48361a74e), so this is resolution delivery rather than conversion.
Minimal Reproduction
import asyncio
import sys
import uuid
from datetime import timedelta
from pathlib import Path
import temporalio
from temporalio import activity, workflow
from temporalio.client import WorkflowHistory
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Replayer, UnsandboxedWorkflowRunner, Worker
@activity.defn
async def work(index: int) -> str:
return str(index)
@activity.defn
async def expired() -> bool:
return False
@activity.defn
async def finish() -> str:
return 'finished'
@workflow.defn
class Fanout:
@workflow.run
async def run(self) -> str:
pending = {workflow.start_local_activity(work, i, start_to_close_timeout=timedelta(seconds=10))
for i in range(3)}
while pending:
done, pending = await workflow.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for task in done:
await task
if pending:
await workflow.execute_local_activity(expired, start_to_close_timeout=timedelta(seconds=10))
return await workflow.execute_local_activity(finish, start_to_close_timeout=timedelta(seconds=10))
async def main():
print('SDK', temporalio.__version__, flush=True)
replayer = Replayer(workflows=[Fanout], workflow_runner=UnsandboxedWorkflowRunner())
if len(sys.argv) > 1:
for filename in sys.argv[1:]:
try:
await replayer.replay_workflow(WorkflowHistory.from_json('minimal', Path(filename).read_text()))
print('MINIMAL_CROSS', filename, 'PASS', flush=True)
except Exception as error:
print('MINIMAL_CROSS', filename, 'FAIL', type(error).__name__, str(error), flush=True)
return
async with await WorkflowEnvironment.start_time_skipping() as server:
async with Worker(server.client, task_queue='minimal', workflows=[Fanout],
activities=[work, expired, finish], workflow_runner=UnsandboxedWorkflowRunner()):
for trial in range(10):
handle = await server.client.start_workflow(Fanout.run, id=str(uuid.uuid4()), task_queue='minimal')
result = await handle.result()
history = await handle.fetch_history()
try:
await replayer.replay_workflow(history)
print('MINIMAL', trial, 'LIVE', result, 'REPLAY PASS', flush=True)
except Exception as error:
print('MINIMAL', trial, 'LIVE', result, 'REPLAY FAIL', type(error).__name__, str(error), flush=True)
if __name__ == '__main__':
asyncio.run(main())
Run it with no arguments. It starts a time-skipping environment, runs the workflow 10 times, and replays each history immediately after the live run finishes.
Environment/Versions
1.31.0 10 REPLAY PASS, 0 REPLAY FAIL
1.32.0 0 REPLAY PASS, 10 REPLAY FAIL
1.33.0 0 REPLAY PASS, 10 REPLAY FAIL
Python 3.11 on Linux, time-skipping WorkflowEnvironment, UnsandboxedWorkflowRunner. Each version was run in its own virtualenv with nothing else installed beyond temporalio and its dependencies.
Additional context
The bisect lands on 1.32.0, which carries sdk-rust#1442, "Allow LAs to wake up workflow while another is still executing", merged 2026-08-04. That PR describes replay as preserving the live grouping. The trace above looks like a counterexample to that.
This is related to #1578 but points the other way. The shape in that issue never replayed clean, while this one replayed clean on 1.31.0 and regressed on the release carrying the change, and the symptom is a misrouted payload rather than a sequence mismatch.
Histories recorded on 1.32.0 and 1.33.0 also fail replay under the SDK version that recorded them, so this is not only a concern for histories written before an upgrade.
- Lingua principale
- Python
- Stelle
- 1.2k
- Fork
- 245
- Merge medio
- 4g 13h
- PR unite (30g)
- 50
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di temporalio/sdk-python
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
temporalio/sdk-python#1897 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
temporalio/sdk-python#496 ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug] Heartbeat Task Slot information is not pulled when MetricBuffer is configuredForse già presa @Sushisource l’ha presa 24 giorni fa. Apertabug
temporalio/sdk-python#1817 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Cloud CI Skips Nexus TestsForse di nuovo libera @tconley1428 l’ha presa 52 giorni fa e non c’è nessuna pull request aperta. Aperta
temporalio/sdk-python#1704 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Windows ARM64 wheel supportAperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
temporalio/sdk-python#1592 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di temporalio/sdk-python
Issue simili
-
upstream update
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
conan-io/conan-center-index#31098 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
john-kurkowski/tldextract#382 ·
-
comp/tools duplicate P2 sweeper:risk-compatibility tool/mcp type/bug
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
NousResearch/hermes-agent#132042 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
deepset-ai/haystack#13092 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 85/100
feder-cr/invisible_playwright_mcp#1408 ·
I maintainer di solito rispondono entro 1 giorno