Feature #20276
closedIntroduce Fiber interfaces for Blocking operations on Ractors
Description
Motivation¶
I am trying to build a web server with Ractors. The lifecycle for a request in the current implementation is
- main ractor sends request to worker ractor
- worker ractor handles response
- worker ractor sends response to main ractor
- main ractor writes response
- repeat
The main ractor utilizes the Async gem (specifically async-http) to handle connections concurrently, meaning each request is handled on a separate fiber.
The issue I am running into is after I send a request to a worker ractor, I need to do a blocking wait until I receive a response.
While I am waiting for the response, I cannot take any more connections.
Solution¶
If the fiber scheduler had a hook for Ractor.receive
or Ractor#take
(both of which are blocking), the main ractor can send the message, handle other connections while the worker processes the request. When the worker produces a message, it will then take the reqeust and write it in the socket. Specifically, I think the block
and unblock
hooks should be implemented for Ractors, considering Threads and Mutexes already use them.
Updated by forthoney (Seong-Heon Jung) 10 months ago
- Subject changed from Introduce Fiber interfaces for Ractors to Introduce Fiber interfaces for Blocking operations on Ractors
- Description updated (diff)
Updated by ko1 (Koichi Sasada) 10 months ago
- Status changed from Open to Feedback
I understand the motivation but now the ractor-thread combination is not well-defined yet and combination with Fiber (scheduler) is also earlier (because of Ractor's specification and implementation).
Updated by forthoney (Seong-Heon Jung) 10 months ago
ko1 (Koichi Sasada) wrote in #note-2:
I understand the motivation but now the ractor-thread combination is not well-defined yet and combination with Fiber (scheduler) is also earlier (because of Ractor's specification and implementation).
I actually tried writing out some code to picture how this might be implemented, and I understand the complexity now. In the current implementation rb_fiber_scheduler_block/unblock
, if ractor_a
were to call ractor_b.take
we would need the following process:
-
ractor_a
emitsblock
event toractor_a
's scheduler -
ractor_a
's current fiber yields, and different fiber runs onractor_a
-
ractor_b
calls yield -
ractor_b
emitsunblock
event toractor_a
's scheduler
With the strict object sharing rules between ractors, I can see why it may require a major rewrite to make Ractor blocking operations a fiber-scheduler event.
How is this alternative?
Instead of relying on automatic scheduling via Fiber scheduler events, we can rely a bit on programmer intervention.
If there is a method like Ractor#take_nonblock
which works similar to IO#read_nonblock
, the programmer can write code like
loop do
msg = other_ractor.take_nonblock
rescue Errno::EAGAIN
task.yield
retry
end
This may not be 100% efficient since the scheduler would need to continuously "ask", but better than this not being possible at all.
Updated by ioquatix (Samuel Williams) 10 months ago
I support this proposal.
A simple way to deal with this right now might be the following code (I have not tested it):
Thread.new do
other_ractor.take
end.value
In general, the block
/unblock
operations should be sufficient, but it might require the ability for Ractor
to invoke functionality across it's boundary OR we might need to implement some RPC mechanism (this is common in Actor based concurrency).
Updated by forthoney (Seong-Heon Jung) 10 months ago
ioquatix (Samuel Williams) wrote in #note-4:
I support this proposal.
A simple way to deal with this right now might be the following code (I have not tested it):
Thread.new do other_ractor.take end.value
In general, the
block
/unblock
operations should be sufficient, but it might require the ability forRactor
to invoke functionality across it's boundary OR we might need to implement some RPC mechanism (this is common in Actor based concurrency).
Tested on locally and it seems to work, at least with Async. Here's my code.
Async do |task|
1.upto(3) do
r = Ractor.new do
Ractor.recv
fib = ->(x) { x < 2 ? 1 : fib.call(x - 2) + fib.call(x - 1) }
puts "fin"
end
task.async do
Thread.new do
r.send(nil) # ractor start
r.take
end.value
end
end
end
Not very scientific but the results are printed at roughly the same time.
Updated by forthoney (Seong-Heon Jung) 10 months ago
On a side note, I may have unintentionally discovered an IRB bug in the process. If you run the above in IRB and use Ctrl-C to exit, IRB hangs and becomes unresponsive.
Updated by ioquatix (Samuel Williams) 10 months ago
If I had to take a guess, I'd say it's a bug with waiting on a Ractor while handling signals?
Updated by forthoney (Seong-Heon Jung) 9 months ago
ioquatix (Samuel Williams) wrote in #note-7:
If I had to take a guess, I'd say it's a bug with waiting on a Ractor while handling signals?
Under closer inspection, the Thread#join
workaround should work in theory but actually deadlocks in practice, likely due to an implementation bug. Here's the relevant bug report. IRB is likely unresponsive because of this deadlock.