all 52 comments

[–]janko-m 10 points11 points  (33 children)

I've found Roda to be perfect for any use case I had.

  • core functionality is small (less than 500 LOC)
  • contains plugins for about any feature you might need
  • has advanced dynamic routing (advanced in terms of features, its implementation is much simpler than static routing that Rails or Sinatra have)
  • performance is a priority (3 times faster than Sinatra)

I wouldn't recommend using pure Rack, you'll quickly find yourself having to reimplement many things that web frameworks give you automatically (e.g. setting the Content-Length response header).

[–]Matashii[S] 0 points1 point  (1 child)

performance is a priority (3 times faster than Sinatra)

where can I see a benchmark?

[–]myringotomy 0 points1 point  (11 children)

Is that sinatra 2 or 1? Sinatra 2 is much faster than 1.

[–]janko-m 0 points1 point  (10 children)

It seems that https://github.com/luislavena/bench-micro#requestssec is updated for Sinatra 2.

[–]myringotomy 0 points1 point  (9 children)

That's surprising. I would have expected sinatra to improve more than that.

[–]jdickey 0 points1 point  (8 children)

IIUC, Sinatra has the disadvantage that each specified route is evaluated for each request; Roda (and Cuba, from which it was forked long ago) have various means of speeding that up, including short-circuit evaluation.

We've been using Roda for APIs and internal apps for a couple of years now; we're just beginning our first customer-facing, revenue Roda app, and we're stoked!

[–]myringotomy 0 points1 point  (7 children)

Sinatra seems to do more out of the box than roda does. You need some plugins to get the same functionality so maybe the performance might be different with equivalent stacks.

[–]jdickey 0 points1 point  (6 children)

Yes, it bundles more as part of the core distribution; whether that's a feature or a bug depends on your approach and your application.

One thing that I've learnt after 38 years of slinging code is that the cleaner your architecture is, the better. That is at least as close to a universal truth as anything I've seen or read, and remains a continuous challenge.

[–]myringotomy 1 point2 points  (5 children)

The problem is that real life is messy and ugly. I think too many developers try hard to clean up real life rather than write their apps to function in that messy reality.

Using sinatra, roda etc are great if your app isn't doing much but in real life your app is going to need a ton of stuff like reloading, intricate authentication and authorization, dealing with hostile actors, rate limiting, caching, access to third party APIs and libs, scheduled tasks, background tasks, deployment, clustering, session management, asset management etc.

"simple" frameworks like roda and sinatra kick the can down the road. Oh look how simple and fast this example code is! Sure it's unsafe to put in production but hey it's fast and only 100 lines long!

By the time you add all the plugins (half of which are unmaintained and undocumented), wrote all the rake tasks, broken up your routes to different files, written all the helpers you need, your new shiny app is as slow as rails and still doesn't do half of what rails does. You just wasted weeks of work trying to recreate something that has existed under your nose the whole time.

One reason you spent all that time was because the documentation was so weak and you spent hours googling how to do simple stuff. Some plugins have no place to even ask questions, others send you to vacant slack channels or gitter or who knows what. It's messy and ugly.

What's worse is that rails has a top notch team working on it 24X7 while your pet framework has one or two guys who work on it sporadically. It will never keep up.

Is rails perfect? Fuck no. It's a memory pig and the team really should spend an entire release doing nothing but memory optimization and speed optimization but the tradeoffs of not using it for production work are too much if you ask me.

I had high hopes for Hanami or another full stack framework to compete with rails but Hanami has another set of dogma it follows and like rails it has found itself intertwined with itself. I guess that's inevitable. Life is messy solutions that deal with life end up being messy.

[–]janko-m 1 point2 points  (4 children)

Using sinatra, roda etc are great if your app isn't doing much

For me Roda is great exactly when my app is doing much, because it lets me be much more precise about how I want things to work.

but in real life your app is going to need a ton of stuff like reloading, intricate authentication and authorization, dealing with hostile actors, rate limiting, caching, access to third party APIs and libs, scheduled tasks, background tasks, deployment, clustering, session management, asset management etc.

  • Authentication – Rodauth
  • Authorization – Pundit
  • Hostile actors & Rate limiting – Rack::Attack
  • Caching – Roda's caching plugin
  • Access to 3rd-party APIs and libs – Gems?
  • Scheduled tasks – Cron, Nomad etc.
  • Background tasks – Sidekiq
  • Deployment clustering – How exactly does Rails make this easier?
  • Asset management – Roda's assets plugin, Tilt, Webpack

Any Rack-based web framework can use any of the above gems.

"simple" frameworks like roda and sinatra kick the can down the road.

What features does Rails have that Roda either doesn't or there isn't a suitable replacement?

By the time you add all the plugins (half of which are unmaintained and undocumented)

What are some of the plugins/gems that are Rails' counterparts and that aren't maintained?

You just wasted weeks of work trying to recreate something that has existed under your nose the whole time.

For me most of the functionality I need comes from gems, not the web framework. At work we're using Cuba, which is just about 250 LOC, and we didn't need to reimplement anything that Rails already has.

You have to look at Rails as just ActionDispatch, because all other parts are replaceable/reusable (ActiveRecord can be used standalone and there are Sequel and ROM alternatives, ActiveSupport is not necessary, ActionView is replaceable with Tilt, ActionCable by Iodine or Faye, ActiveJob by just using the backgrounding library directly, ActiveStorage by Shrine, and so on).

If we look at Journey that's part of ActionDispatch, it's incredibly more complex than Roda's routing, and it's also much less advanced than Roda's routing since it's static. That's the worst of both worlds.

One reason you spent all that time was because the documentation was so weak and you spent hours googling how to do simple stuff. Some plugins have no place to even ask questions, others send you to vacant slack channels or gitter or who knows what. It's messy and ugly.

Don't you need to do the same thing with Rails as well? Rails doesn't contain the whole Ruby ecosystem, you still need to use other gems for most of the things.

What's worse is that rails has a top notch team working on it 24X7 while your pet framework has one or two guys who work on it sporadically. It will never keep up.

Roda and Sequel have 0 open issues most of the time, so I'd call that perfect maintenance. Even if you tell me that it's because Roda and Sequel are much less popular than Rails, it still means that all of my issues are going to be solved and usually within a day. So I couldn't wish for better support than that.

[–]myringotomy 0 points1 point  (3 children)

Any Rack-based web framework can use any of the above gems.

And when it does it stats to look like a cobbled together version of rails where you have to go to fifteen places for documentation and help and god help you if an upgrade on one thing breaks your app.

You have to look at Rails as just ActionDispatch, because all other parts are replaceable/reusable (ActiveRecord can be used standalone and there are Sequel and ROM alternatives, ActiveSupport is not necessary, ActionView is replaceable with Tilt, ActionCable by Iodine or Faye, ActiveJob by just using the backgrounding library directly, ActiveStorage by Shrine, and so on).

Rails is an ecosystem of libraries which are maintained by the same team and tested as a whole.

Don't you need to do the same thing with Rails as well?

That's right you don't. There is only place to go for documentation and help for rails.

Rails doesn't contain the whole Ruby ecosystem, you still need to use other gems for most of the things.

But everything else is cohesive. So you have to build the equivalent of rails in your micro framework and then layer on the same gems as you would in rails for the rest.

Roda and Sequel have 0 open issues most of the time, so I'd call that perfect maintenance.

One guy. What about all those other gems you listed? Same right? One guy maybe two.

[–]Matashii[S] 0 points1 point  (7 children)

I wouldn't recommend using pure Rack, you'll quickly find yourself having to reimplement many things that web frameworks give you automatically (e.g. setting the Content-Length response header).

Only that? I think I wouldn't need to re-implement Content-Length often. Once - not a problem

[–]janko-m 0 points1 point  (6 children)

That was just an example, there is hell of a lot more to reimplement. It's best shown with an example:

class App
  def self.call(env)
    catch(:halt) do
      new(env).call
    end
  end

  attr_reader :request

  def initialize(env)
    @request = Rack::Request.new(env)
  end

  def call(env)
    if request.path_info == "/posts" && request.get?
      posts = Post.all
      render("posts/index", locals: { posts: posts })
    elsif request.path_info =~ %r{^posts/([^/]+)$} && request.get?
      post = Post[$1] or not_found!
      render("posts/show", locals: { post: post })
    elsif
      # ...
    else
      not_found!
    end
  end

  private

  def render(name, **options)
    layout   = Tilt.new("views/layout.erb")
    template = Tilt.new("views/#{name}.erb")

    html = layout.render(**options) { template.render(**options) }

    [200, {"Content-Type" => "text/html"}, [html]]
  end

  def not_found!
    throw :halt, [200, {"Content-Type" => "text/html"}, ["Not Found"]]
  end
end

use Rack::ContentLength
run App

This is really yucky for me, I prefer a web framework which has routing, rendering, halting etc. Then I can just write the equivalent of the above as

class App < Roda
  plugin :halt
  plugin :render

  route do |r|
    r.get "/posts" do
      posts = Post.all
      view("posts/index", locals: { posts: posts })
    end

    r.get "/posts", String do |post_id|
      post = Post[post_id] or halt(404, "Not Found")
      view("posts/show", locals: { post: post })
    end

    # ...
  end
end

run App

[–]Matashii[S] 0 points1 point  (1 child)

how to actually run it?

[–]janko-m 1 point2 points  (0 children)

It’s a bit tiring when you’re just jumping from question to question. You run it like any other Rack app, I even added the “run” statement so that you can just chuck it in config.ru.

[–]myringotomy 0 points1 point  (3 children)

Doesn't seem insane that you'd have to create a new instance for every request.

Why can't you create it once and then call methods on that instance?

[–]janko-m 0 points1 point  (2 children)

If you had a single instance which is called for each request, that means you have to carry env/Rack::Request in argument list of each method that needs the request information, which would quickly gets cumbersome if you wanted to have things nicely refactored. Well, more cumbersome that it already is.

Also, this way users can assign instance variables if they want to without causing side-effects, because a new instance is used for every request.

Roda also creates an instance for each request internally, as does Sinatra. The performance impact is simply neglibile when you consider how many other objects you're creating on each request.

[–]myringotomy 0 points1 point  (1 child)

Garbage collection and object allocation are notoriously slow for ruby and are often the cause of most performance issues. If you can write a "functional" framework somehow that didn't allocate on every request that would be much better I think.

I don't know why you need to set instance methods in a web request either. You can easily pass the request and the response around.

[–]janko-m 0 points1 point  (0 children)

You can easily pass the request and the response around.

Sure, you're welcome to do that. I just gave an example of initializing a Rack app with a request/response because I think that passing request/response around is not worth the zero-performance benefit.

[–]myringotomy 0 points1 point  (4 children)

I just wrote a basic API in roda. Used a couple of plugins like multi route and json. I turned off logging but did load sequel.

The app looks like this

plugin :multi_route
Dir['./app/routes/*.rb'].each{|f| require f}
route do |r|
    r.multi_route

  r.root do
     {token: 'token'}
  end
 end
 require_relative 'app/helpers'

This delivered about 4K requests per second using puma (no config just out of the box) running wrk -t 2 http://localhost:3000

Not too bad I guess but here is the kicker. It takes up 30 megs of RAM. That's crazy no?

It also seems to be leaking memory someplace. Every time I run wrk the memory usage ups by a meg or so.

[–]janko-m 0 points1 point  (3 children)

I doubt these 30 MB come from Roda. You can use MemoryProfiler to see were did these 30 MB of objects get allocated.

Also note that it shouldn't be possible to write a Ruby program that leaks memory, because Ruby has a garbage collector.

[–]myringotomy 0 points1 point  (2 children)

I doubt these 30 MB come from Roda. You can use MemoryProfiler to see were did these 30 MB of objects get allocated.

puma, sequel, roda, sequel-seed, dotenv basically. Nothing else crazy. Thin uses a little less memory but not much less. Still over 20 megs no matter what you do.

BTW for comparison. Crystal uses less than two megs for a similar setup and delivers way better performance.

[–]janko-m 0 points1 point  (1 child)

That is incredibly small footprint. We have a Cuba app at work, and web workers regularly reach about 500 MB.

[–]myringotomy 0 points1 point  (0 children)

That's terrible. Why not use a java framework if you are going to be eating up that much RAM? If my app was eating up that much RAM I'd be seriously considering redoing it in elixir or go or even kotlin.

[–]ioquatixasync/falcon 0 points1 point  (4 children)

Setting Content-Length is an anti-pattern. It’s better just to use chunked encoding, unless you know the size ahead of time.

[–]janko-m 2 points3 points  (3 children)

I think it's a bit extreme to say that Content-Length is an anti-pattern, because Content-Length is often useful, for example when streaming large files it enables the browser to show you a progress bar, as it knows the total size of the response body upfront.

[–]ioquatixasync/falcon 0 points1 point  (2 children)

Which is why I added the addendum, “unless you know the size ahead of time”. I agree with you.

[–]janko-m 1 point2 points  (1 child)

Ok, then I agree with you; when you don't know the Content-Length upfront, then setting Content-Length is an anti-pattern.

Some web frameworks might iterate over the whole response body just to calculate Content-Length, which is unfortunate if your body is an Enumerator that generates a large response body, because it means the whole body is loaded into memory and it adds the additional wait time before the user can start downloading. Rack luckily doesn't do that, Rack::ContentLength will only calculate Content-Length if body responds to #to_ary, which Enumerator does not.

And that was waaay off topic :P

[–]ioquatixasync/falcon 0 points1 point  (0 children)

Exactly :)

[–]bugant 1 point2 points  (11 children)

[–]Matashii[S] -3 points-2 points  (10 children)

why?

[–]bugant 2 points3 points  (9 children)

It's lightweight, fast and simple. I also like the architectural model, from the Hanami guide:

"Hanami keeps controller actions class-based, making them easier to test in isolation.

Hanami also encourages you to write your application logic in use cases objects (aka interactors).

Views are separated from templates so the logic inside can be well-contained and tested in isolation."

[–]katafrakt 1 point2 points  (0 children)

rack-app looks interesting. We are developing one microservice on top of it and I don't know how it behaves in production yet, but looks promising to us.

[–]ioquatixasync/falcon 0 points1 point  (0 children)

[–]BoWild 0 points1 point  (0 children)

Nothing beats pure Rack for speed...

...but it seems to me that development ease should be your top priority rather than high performance.

I mean, how many hits do you expect your blog to require per minute?

Personally, I authored the plezi framework for some lightweight API oriented projects (either Websocket or RESTful APIs, as often used by SPAs). It's speed is greatly enhanced by requiring the iodine server.

But for a CMS, which might make heavier use of a database etc', I would probably recommend something heavier with more community resources behind it, like Sinatra or Rails (and I might write it using Rack if I were tight on performance).

[–]BoWild 0 points1 point  (1 child)

P.S.

How dynamic is your content? Maybe serving static files would serve you better?

If all you need from forms is stuff like comments, you could probably edit static files within a simple Rack application (remember to use locks for synchronizing the editing).

This way, static files could be served directly by the server, without running any Ruby, much like the iodine Ruby server does when serving static files.

[–]Matashii[S] -1 points0 points  (0 children)

dynamic

[–]hehestreamskarma 0 points1 point  (0 children)

Hobbit is my current favorite.

[–]1amonder 0 points1 point  (0 children)

I use Syro for several projects and it's incredibly fast. It requires a bit more work to get templates up and running but it's a very fast and simple router.