noteflakes

๐Ÿ‡ต๐Ÿ‡ธ

Stop the war, stop the occupation!

ื“ื™ ืœืžืœื—ืžื”, ื“ื™ ืœื›ื™ื‘ื•ืฉ!

28ยท06ยท2025

Introducing UringMachine

UringMachine is a new Ruby gem for performing concurrent I/O using io_uring and Ruby fibers. In UringMachine I’ve implemented all the lessons I’ve learned from my previous attempts at bringing io_uring to Ruby: Polyphony, a comprehensive gem providing io_uring functionality, structured concurrency, and monkey-patching for the Ruby standard library; and IOU, a low level async API for using io_uring from Ruby.

Here are some of those lessons:

Monkey-patching

I’ve defended monkey-patching before, but my stance now is more nuanced. Frankly, it’s a big pain to maintain, because the Ruby stdlib is evolving, and in some cases it’s really an excercise in futility. So, UringMachine doesn’t really touch the standard IO classes, it just doesn’t use them. UringMachine works with plain file descriptors:

require 'uringmachine'

machine = UM.new
fd = machine.open('foo.bar', UM::O_RDONLY)
buf = +''
ret = machine.read(fd, buf, 1 << 16)
puts "read #{ret} bytes: #{buf.inspect}"
machine.close(fd)

Now, at first sight this might look like a really clumsy way to do I/O in Ruby, but there’s more than meets the eye in the above example. A second example will make things a bit clearer:

filenames = ['foo.bar', 'bar.baz', 'baz.foo']
content_map = {}

MAX_READ_LEN = 65536

# A neat abstraction to do what IO.read(fn) does
def read_file(machine, fn)
  fd = machine.open(fn, UM::O_RDONLY)
  buf = +''
  loop do
    # the -1 means append to buffer
    ret = machine.read(fd, buf, MAX_READ_LEN, -1)
    return buf if ret < MAX_READ_LEN
  end
ensure
  machine.close(fd) if fd
end

fibers = filenames.map do |fn|
  machine.spin do
    content_map[fn] = read_file(fn)
  rescue SystemCallError
    puts "Failed to read #{fn}"
  end
end
machine.join(*fibers)
p content_map: content_map

So yes it’s much lower level than IO’s #read and #write methods, but what we gain here is easy launching of concurrent (a.k.a. asynchronous) operations. In the above example, three files are read concurrently in three separate fibers by calling #spin (just like in Polyphony). Each fiber opens the given filename, reads the contents until EOF and then puts the content in a content map. Meanwhile the main fiber calls #join in order to wait for the three spun fibers to terminate.

The “Everything but the Kitchen Sink” Approach

Polyphony was a library that was trying to do a lot of things, such as structured concurrency, on-the-fly file compression (using pipes), HTTP parsing and chunked transfer encdoing, and an actor API with message passing.

The result was a big gem (~2.7KLOC of Ruby, plus ~6KLOC of C) with a very big surface area, lots of classes, lots of money-patches. In comparison, UringMachine is currently at about 200LOC of Ruby, plus ~2.7KLOC of C.

UringMachine does not try to do everything that’s related to concurrency. Instead, it focuses on providing the basics, which you can then use to build out the abstractions you want, such as message passing between fibers, structured concurrency etc.

A Simpler, More Robust Implementation

Polyphony has started as a wrapper around libev, and has evolved with time to provide an alternative backend that uses io_uring instead of libev. UringMachine was built from the beginning as a Linux-only, io_uring-only library. Its design is also in many ways simpler than that of the Polyphony backend.

I still haven’t run benchmarks comparing UringMachine to Polyphony, but from using UringMachine for running a webserver, it looks about the same, which means fast.

A New Ecosystem

Polyphony is but one of a bunch of Ruby gems I’ve been writing for fun and profit. Some of those tools were written just to explore what’s possible in Ruby, and some of those are being actively used in production on my clients’ websites.

But UringMachine does not offer the same (theoretical) level of integration with the Ruby ecosystem as Polyphony. It just does I/O its own way. It does not interfere with other gems or the way Ruby does I/O, but any I/O that does not go through UringMachine means that the parts that do use UringMachine for I/O and concurrency may be waiting for stock Ruby I/O operations to complete before being able to complete their own I/O operations.

Thus, to do anything useful, UringMachine will need to be accompanied by other gems that create an ecosystem around it.

TP2 - a New Webserver for Ruby

TP2 is a new webserver based on UringMachine. It is the continuation of my work on Tipi. TP2 also has a much simpler design than Tipi (and a much smaller codebase). TP2 uses UringMachine for managing I/O and concurrency.

Syntropy - a New Web Framework for Ruby

A third part of the new ecosystem is Syntropy, a new web framework I’ve been working on. It, too, is a continuation of the work I’ve been doing in Impression.

Like Impression, Syntropy is a filesystem-based router. The routes of the app are defined by the directory and file structure of your app. So for example, HTTP requests to /foo/bar will be routed to site/foo/bar.rb. Syntropy can also serve static files and markdown files.

Dynamic routes are served by Ruby files that are called modules. Each module exports an request handler, which can be in the form of a custom class, a Papercraft template, or a simple Proc:

# The world's simplest possible Syntropy module:
export ->(req) { req.respond('Hello, world!') }

Other features in Syntropy:

Future directions for Syntropy include:

Future Work on UringMachine

UringMachine is still missing some features. Here are some of the things I intend to add in the near future:

Conclusion

UringMachine, TP2 and Syntropy represent a new direction in my work. My aim is to build tools that fit my way of writing software, and that are tailor-made for my workflow and my preferences. As such, I’m trying to avoid feature creep, and instead focus on just adding (or changing) the stuff I need in order to execute for my clients.

Hopefully, someone else may find these projects interesting enough to use them, but it’s really more about scratching my own itch. The proof is right here on this website, which is made with the above tools.