[ruby-core:126158] [Ruby Bug#22211] Fiber::Scheduler stalls on Ractor::Port.receive
Issue #22211 has been reported by rkh (Konstantin Haase). ---------------------------------------- Bug #22211: Fiber::Scheduler stalls on Ractor::Port.receive https://bugs.ruby-lang.org/issues/22211 * Author: rkh (Konstantin Haase) * Status: Open * ruby -v: ruby 4.1.0dev (2026-07-24T12:18:38Z master 180b664bd9) +PRISM [arm64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- Quickly upfront: This might be a duplicate of #20276, and let me know if the official solution is still to launch a thread for each port so it can block (or add some more complex thread orchestration). Anyway, I think `Ractor::Port#receive` should call `Fiber::Scheduler#block`/`Fiber::Scheduler#unblock` if a scheduler is set (or some other hook), the way that `Thread::Queue` does. Instead, it completely blocks the scheduler. Example to reproduce: ``` ruby Warning[:experimental] = false class Scheduler def initialize @fiber = Fiber.current @blocked = Set.new @ready = Queue.new end def scheduler_close @ready.pop.transfer while @blocked.any? end def block(blocker, timeout = nil) raise NotImplementedError, "block: timeout not supported" if timeout @blocked << Fiber.current @fiber.transfer ensure @blocked.delete(Fiber.current) end def unblock(blocker, fiber) = @ready << fiber def io_write(io, buffer, offset, length) = Thread.new { buffer.write(io, offset, length) }.value def fiber(&) = Fiber.new(blocking: false, &).tap(&:transfer) def respond_to_missing?(...) = true def method_missing(method, ...) = raise NotImplementedError, "#{method}: not implemented" end Fiber.set_scheduler(Scheduler.new) # This would work: # # queue = Queue.new # Fiber.schedule { puts queue.pop } # queue.push "Hello from Queue!" port = Ractor::Port.new Fiber.schedule { puts port.receive } port.send("Hello from Ractor::Port!") # never gets here ``` -- https://bugs.ruby-lang.org/
Issue #22211 has been updated by luke-gru (Luke Gruber). The tricky part in integrating the fiber scheduler with `Ractor::Port` `send/receive` is what to do when you want to wakeup (unblock) a fiber on another Ractor. Normally unblocking a fiber is done on the same Ractor and it's done in a context where you can call directly into `rb_fiber_scheduler_unblock`. When the Fiber Scheduler is on a different Ractor, you can't call its unblock method directly (and a Ruby thread may not even be running on that Ractor. It could be in a syscall, etc.). What we could do is have a special high priority Ruby thread on each Ractor that only calls Fiber Scheduler unblock methods. When you send on a port, you queue up the scheduler unblock operation on that thread and then set the thread to `ready`. The thread scheduler always checks if this thread is ready before dequeuing a real thread, and if so it runs the unblock method. Even that's not ideal because it would go through a thread switch and a fiber switch every time you send to a Ractor that has a Fiber Scheduler, but it's the best I can come up with right now. I created a branch [here](https://github.com/luke-gruber/ruby/tree/port_send_receive_fiber_scheduler) just for experimentation. Right now it creates a new thread on each `Port#send` (not ideal). I'll work on caching the thread soon. Scheduler work is not our priority at the moment as we're looking into making GC faster with Ractors (ractor-local GC). However I might work on this a bit if I have some time. ---------------------------------------- Bug #22211: Fiber::Scheduler stalls on Ractor::Port.receive https://bugs.ruby-lang.org/issues/22211#change-118262 * Author: rkh (Konstantin Haase) * Status: Open * Assignee: ractor * ruby -v: ruby 4.1.0dev (2026-07-24T12:18:38Z master 180b664bd9) +PRISM [arm64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- Quickly upfront: This might be a duplicate of #20276, and let me know if the official solution is still to launch a thread for each port so it can block (or add some more complex thread orchestration). Anyway, I think `Ractor::Port#receive` should call `Fiber::Scheduler#block`/`Fiber::Scheduler#unblock` if a scheduler is set (or some other hook), the way that `Thread::Queue` does. Instead, it completely blocks the scheduler. Example to reproduce: ``` ruby Warning[:experimental] = false class Scheduler def initialize @fiber = Fiber.current @blocked = Set.new @ready = Queue.new end def scheduler_close @ready.pop.transfer while @blocked.any? end def block(blocker, timeout = nil) raise NotImplementedError, "block: timeout not supported" if timeout @blocked << Fiber.current @fiber.transfer ensure @blocked.delete(Fiber.current) end def unblock(blocker, fiber) = @ready << fiber def io_write(io, buffer, offset, length) = Thread.new { buffer.write(io, offset, length) }.value def fiber(&) = Fiber.new(blocking: false, &).tap(&:transfer) def respond_to_missing?(...) = true def method_missing(method, ...) = raise NotImplementedError, "#{method}: not implemented" end Fiber.set_scheduler(Scheduler.new) # This would work: # # queue = Queue.new # Fiber.schedule { puts queue.pop } # queue.push "Hello from Queue!" port = Ractor::Port.new Fiber.schedule { puts port.receive } port.send("Hello from Ractor::Port!") # never gets here ``` -- https://bugs.ruby-lang.org/
Issue #22211 has been updated by rkh (Konstantin Haase). Interesting! I've been playing with workarounds in Ruby-land where I have up to one extra thread per Ractor, which executes `Ractor.select(coordinating_port, *ports)` with a backlog of waiting fibers per port, and then uses a `ConditionalVaribale`/`Mutext` to actually block the fiber. Not trivial and needs a wrapper around `Port.receive` and `Ractor.select`, but better than doing `Thread.new { port.receive }.value`. The nice thing is that the thread can be launched lazily, since `receive` can check whether it is being called from within a non-blocking fiber. Also, I understand if this isn't a priority now, but generally think that at some point this would be great to address. ---------------------------------------- Bug #22211: Fiber::Scheduler stalls on Ractor::Port.receive https://bugs.ruby-lang.org/issues/22211#change-118265 * Author: rkh (Konstantin Haase) * Status: Open * Assignee: ractor * ruby -v: ruby 4.1.0dev (2026-07-24T12:18:38Z master 180b664bd9) +PRISM [arm64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- Quickly upfront: This might be a duplicate of #20276, and let me know if the official solution is still to launch a thread for each port so it can block (or add some more complex thread orchestration). Anyway, I think `Ractor::Port#receive` should call `Fiber::Scheduler#block`/`Fiber::Scheduler#unblock` if a scheduler is set (or some other hook), the way that `Thread::Queue` does. Instead, it completely blocks the scheduler. Example to reproduce: ``` ruby Warning[:experimental] = false class Scheduler def initialize @fiber = Fiber.current @blocked = Set.new @ready = Queue.new end def scheduler_close @ready.pop.transfer while @blocked.any? end def block(blocker, timeout = nil) raise NotImplementedError, "block: timeout not supported" if timeout @blocked << Fiber.current @fiber.transfer ensure @blocked.delete(Fiber.current) end def unblock(blocker, fiber) = @ready << fiber def io_write(io, buffer, offset, length) = Thread.new { buffer.write(io, offset, length) }.value def fiber(&) = Fiber.new(blocking: false, &).tap(&:transfer) def respond_to_missing?(...) = true def method_missing(method, ...) = raise NotImplementedError, "#{method}: not implemented" end Fiber.set_scheduler(Scheduler.new) # This would work: # # queue = Queue.new # Fiber.schedule { puts queue.pop } # queue.push "Hello from Queue!" port = Ractor::Port.new Fiber.schedule { puts port.receive } port.send("Hello from Ractor::Port!") # never gets here ``` -- https://bugs.ruby-lang.org/
Issue #22211 has been updated by rkh (Konstantin Haase). luke-gru (Luke Gruber) wrote in #note-2:
The tricky part in integrating the fiber scheduler with `Ractor::Port` `send/receive` is what to do when you want to wakeup (unblock) a fiber on another Ractor.
Would it be possible to add something like `ractor_receive`/`ractor_select`/`ractor_value` to the `Fiber::Scheduler` interface instead, with the wakeup being the scheduler's responsibility? Similar to how dealing with IO has explicit callbacks. That way, having a thread that can be woken up would be the scheduler's responsibility, with a naive implementation being something like this: ```ruby def ractor_select(*args) = Thread.new { Ractor.select(*args) }.value def ractor_receive(port) = Thread.new { port.receive }.value def ractor_value(ractor) = Thread.new { ractor.value }.value ``` That way, the scheduler could implement a single background thread itself running `Ractor.select(...)` (again, similar to IO). ---------------------------------------- Bug #22211: Fiber::Scheduler stalls on Ractor::Port.receive https://bugs.ruby-lang.org/issues/22211#change-118592 * Author: rkh (Konstantin Haase) * Status: Open * Assignee: ractor * ruby -v: ruby 4.1.0dev (2026-07-24T12:18:38Z master 180b664bd9) +PRISM [arm64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- Quickly upfront: This might be a duplicate of #20276, and let me know if the official solution is still to launch a thread for each port so it can block (or add some more complex thread orchestration). Anyway, I think `Ractor::Port#receive` should call `Fiber::Scheduler#block`/`Fiber::Scheduler#unblock` if a scheduler is set (or some other hook), the way that `Thread::Queue` does. Instead, it completely blocks the scheduler. Example to reproduce: ``` ruby Warning[:experimental] = false class Scheduler def initialize @fiber = Fiber.current @blocked = Set.new @ready = Queue.new end def scheduler_close @ready.pop.transfer while @blocked.any? end def block(blocker, timeout = nil) raise NotImplementedError, "block: timeout not supported" if timeout @blocked << Fiber.current @fiber.transfer ensure @blocked.delete(Fiber.current) end def unblock(blocker, fiber) = @ready << fiber def io_write(io, buffer, offset, length) = Thread.new { buffer.write(io, offset, length) }.value def fiber(&) = Fiber.new(blocking: false, &).tap(&:transfer) def respond_to_missing?(...) = true def method_missing(method, ...) = raise NotImplementedError, "#{method}: not implemented" end Fiber.set_scheduler(Scheduler.new) # This would work: # # queue = Queue.new # Fiber.schedule { puts queue.pop } # queue.push "Hello from Queue!" port = Ractor::Port.new Fiber.schedule { puts port.receive } port.send("Hello from Ractor::Port!") # never gets here ``` -- https://bugs.ruby-lang.org/
participants (2)
-
luke-gru (Luke Gruber) -
rkh (Konstantin Haase)