Skip to content

Deliver cross-thread task cancellation on the owning scheduler - #466

Open
infiton wants to merge 1 commit into
socketry:mainfrom
infiton:fix/cross-thread-task-cancellation
Open

Deliver cross-thread task cancellation on the owning scheduler#466
infiton wants to merge 1 commit into
socketry:mainfrom
infiton:fix/cross-thread-task-cancellation

Conversation

@infiton

@infiton infiton commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Task#cancel currently raises through Fiber.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#raise fails with FiberError. The fallback then queues Cancel::Later on the caller's scheduler, where it repeatedly retries the same impossible cross-thread raise.

As a result:

  • The target task is never cancelled.
  • task.cancel; task.wait does not complete.
  • The caller's reactor busy-spins, repeatedly allocating exceptions and Cancel::Later operations.

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 @fiber before removing the task from its parent's child list. If handoff depends on @fiber&.alive?, a foreign canceller can observe the cleared fiber and run cancel!/finish! concurrently with the owner, causing a double removal and task-tree corruption.

Fix

While a task remains attached to another scheduler, enqueue Cancel::Later on that scheduler through its thread-safe unblock path, 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_cancel behavior and cancellation causes, and leaves same-reactor cancellation unchanged.

Testing

Added regression coverage for both cases:

  • A separate reactor thread cancels and waits for a task, verifying both reactors terminate, the target is cancelled, its ensure block executes, and the original cause is preserved.
  • A deterministic finalization interleaving pauses the owner after @fiber is 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:

  • 540 tests, 1,198 assertions.
  • 147 RuboCop files, no offenses.
  • 500 finalization-race runs and 200 original reproducer runs without a crash or failure.

@infiton

infiton commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

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
@ioquatix

ioquatix commented Aug 5, 2026

Copy link
Copy Markdown
Member

@infiton do you mind explaining the use case/scenario where you want to do this?

@infiton
infiton force-pushed the fix/cross-thread-task-cancellation branch from 60cac5e to 718f9fe Compare August 5, 2026 22:35
@infiton

infiton commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@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)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants