Polyphony: Fine-Grained Concurrency for Ruby
github.com
github.com
I am interested in how concurrency can be represented elegantly and efficiently, so I am interested in how libraries can simplify async and make it easier to reason about and write
What kind of code do you want to be capable of writing? Any imaginary syntax you can think of?
The libev and ioring support is great for IO scalability (https://github.com/enki/libev not sure if this is the official repo)
In Python I use the "select" module and use epoll on Linux.
I am currently thinking of designing an API that allows the registration of epoll-like listeners to arbitrary objects, including business objects, so you can efficiently register a listener on multiple behaviours of multiple arbitrary objects.
I wrote an async/await simulation in Java and my scheduler is really simple, it's just a for loop that checks to see if there are any tasks that can progress. I notice the switch_fiber in polyphony must do something similar. This is similar to a yield in a coroutine.
My async/await simulation takes the following program:
task1:
while True:
handle1 = async task2();
handle2 = async task3();
print(await handle1)
print(await handle2)
task2:
n = 0
while True:
yield n++
task3:
n = 0
while True:
yield n++
I had someone on HN explain to me how async works in Rust, but I've not written enough Rust to truly understand async in rust. https://news.ycombinator.com/item?id=33592334This was the main driving force behind the development of Polyphony. I've been exploring all the different ways of expressing concurrent operations: callbacks, promises, async/await. Each of these solutions has its drawbacks: callbacks make you split your app logic across different functions; promises are a somewhat better API but do not solve the split logic problem; async/await potentially leads to two types of functions (the "what color is your function" problem). All three kind of break Ruby's exception handling model.
In contrast, Polyphony relies on Ruby fibers, which provide a base for implementing cooperative concurrency, where each fiber does its work, yielding execution whenever it performs a blocking operation, such as reading from a socket, or waiting for a timeout. In addition, fibers are arranged into a tree where each fiber is considered the child of the fiber from which it was spawned, and is guaranteed to stop executing before its immediate parent stops executing. Any uncaught exception raised by a fiber will be propagated up the fiber tree. This implements a pattern known as Structured Concurrency [1]. Fibers can communicate by sending each other messages, in a similar fashion to Erlang/Elixir processes.
There's no direct equivalent in Polyphony to the program you included, which seems to implement two generators running asynchronously. But the following does something similar.
def number_generator
n = 0
loop do
client = receive
client << (n += 1)
end
end
task1 = spin { number_generator }
task2 = spin { number_generator }
10.times do
task1 << Fiber.current
puts "next number from task1: #{receive}"
task2 << Fiber.current
puts "next number from task2: #{receive}"
end
So in the above program, two fibers are spun, each running the same number generator, which repeatedly waits for a message which is actually the fiber of the "client", and sends the next number to the client. In Python there's the Trio library [2], which implements structured concurrency, and which was actually one of the main inspirations for developing Polyphony.
[1] https://en.wikipedia.org/wiki/Structured_concurrency
[2] https://github.com/python-trio/trioI would wonder if pipelines, and Kafka style and functional reactive programming and reactive designs could be adapted to be async.
I would love to know how you would approach the design of a fiber application.
From my limited understanding of fibers, I would start by drawing a diagram, probably a tree or graph of all the components and draw links between the connections between fibers and components.
From what I understand of the code, you can send fibers to other fibers with the "<<" operator.
This reminds me of Golang's channels and send and receive operators on channels.
I notice the use of the word "spin" to "create a fiber" which seems to be an analogy of fibers being literal fibers or strings that are attached to eachother and "pull" and "push".
I asked the following two questions on Stackoverflow.
What's the most efficient way to fork and join control flow multiple times in a multithreaded program?
https://stackoverflow.com/questions/75866966/whats-the-most-...
A while back I was trying to understand Python's asyncio and I asked this question:
What's the canonical approach to creating multiple producers and consumers with coroutines in python asyncio? https://stackoverflow.com/questions/74420108/whats-the-canon...
I feel that wiring up asynchronous system components is kind of not ideal because you have dependencies have to refer to eachother and you don't want to solidify dependencies because you might want to reuse components.
This article highlights that problem of wiring up coroutines/fibers in the right order and which side refers to which. https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
I wonder if there could be a pattern where every fiber receives a sequence of fibers that "configure" the fiber, like a calling convention of a method, this way runtime configuration can be separate from the definition of asynchronous an pipeline.
In my original code sample, my code refers to the children tasks. In your code sample, the parent refers to the children tasks. This seems to be a property of structured concurrency.
You can pass any data between fibers using "receive" and "<<", including fiber refs.
> In my original code sample, my code refers to the children tasks. In your code sample, the parent refers to the children tasks. This seems to be a property of structured concurrency.
In general, yes, but in Polyphony at least you can easily walk the fiber tree using "Fiber#parent" and "Fiber#children".
That's assuming Polyphony is aiming to replace most or all of it.
Not really. Concurrent-Ruby implements a lot of different concurrency patterns. Polyphony has a much smaller scope, though I imagine you could reimplement a lot of concurrent-ruby on top of Polyphony. I think there's a place for the two in the Ruby ecosystem.
Dis they expect everyone to implement the scheduler om their own?
I am aware of the existing fiber scheduler implementations you can get on Ruby Gems but there should be official implementation.
- code examples
- motivation for building
- what I should try this for vs the alternatives
That is utterly incorrect. Make it s/Ruby/Rails and you'd be somewhat more correct. Cue Rack, Sinatra, Hanami, Roda, and that's just web "frameworks".
> all of your dependency tree (three quarters of which probably had its last release in 2017)
I don't see how that could make sense. Some libraries can be mature and thus not warrant much, if any, update.
Also, the aforementioned pattern is only present on a small subset of Ruby gems, usually ones that are close in mindset to Rails.
Nothing prevents anyone to code in Ruby in a functional-like style, nor pick dependencies that are written with such sensibility. But if you wilfully pick Rails you can't really expect to veer too far from the philosophy that comes with it.
I can see it being useful for many projects, web or non-web.