Deliver cross-thread task cancellation on the owning scheduler - #466
Deliver cross-thread task cancellation on the owning scheduler#466infiton wants to merge 1 commit into
Conversation
|
cc @tavianator @samuel-williams-shopify curious if ya'll think this is the right approach; talking with @tavianator in slack and he suggested that cross-thread cancellation should just not be supported |
Assisted-By: devx/018c26c0-16c2-4676-bd97-d1799cdb0afc
|
@infiton do you mind explaining the use case/scenario where you want to do this? |
60cac5e to
718f9fe
Compare
|
@ioquatix I actually don't want to do this on purpose; this was discovered as I was hunting a bug where in certain situations our app would go crazy and show these huge stacks of async cancellations. The issue was that we had a worker thread that was working off a queue of tasks but through a certain path the main thread was pushing tasks created on its scheduler into the work threads queue. I think this is probably an anti pattern and something best avoided so if you think it better to just fail fast and loudly that makes sense too (I don't see a super easy mechanism for that) |
Problem
Task#cancelcurrently raises throughFiber.scheduler, which is the caller's scheduler rather than necessarily the scheduler that owns the task's fiber.When cancellation originates from another reactor thread,
Fiber#raisefails withFiberError. The fallback then queuesCancel::Lateron the caller's scheduler, where it repeatedly retries the same impossible cross-thread raise.As a result:
task.cancel; task.waitdoes not complete.Cancel::Lateroperations.In the reproduction, this caused roughly 150,000 failed raises over three seconds.
Cross-thread cancellation must also remain on the owning thread during task finalization.
Task#finish!clears@fiberbefore removing the task from its parent's child list. If handoff depends on@fiber&.alive?, a foreign canceller can observe the cleared fiber and runcancel!/finish!concurrently with the owner, causing a double removal and task-tree corruption.Fix
While a task remains attached to another scheduler, enqueue
Cancel::Lateron that scheduler through its thread-safeunblockpath, regardless of the current value of@fiber.The owning reactor is woken and performs the complete existing cancellation operation on the correct thread. This serializes promise, child-tree, and finalization mutations with the task owner, preserves
defer_cancelbehavior and cancellation causes, and leaves same-reactor cancellation unchanged.Testing
Added regression coverage for both cases:
ensureblock executes, and the original cause is preserved.@fiberis cleared, cancels from another thread, and verifies there is no double finalization and the completed result is preserved.Additional local verification on Ruby 4.0.6: