Talks

Roda: Simplicity, Reliability, Extensibility, Performance

A talk from RubyConfTH, held in Bangkok, Thailand on December 9-10, 2022.

Find out more and register for updates for our 2023 conference at https://rubyconfth.com/

RubyConfTH 2022 videos are presented by Cloud 66. https://cloud66.com

RubyConf TH 2022

00:00:21.080 Hello everyone! It's an honor to present at RubyConf Thailand. My presentation today is on Rhoda, Ruby's fourth most popular web framework after Rails, Sinatra, and Grape. Rhoda was released back in 2014, which means it's over eight years old now. It is focused on four goals: simplicity, reliability, extensibility, and performance.
00:00:34.980 In this presentation, I'll discuss how Rhoda achieves these goals and why you may want to use it in your applications. My name is Jeremy Evans. I'm a Ruby committer who focuses on fixing bugs in Ruby. I am also the author of "Polished Ruby Programming," which was published last year and aimed at intermediate Ruby programmers. This book focuses on teaching the principles of Ruby programming and the trade-offs to consider when making implementation decisions.
00:00:53.640 What differentiates Rhoda from other Ruby web frameworks is that it is based on the concept of a routing tree built out of Ruby blocks. The routing tree integrates routing with request handling, which has multiple advantages compared to the routing approaches used by other Ruby web frameworks.
00:01:18.180 You use Rhoda’s route method to set the routing tree for the application. All requests to the web application are yielded to the routing tree block. Rhoda’s convention is to use 'r' as the name for the route block variable. Unlike most other Ruby web frameworks, where you do not have control over the details of the routing process, with Rhoda, you fully control how routing happens by calling methods on the request object.
00:01:42.240 For example, 'r.on' will yield to the block if all the arguments match the request. If the request path starts with '/foo,' this will not match, and routing will continue. However, if the request path starts with '/albums' followed by some number, this will match, and that part of the path will be consumed, invoking the block passed to the method. If the request arguments match using the Integer class, the number will be yielded to the block as an integer, making it easy to extract data from the request path without having to reference a hash of parameters.
00:02:11.340 This line shows the true power of Rhoda. At any point during routing, since you are writing the routing code, you can implement your own behavior. For instance, we use the integer taken from the request path to find a matching album. If we find the album, we set the album instance variable, which all routes inside this branch can use.
00:02:35.580 The ability to share logic and perform arbitrary actions during routing is what makes Rhoda applications significantly simpler than those written in other frameworks. If we find a matching album, routing continues. The next method called is 'r.is' with no arguments, which will only match if the request path has already been completely consumed. This will match requests such as '/albums/1' but not '/albums/1/tracks.' Assuming that the request path was fully consumed, the block passed to 'r.is' will be called.
00:03:05.819 Inside this block, we have calls to 'r.get' and 'r.post'. 'r.get' will yield if the request method is GET, and 'r.post' will yield if the request method is POST. Inside the 'r.get' and 'r.post' blocks are where you would put the code to handle the related routes that can both use the album instance variable.
00:03:34.379 Assume the request path is '/albums/1/tracks.' In that case, the 'r.is' method will not match, and it returns without yielding the block, causing routing to continue to the next expression, 'r.get' with an argument of 'tracks.' This will only match if the request method is GET and the remaining part of the path is '/tracks.' So if the request path is '/albums/1/tracks' and the request method is GET, this will match because the '/albums/1' part was matched by the 'r.on' call.
00:04:08.640 Hopefully, that gives you a sense of how routing works in Rhoda. The most important part to remember is that Rhoda allows you to run arbitrary code at any point during the routing process. While I mentioned that Rhoda focuses on simplicity, reliability, extensibility, and performance, performance is the most objective advantage. You can directly compare the performance between Rhoda and other Ruby web frameworks.
00:05:02.639 Tech and Power has a well-known set of web framework benchmarks. These are the results of the benchmarks when using the Puma web server. As shown here, the combination of Rhoda with the eSQL database library is the fastest, about 60% faster than Sinatra and over five times faster than Rails. One thing to be aware of is that Tech and Power's benchmarks only assess applications with a small number of routes. It's also useful to benchmark an application with a large number of routes.
00:05:53.699 The 'r10k' benchmark uses applications with 10, 100, 1000, and 10,000 routes to evaluate routing scalability. To avoid web server overhead, it tests using the Rack API directly. Here are the runtime results for Rhoda, Sinatra, Rails, and Hanami. Pay no attention to the absolute numbers; only the relative performance differences matter.
00:06:01.320 While the graph makes it obvious that Rhoda is much faster, it's hard to see how much faster. In this benchmark, Rhoda is about 13 to 675 times faster than Sinatra, 40 to 75 times faster than Rails, and 5 to 8 times faster than Hanami, depending on the number of routes. Regarding memory usage, Rhoda always uses the least amount of memory: about 15 to 65% less memory at 10 routes and 55 to 60% less memory at 10,000 routes.
00:06:40.560 Most of us know that performance differences in benchmarks aren’t often a good indication of performance differences in real-world applications. I've converted multiple production Rails applications to Rhoda, and my experience shows that Rhoda is about twice as fast as Rails for the same production application while using a third less memory. Rhoda wins easily on performance, but to me, the larger advantage is that Rhoda allows you to write simpler code to implement your web application. When you can write simpler code, you're likely to decrease the number of bugs and make it easier to fix those bugs and add features.
00:07:21.240 The simplicity advantage that Rhoda offers compared to most other Ruby web frameworks is due to its integration of routing and request handling. The recognition is that routing requests is not an end in itself; it's purely a means to ensure that the request is handled correctly. With the routing tree, routing is not separate from request handling; the two are integrated. As you route a request, you can also handle the request.
00:08:23.819 The advantages of this integration may not be immediately obvious. I provided an example earlier, but I will discuss these integration advantages in more detail, comparing them with web frameworks that lack this integration.
00:08:52.760 Let me start with a simple example in Sinatra. Here we have two routes, both related to a specific album: one for GET and one for POST. When I was using Sinatra, this was a common pattern in many of my applications. However, Sinatra's approach leads to duplication; the path is duplicated in both routes. The conversion of the parameter from a string to an integer and the retrieval of the album from the database is also duplicated in both routes.
00:09:41.880 Using a routing tree, we can simplify things. Instead of duplicating the path in both cases, it's specified once in the branch. Additionally, by using the Integer class argument, the conversion from a string to an integer occurs automatically. Another advantage of using the Integer class as a matcher is that this route will only match if the ID provided is indeed an integer; it will not match in any other cases.
00:10:19.380 As soon as the branch is taken, the album is retrieved from the database, making the album instance variable available in both the GET and POST routes. One of the primary advantages of a routing tree is that it allows you to easily eliminate redundant code by placing it at the highest branch where it is shared by routes beneath that branch.
00:10:50.639 While it's possible to do something similar in Sinatra using before blocks, you will need to specify the path itself three times instead of just once. Furthermore, when using before blocks this way, the shared behavior is in a separate lexical scope, making it more complicated to understand the connection between the shared behavior and the route handling methods. These routes are also in separate lexical scopes, complicating the connection.
00:11:47.020 In Rails, you specify the routes in the config/routes.rb file, and the code to handle the routes goes in a separate controller class in a separate file, usually using a separate method per route. This separation of routing code and controller code adds significant conceptual overhead, making it more work to find the code that handles the route.
00:12:43.380 Much like the Sinatra example, this approach duplicates the parameter conversion and the retrieval of the album from the database in both cases. While Rails offers a way to eliminate redundant code by using a before filter to call a method before the action for a given set of actions, if you want to add more routes where you want to retrieve the album, you have to manually update the only option to the before filter.
00:13:35.519 As with the earlier Sinatra example, the shared behavior isn’t in the same lexical scope, making it harder to connect the shared behavior to the route handling methods. This leads to issues in both Sinatra and Rails, where you might need to manually update access control for new routes—something that can become unwieldy in a larger application.
00:14:10.380 In a large application with many routes, all these improvements add up, resulting in a much simpler application. I analyzed a small application that I originally developed in Sinatra and later converted to Rhoda. This application has a total of 72 routes, with 35 branches in the routing tree, and 29 of those branches contain shared code.
00:15:09.840 This means that, 83% of the time when I branched in the routing tree, Rhoda's integration of routing and request handling eliminated duplicate code. It also shows that Rhoda's routing tree eliminates 29 separate before filters needed to avoid redundancy in Sinatra or Rails.
00:15:50.999 Using a routing tree allows one to naturally share code across all routes under a branch. Thus, web applications using a routing tree can avoid redundant code more naturally. In contrast, many other web frameworks do not make it as easy to share code this way, which can lead to redundant code and inconsistencies.
00:17:11.569 Avoiding redundancy not only reduces code but also minimizes the chances of inconsistencies that can lead to security vulnerabilities. Another aspect of simplicity is how easy it is to handle upgrades to the framework. Some web frameworks radically change their APIs between versions, complicating upgrades.
00:17:57.600 Rhoda chooses evolutionary change instead of revolutionary change, helping users upgrade with ease. Rhoda ships a minor release every month, usually adding a new plugin, feature, or optimization. When Rhoda does break compatibility in major version upgrades, it includes backward compatibility plugins, meaning that most Rhoda 1 applications written back in 2014 can run on the current version of Rhoda with minimal modifications.
00:18:36.920 Having discussed simplicity, I now want to address reliability. One way to evaluate reliability is to look at the framework itself being reliable—what I call internal reliability. Part of Rhoda's reliability comes from its 100% line and branch coverage for all code.
00:19:28.259 While internal reliability is significant, it’s more important that your framework enables you to write more reliable applications. Rhoda offers two features that enhance application reliability. Firstly, Rhoda allows applications to be frozen at runtime. By freezing an application after it is configured and before it accepts requests, you can eliminate most issues associated with runtime modifications.
00:20:02.640 Rhoda pioneered this runtime freezing approach years ago and is currently the only Ruby web framework that promotes freezing in production. An unexpected advantage of freezing applications is that Rhoda can perform additional optimizations for frozen applications by inlining methods, as it knows that the implementation will not modify after freezing.
00:20:47.880 Another reliability feature of Rhoda is its avoidance of polluting the scope of your application with instance variables and methods users may want to use. For instance, it's natural in my production applications to store a time-off request in an instance variable named 'request' and a company response in an instance variable named 'response.' This works seamlessly in Rhoda.
00:21:23.994 In contrast, this approach fails in Sinatra due to its use of the request instance variable to store HTTP request information. If you try to use the same name, it raises an exception later. Rhoda avoids these issues by prefixing internal instance variables with an underscore.
00:22:19.080 Unfortunately, Rails does not manage method pollution nearly as well. As of Rails 7, each controller action includes over 300 additional methods not prefixed, which, if overridden, could lead to problems. In Rhoda, the routing tree includes only six additional methods by default, thereby significantly reducing the chance of conflict.
00:23:00.620 Thus, Rhoda achieves high reliability through three main approaches: 100% line and branch coverage, allowing applications to run frozen in production, and maintaining a clean execution environment by avoiding polluting the scope with uncontrolled instance variables and methods.
00:23:54.499 After discussing Rhoda's goals of performance, simplicity, and reliability, it's time to tackle the final goal: extensibility. Rhoda's extensibility goal translates to a very small core, primarily focused on routing requests via the request method and path, with all non-core features implemented as plugins.
00:24:38.840 Rhoda ships with over a hundred plugins, many of which are external gems. These plugins can add methods to the scope of the route block, as well as methods to request and response classes. Every Rhoda plugin functions like a tool, offering a large toolkit tailored to various applications.
00:25:14.940 This way, you can choose the tools necessary for your application without paying the cost for unused ones. One of Rhoda’s core tenets is that you only pay for what you use. For instance, the render plugin provides support for a complete view layer and can work with any template engine you desire, offering performance optimizations through compiled templates in development mode.
00:26:12.360 For handling JavaScript or CSS in development and compiling them for production, Rhoda includes an assets plugin. The assets plugin is straightforward to configure, without requiring alternative language runtimes like Node unless specifically needed by your template engine.
00:27:02.139 Moreover, Rhoda has a public plugin for serving static files from a directory, and because static file serving is part of the routing tree, you can serve static files only after passing access control checks. If your application uses multiple directories for static files, Rhoda offers a multi-plugin to manage this elegantly.
00:27:54.620 As an example of Rhoda's minimalism, there is no default method for HTML escaping; instead, Rhoda ships with an HTML plugin that adds such functionality. On the other hand, for API applications designed to return JSON, the JSON plugin allows your routing tree blocks to return arrays or hashes, which are automatically converted to JSON.
00:28:47.680 If your application needs to accept JSON input, Rhoda includes a JSON parser plugin for parsing submitted request bodies. By default, Rhoda uses a single route block, which can limit routing for large applications, but there are multiple ways to split the routing tree into separate Ruby blocks across files.
00:29:28.620 The most common plugin for this is called hash branches, which allows you to use separate blocks and files for each top-level branch in the routing tree in a nested format. Rhoda also includes a content security policy plugin to easily configure security settings for your application on a per-branch basis.
00:30:27.060 Many security issues in Ruby web applications arise from improper parameter handling. Rhoda includes a Typecast params plugin that manages most parameter typecasting needs, ensuring parameters conform to expected types before use. Moreover, Rhoda's CSRF plugin implements strong cross-site request forgery protection with token validation.
00:30:49.720 Finally, Rhoda comes with a sessions plugin for encrypted cookie sessions, checking for a valid HMAC before decrypting the session cookie to mitigate timing attacks and other cryptographic vulnerabilities.
00:31:32.100 In summary, Rhoda maintains a small core with most features implemented through plugins, meaning you only incur the costs of the plugins you choose, as opposed to Rails, which ships with all features enabled by default.
00:31:51.980 Some Ruby programmers believe that using something other than Rails means rebuilding what Rails provides. That might apply to Sinatra, but it certainly doesn't hold for Rhoda, as it often ships with equivalent features or superior third-party libraries compatible with both Rails and Rhoda.
00:32:15.360 I'll briefly compare various Rails components to their Rhoda equivalents. Action Pack handles routing and request processing in Rails, while Rhoda's core and its routing plugins directly replace that functionality. Action View for template rendering in Rails has an equivalent in Rhoda's render plugin.
00:32:58.620 For email, Rails has Action Mailer, and Rhoda includes a mailer plugin that also utilizes the routing tree, allowing similar emails to share code, reaping the same benefits as the routing tree.
00:33:44.280 Action Mailbox in Rails for processing received emails has a Rhoda equivalent— the mailbox processor plugin, which uses a modified routing tree approach to simplify email processing.
00:34:29.959 Beyond these direct equivalents, some parts of Rails lack direct counterparts in Rhoda but can be addressed through superior third-party libraries. For database access, while Rails uses Active Record, I recommend using the SQL database library with Rhoda for enhanced speed and capabilities.
00:35:05.160 Active Job in Rails is an abstraction layer for job libraries, but you can avoid this overhead by using the job library's native API, such as Sidekiq. Action Text for rich text editing in Rails can be effectively substituted with any JavaScript editor, storing data with SQL.
00:35:45.120 Finally, Active Support in Rails modifies many core Ruby classes, often causing issues with libraries not designed to integrate seamlessly with Rails. In general, using any web framework outside of Rails allows for returns to Ruby's standard library, which I find avoids many common pitfalls.
00:36:45.620 If you're already familiar with Rails, I hope this presentation has clarified how you might handle similar needs with Rhoda. If you're inspired to explore Rhoda further, there's a free online book named "Mastering Rhoda" that I maintain.
00:37:14.420 If you enjoyed this presentation, I invite you to read my book, "Polished Ruby Programming." Thank you for listening, and I’m looking forward to any questions during the break!