Test server: refused CancelTimer leaves the workflow task complete in memory but STARTED in history, so it is never timed out or redelivered
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 42/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- java
調査の方向性
completeWorkflowTask とその update(...) 操作から始め、次に TestWorkflowMutableStateImpl.processCancelTimer、timeoutWorkflowTask、fireTimer を追跡します。コマンド処理で例外が発生したときに、workflow task のステートマシンとタイマーマップがどのように変化するかを確認します。完了の条件は、拒否された完了であっても timeout または WorkflowTaskFailed と再配信によって task を復旧可能な状態に保ち、説明されているタイマーの挙動に対処するか、明示的にスコープ外として扱うことです。
索引モデルが issue の本文から書いたものです。
説明
Expected Behavior
When RespondWorkflowTaskCompleted is refused because a command is invalid, the workflow task should be failed (WorkflowTaskFailed in history) and rescheduled, so the worker can replay and continue, which is what the real Temporal server does (for a cancel of an unknown timer it fails the task with BAD_CANCEL_TIMER_ATTRIBUTES; for a cancel of a timer whose TimerFired is still buffered, it drops the buffered fire and records TimerCanceled).
At minimum, a refused completion should leave the task in a state its own start-to-close timeout can act on.
Actual Behavior
A completion carrying a CancelTimer for a timer id the server no longer holds in its in-memory timers map is refused by TestWorkflowMutableStateImpl.processCancelTimer with:
code: INVALID_ARGUMENT, message: "invalid history builder state for action"
After that refusal the run never receives another workflow task. History ends
... TimerStarted -> WorkflowExecutionSignaled -> WorkflowTaskScheduled -> WorkflowTaskStarted
and stays there: no WorkflowTaskFailed, no WorkflowTaskTimedOut (the workflow's workflowTaskTimeout was 60 s; we observed 88 s with no event), no redelivery. sdk-core logs WARN Error while completing workflow activation and evicts the run; its subsequent polls return nothing. The workflow stays RUNNING forever.
From the source, the cause is the order of operations inside completeWorkflowTask's update(...):
workflowTaskStateMachine.action(StateMachines.Action.COMPLETE, ctx, request, 0);
for (Command command : commands) { processCommand(...); }
StateMachine.action assigns state immediately. When processCancelTimer throws, update rethrows and the RequestContext is discarded, but the state machine stays at NONE; nothing rolls it back. timeoutWorkflowTask then returns early on workflowTaskStateMachine.getState() == State.NONE, so the start-to-close timeout never produces WorkflowTaskTimedOut, and nothing schedules a new task. The same non-transactional pattern exists in fireTimer (timers.remove(timerId) inside the update lambda) and in processCancelTimer itself.
We could not establish from our side why the timer id was missing: it had been started by the immediately preceding completion 16 ms earlier, was one hour long, and the server's clock (read via getCurrentTime at the end of the test) had not jumped. The server cannot tell us either: the bundled binary carries no slf4j binding (logback-classic is testRuntimeOnly in temporal-test-server/build.gradle), so log.error("Failure firing a timer") and friends go to the NOP logger and nothing reaches stderr.
Related: #2127 (signal handling around the first WFT on the test server), #1377 (predictable log output from the test server).
Steps to Reproduce the Problem
We do not have a deterministic reproduction; it recurs at a low rate in CI. The shape that triggers it:
- Start the time-skipping test server (
TestWorkflowEnvironment.createTimeSkippingfrom the TypeScript SDK) and a workflow whose loop re-arms acondition(..., timeout)timer on every turn (workflow task timeout 60 s). - From the test,
unlockTimeSkippingWithSleepfor 2 days; the run processes ~48 hourly timers during the skip, sends a message via an activity, and its next dMarker x5, StartTimer(50)]`. - Immediately after that completion (history shows
TimerStarted -> WorkflowExecutionSignaled +1ms), signal the workflow. The worker answers the signal's taskelTimer(50), RecordMarker,ScheduleActivityTask, ScheduleActivityTask]`. - The server refuses it as above; the run is stranded.
sdk-core DEBUG trace from the failing run (comevent ids, e.g. the refused task wasHistoryUpdate(previous_started_event_id: 646, started_id: 656, length: 10)) is available on request.
Suggested fixes:
- Perform the workflow-task state-machine transition after the commands validate, or roll it back when a completion is refused, so a refused task stays
STARTEDand its timeout (or aWorkflowTaskFailed) redelivers it. - Treat
CancelTimerfor an already-fired-but-unseen timer the way the real server does (drop the bufferedTimerFired, recordTimerCanceled). - Ship a logging binding in the test-server binary, or route the test service's
log.errorto stderr, so a refused completion is diagnosable.
Specifications
- Version:
temporal-test-server-sdk-typescrnloaded by@temporalio/testing1.16.1); the code paths above are unchanged onmaster` as of 2026-09-17 - Platform: observed on GitHub Actions `ubun; the same sequence passes 25/25 locally on macOS 15 (Apple Silicon), so it is timing-dependent
- Client: TypeScript SDK 1.16.1 (sdk-core),
- 主要言語
- Java
- スター
- 434
- フォーク
- 260
- 平均マージ
- 2日 11時間
- マージ済み PR(30日)
- 19
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
temporalio/sdk-java のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
temporalio/sdk-java#3134 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
temporalio/sdk-java#1825 ·
メンテナーはふだん 1 日以内に返信
-
Proposal: handle SIGTERM by default to initiate graceful worker shutdown対応中かも @eamsden が 3 日前に担当しました。 オープン
temporalio/sdk-java#3135 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 4/5 3〜5日 初心者へのやさしさ 54/100
temporalio/sdk-java#3125 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 3/5 1〜2日 初心者へのやさしさ 55/100
temporalio/sdk-java#3124 ·
メンテナーはふだん 1 日以内に返信
temporalio/sdk-java の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
apache/rocketmq-dashboard#6331 ·
メンテナーはふだん 5 日以内に返信
-
status: team-only type: dependency-upgrade
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
spring-projects/spring-boot#52143 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
area/frontend good first issue kind/cooldown
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
kestra-io/kestra#20704 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信