In your previous reply you wrote:
… it gives you a controlled way to break the rule “child tasks end before parent tasks”.
If task = fiber
, then child fibers end before parent fibers
, which is indeed the case for Polyphony, so where is the problem here?
Let’s discuss the example you give for an HTTP server:
Now a request handler wants to spawn a background task that outlives this HTTP request. How does it do that?
There are multiple ways to achieve this, here’s one of the top of my head:
require 'polyphony'
$background_supervisor = spin { supervise } # wait for child fibers
def handle_request(req)
...
$background_supervisor.spin { ... }
...
end
So a background_supervisor
is a fiber that acts as a nursery. You can either keep it around as a global variable (as in the above example), or you can pass it as an argument, or use some other DI mechanism.
The same is true for primitives like go
or spin
– if you allow them anywhere, then you lose most of the structured concurrency benefits.
Using golang’s go
is not like using Polyphony’s spin
. Goroutines have no hierarchy, while fibers do. Goroutines have no error propagation mechanisms (that I know of, correct me if I’m wrong), while Polyphony fibers do. And if you call spin
at the wrong place, your program might fail.
It is true that in Polyphony you can have a situation where some nested method call can spin up a fiber without the caller knowing it. But my reasoning is that the alternative (having to explicitly pass nursery objects around) would prevent achieving one important design goal that I set for myself, which is that it should be possible to integrate Polyphony with the vast majority of the existing Ruby ecosystem. As I wrote previously, I prefer Polyphony be useful rather than “pure” or “correct”.
So this means that any operation can raise any arbitrary error from any unrelated child task, right?
Yes. How else would you want to be structured?
But it sounds like in Polyphony’s current design, if some other function spawned a task that’s doing some unrelated networking with an unrelated peer, then that might suddenly propagate out of my get
function, and be misinterpreted as a failure doing this HTTP request, and trigger the wrong response.
That is correct, unless the spawned task that’s doing unrelated stuff is spawned in the context of another controller fiber, as in the code example above. I mean, if you follow that line of reasoning, maybe we should all just implement checked exceptions à la Java
Thank you for this debate! Having to explain how Polyphony works and how it differs from Trio makes things clearer for me as well, and also shows me how Polyphony might be improved, especially regarding documentation and “developer education”.
BTW, you talked before about “happy eyeballs”, here’s a version of it in Polyphony, which demonstrates some of the issues we’ve been discussing. I’ll be happy to explain how this works:
require 'polyphony'
def try_connect(target, supervisor)
puts "trying #{target[2]}"
sleep rand * 0.2
socket = TCPSocket.new(target[2], 80)
puts "connected to #{target[2]}"
supervisor.schedule [target[2], socket]
rescue IOError, SystemCallError
# ignore error
end
def happy_eyeballs(hostname, port, max_wait_time: 0.010)
targets = Socket.getaddrinfo(hostname, port, :INET, :STREAM)
t0 = Time.now
fibers = []
supervisor = Fiber.current
spin do
targets.each do |t|
spin { try_connect(t, supervisor) }
sleep(max_wait_time)
end
suspend
end
target, socket = move_on_after(5) { suspend }
supervisor.shutdown_all_children
if target
puts format('success: %s (%.3fs)', target, Time.now - t0)
else
puts 'timed out'
end
end
happy_eyeballs('debian.org', 'https')