[Bug] Fiber activity executor: cancelling an activity permanently strands the poller under a fiber scheduler
维护者通常 1 天内回复
@GregoryTravis 已经在做这个了。
开始于 2026年9月8日。
评估
这个 Issue 还没有评估数据。
描述
What are you really trying to do?
Run long-lived workflows whose activities spend most of their time awaiting slow external I/O (model/API calls, DB writes), on the fiber activity executor under an Async reactor so one worker serves many workflows concurrently. In this workload, newer input routinely supersedes work still in flight, so cancelling a running activity is an everyday event for us, not an edge case.
Describe the bug
Temporalio::Worker::ActivityExecutor::Fiber#set_activity_context cancels an
activity with Fiber#raise. Under a fiber scheduler that uses Fiber#transfer
to switch fibers (Async, the scheduler the fiber executor is designed to be used
with), Fiber#raise moves control INTO the target and the caller never gets it
back.
The caller is the worker's poller loop. So the first cancelled activity strands
it permanently: the reactor stays up, the process looks healthy, every thread is
idle, and the worker never processes another task on any workflow until it is
restarted. Nothing is raised, nothing is logged.
Where:
lib/temporalio/worker/activity_executor/fiber.rb:
def set_activity_context(defn, context)
::Fiber[:temporal_activity_context] = context
return unless defn.cancel_raise
fiber = ::Fiber.current
context&.cancellation&.add_cancel_callback do
fiber.raise(Error::CanceledError.new('Activity canceled'))
end
end
Why it happens:
Async drives task fibers with Fiber#transfer, and Fiber#raise on a
transferred fiber uses transfer semantics. Control moves into the target; when
the target next blocks it transfers to the SELECTOR, not back to the raiser. The
raiser is on no ready list and in no wait queue, so nothing ever resumes it.
In the worker this is reached from handle_cancel_task on the poller fiber, so
one cancelled activity ends the worker's ability to process anything. For an
workload where a workflow routinely cancels an in-flight activity
(a user sending a second message while a step is running), this is an ordinary
path rather than an edge case.
Expected:
Cancelling a fiber-executor activity raises CanceledError in the activity and
leaves the canceller running.
Minimal Reproduction
No Temporal server needed. This is the executor's cancellation shape reduced to
two fibers under Async.
require "async"
Async do |task|
target = Fiber.schedule do
sleep(5)
rescue => e
$stderr.puts "TARGET: rescued #{e.class}"
sleep(0.05) # the activity reports its completion here
$stderr.puts "TARGET: finished"
end
task.sleep(0.2)
$stderr.puts "RAISER: raising"
target.raise(RuntimeError.new("cancel"))
$stderr.puts "RAISER: control came back" # NEVER PRINTS
end
$stderr.puts "REACTOR: exited" # NEVER PRINTS
Observed (timeout 60 ruby repro.rb; exits 124):
RAISER: raising
TARGET: rescued RuntimeError
TARGET: finished
RAISER: control came back and REACTOR: exited never print. The raiser is
suspended with nothing holding a reference to it, and the reactor never winds
down.
The second sleep in the target is load-bearing and is not artificial: a real
activity, after unwinding, reports its completion through the bridge, which
blocks. That is what transfers control to the selector instead of back to the
raiser. A target that unwinds and returns immediately can mask the bug.
Environment/Versions
Reproduced independently on two stacks:
| Ruby | temporalio | async | |
|---|---|---|---|
| Reporter | 3.4.7 | 1.7.0 | 2.45.1 |
| Second reproduction | 3.4.7 | 1.6.0 | 2.39.0 |
The stock executor is unchanged in 1.7.0, the latest release at the time of
writing, so this is not fixed upstream.
Additional context
Fix that works for us (shipped as an application-level prepend):
Route the raise through the scheduler when it offers one. Async's own
cancellation (Async::Task#cancel) does exactly this rather than calling
Fiber#raise directly:
context&.cancellation&.add_cancel_callback do
error = Error::CanceledError.new('Activity canceled')
scheduler = ::Fiber.scheduler
if scheduler.respond_to?(:raise)
scheduler.raise(fiber, error)
else
fiber.raise(error)
end
end
Async::Scheduler#raise delegates to the selector, and the selector's
implementation is the reason this is correct rather than merely different
(io-event, Selector#raise):
def raise(fiber, *arguments, **options)
optional = Optional.new(Fiber.current)
@ready.push(optional) # enqueue the CALLER ...
fiber.raise(*arguments, **options) # ... and only then transfer
ensure
optional.nullify
end
The caller is put on the event loop's ready list before it gives up control, so
when the target eventually blocks and transfers to the selector, the selector
has the caller to resume. Nothing about the bare raise supplies that.
Running the reproduction above through Fiber.scheduler.raise(target, ...)
prints all four lines and exits 0.
Note that Fiber::Scheduler#raise is not a Ruby scheduler hook, so the
respond_to? guard matters: a scheduler that does not offer one keeps today's
behaviour rather than crashing.
Workaround:
Prepending the corrected set_activity_context onto
Temporalio::Worker::ActivityExecutor::Fiber works and covers
Temporalio::Testing::ActivityEnvironment as well, since it resolves the same
executor instance. Passing a corrected subclass through Worker.new's
activity_executors: option also works but reaches only workers you construct;
ActivityEnvironment takes its own separate activity_executors: keyword and
does not see the worker's.
- 主要语言
- Ruby
- 星标
- 206
- 派生
- 42
- 平均合并
- 1 天 48 分钟
- 30 天内合并 PR
- 16
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
temporalio/sdk-ruby 的其他 Issue
-
[Feature Request] Add thread lifecycle hook to `Worker::ThreadPool`可能重新可做 @GregoryTravis 于 36 天前认领,目前没有进行中的 PR。 未关闭enhancement
temporalio/sdk-ruby#563 · 已指派 1 人 ·
维护者通常 1 天内回复
-
[Feature Request] Implement operator commands for Standalone Activities可能重新可做 @GregoryTravis 于 86 天前认领,目前没有进行中的 PR。 未关闭enhancement
temporalio/sdk-ruby#440 · 已指派 1 人 ·
维护者通常 1 天内回复
-
难度 3/5 1-2 天 新手友好度 45/100
temporalio/sdk-ruby#401 ·
维护者通常 1 天内回复
-
enhancement
难度 5/5 一周以上 新手友好度 20/100
temporalio/sdk-ruby#393 ·
维护者通常 1 天内回复
-
[Feature Request] Allow overriding of the Worker Deployment Version when invoking a Child Workflow未关闭enhancement
难度 4/5 3-5 天 新手友好度 35/100
temporalio/sdk-ruby#361 ·
维护者通常 1 天内回复
查看 temporalio/sdk-ruby 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 84/100
-
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 90/100
维护者通常 3 天内回复
-
L: docker L: elm L: github:actions L: helm L: ruby:bundler
难度 2/5 1-3 小时 新手友好度 85/100
dependabot/dependabot-core#16425 ·
维护者通常 2 天内回复
-
enhancement
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 2 天内回复