Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Where Ruby/Sinatra falls short (pc-kombo.com)
59 points by onli on Oct 15, 2018 | hide | past | favorite | 68 comments


I find it really interesting that the Ruby language led to two such wildly different, yet equally valid approaches to web apps - Sinatra and Rails.

Sinatra is the most minimal web framework I've ever used. It's fantastic for doing quick mockups and small apps. Yes, you will hit limitations. Bicycles are not good dump trucks. And even before you hit hard limits, you're going to find yourself inventing a lot of structure/convention to keep it from devolving into pretty PHP.

Rails, on the other hand, is fantastically complete. It does everything for you. Convention over configuration is a brilliant concept, and the self-discovery nature of the ActiveRecord ORM is something that is totally natural in Ruby and quite awkward in other languages. For writing basic CRUD apps (which is much of the software world), it's really hard to beat the efficiency of Rails.

And when people whinge about performance... well, so what? Horizontal scaling, friends. Developer performance is much more important than software performance in most cases (especially since networks and databases form the bulk of your performance cost out in the real world). But efficient developers, that's magical.


In my experience with web applications at non-Googley scale, most performance problems are occurring in places like SQL queries, rather than the raw speed of the web application stack itself.

Another really common mistake is tasks being performed inside the web request that should instead be offloaded to background job processing. If something is going to take more than milliseconds, it should be a job. If you see a PR where someone is bumping up the HTTP timeout in order to keep their slow processing request from being timed out by the webserver, they are doing something inline that should actually be a job.


Are there any good books or other resources on building out a system to do background processing? It seems that it's a common task, but there are different approaches.


The Ruby world at least seems to have settled on relatively simple Redis backed queues, where you directly request the job to be executed rather than use a more generic message broker.

For sure there are more complex scenarios, but if you're just getting started you'll be well served by just reading the docs for sidekiq.


Unless you're building something with very specific requirements just use Sidekiq [0]. It's easy to install and performs very well.

0 - https://github.com/mperham/sidekiq


Not a direct answer, but Rails now includes ActiveJob which abstracts over the various jobs are available and is nice and simple to set up.


> And when people whinge about performance... well, so what? Horizontal scaling, friends.

I hear this a lot, but in my experience, it's not a workable solution to this particular problem. 100% of the systems I've worked on in the conventional 'slow' languages (Ruby, Python, Elisp, declaration-less Common Lisp, etc) hit significant performance bottlenecks in typical single-thread use cases. These languages enable me to work quickly when making a prototype, but then I spend a long time optimizing it to run fast enough for a single user.

Don't get me wrong. The speed of development for these HLLs is great. It's much better to be able to move continuously from the first prototype to a production system. (I don't want to write a prototype in C, nor do I want to throw out a working HLL prototype and rewrite it all in C.) The benefit, though, is that I can get a prototype running quickly, and iterate quickly. It doesn't obviate all performance requirements just because there's an HTTP server involved.

Horizontal scaling is a great solution to "my web app suddenly got very popular, and it works great for 1000 occasional users but today I need the capacity to deal with 10,000 simultaneous users".


It really depends on the problem space, though. And for most reasonable applications, Rails/Sinatra scale just fine through well-understood horizontal methods. By the time you're getting to where they don't scale well, you're probably longing desperately for a rewrite anyway, because your core problem has shifted from "Does this solve an actual business case?" to "How do I make this support a heavy load?"


This split between micro and macro frameworks wasn't done in Ruby because of some intrinsic properties of the language. The same split was copied in many other languages, from JavaScript to Java. I think the reason it started in Ruby is because that language drew a lot of pioneers who said "the way things are right now isn't good enough, let's try something completely different" which mindset led to Rails in the first place, largely as a response to the Java web ecosystem, and then to Sinatra as a response to Rails. It's because this mindset is a sort of (admirable) perfectionism, seeking the holy grail of computer programming, and it's good that there are many people like that. A large number of these Ruby devs inevitably moved on from Ruby in this continual search, some seeking statically typed languages like Haskell or OCaml but most popularly Go, and some seeking dynamically typed languages like Clojure and Elixir but often settling on Node.js.


> This split between micro and macro frameworks wasn't done in Ruby because of some intrinsic properties of the language. The same split was copied in many other languages, from JavaScript to Java. I think the reason it started in Ruby

web.py predates Sinatra (the 2005 rewrite of Reddit was on web.py), Django is about as old as RoR, Zope dates back to the late 90s, TwistedWeb is quoted in the 2003 WSGI PEP (333).

Stating that Ruby started µfw and the split between micro- and macro-frameworks seems like an incomplete and severely blinkered reading of history.


I do lots of experiments in Sinatra and some of those use Sequel (when I need a database). I think Sinatra has some rough edges, and it surprised me how I needed to initialise a Sinatra app:

    module MyApp
      class Web < Sinatra::Base
        def initialize(app = nil, params = {})
          super(app)
          # setup stuff
        end
      end
    end
Also, I also find it sad that people never seem to know that they can use mount command in Rails where you can mount random rack-apps in your routes, even though most people probably do use it with some of their gems, here it is with Sidekiq:

    mount Sidekiq::Web => '/sidekiq'


Why then don't you use the classic style without any initialization?


In my opinion, Rails is only really great if you have a very tight correspondence between your routes, your models, their JSON, and the database tables. When you start separating those apart, that's when you start to suffer a lot with rails.


Why don't you have a tight correspondence, though? Is it something intrinsic in the problem space, or you just want to do it the way you think of it?

In a number of cases, I've found that the Rails Way was smarter than my original assumptions.


... because not everything needs it?

If you’re mostly writing CRUD apps, you’re correct. But the world is not all CRUD, even if you’re only looking at the web space.


So at some point, you have to ask yourself if the headache of working around something Rails doesn't do well is worth throwing Rails out completely over. Can it be tossed off to a microservice and routed away in nginx or whatever? Does it really not suit the Active Record pattern?

If you need a screwdriver, then stop complaining about how much your hammer sucks and get a screwdriver instead. Or ask yourself if a nail could work in the same situation.


Yeah, that was my point.


So what exactly are those correspondences that don't match? Because in a decade of web dev I have not really seen a satisfactory counterexample. Fundamentally everything lives in a request-response cycle and almost everything you load comes from a DB or as a file asset.


Curious why you think that, or why that problem is specific to Rails. I've worked on some massive rails apps, and never experienced that pain. Or, at least not any more pain than any other large app.


I’m curious why you wouldn’t agree. Rails makes a ton of assumptions about how tables, models, controllers, and routes all line up together. Overriding that or removing part of it can get painful.

I don’t think it’s a problem, nor do I think it’s specific to rails. All technologies work best for the cases they were designed for, that’s how design works. Rails was designed for CRUD apps, and it’s brilliant at that. The trick is knowning when you’re not making a CRUD app.


> Rails makes a ton of assumptions about how tables, models, controllers, and routes all line up together.

Rails assumes some very basic, predictable, and consistent naming conventions by default. Beyond that, it has virtually no assumptions about what data feeds into a particular route or controller.

For instance, if I have a controller named 'dashboard#index', at /dashboard, that controller doesn't care if I have a Dashboard model, or UserDashboard, or if I have a 'dashboards' table in my db, or a collection of 50 other random models that all combine to create a dashboard on the fly. If I have a 'people#update' route, it makes no assumptions that I have a Person or People model or db table. I can put whatever I want into that controller action with zero penalty.

Generally speaking I am going to align my controller, model, and route names, but that's more for readability and maintainability concerns, not for the benefit of the framework.


In an app, I can import a picture either through attaching an image or pasting a URL in a field on the UI. That URL field does not exist in the DB, but in order to benefit from ActionView's helpers, I had to declare the field on my model, even though the value in that field never goes to the model. The controller takes the field's value and does the right thing with it.

This is a very simple example of a common task that the separation between UI and backend fails.


Obviously I have no insight into your implementation, but I"m not sure why you would "have to" declare the field on your model. There are probably a lot of different ways handle this situation. For instance, you probably don't have to use the ActionView helpers. There's nothing preventing your controller and view from just passing non-model parameters back and forth.

Rails has never made the promise of giving you every a solution for every situation in the wild right out of the box, and I don't understand why people seem to think it does. No framework could ever do that. What it does is abstract away the common stuff that makes up probably 95% of most web apps, and gives you the ability to roll the rest on your own.


Sinatra is not an "approach to web apps" -- it's a single purpose tool that maps HTTP requests to Ruby. Like Flask or Web.py (Python) and Express (JS).

You are comparing apples to oranges.


And you're saying oranges aren't fruit because they aren't like apples.

It's actually a not-insignificant amount of code that creates an approach so cleverly transparent that you are able to interpret it as no approach at all.


"oranges" is a very narrow range of colours between 585 and 620 nm wavelength. I'm not surprised wikipedia has a page on that https://en.wikipedia.org/wiki/Shades_of_orange


Sinatra is the DSL/object model. Rack is the http request mapper.


No, that's Rack.


Let's not forget merb which Rails got merged into (or the other way round).


This post would be more appropriately named "Where my knowledge of building web applications falls short" - the title is kinda inflammatory and a lot of the commenters here likely haven't bothered to read it and are just throwing in their two cents on the technologies mentioned in the title.

None of the complaints here are the fault of Ruby or Sinatra. One exception could be the trailing slashes complaint. The issue exists in really any web framework, though. Flask, for example, will give you the same hiccup.

I tend to defeat it in Flask with `app.url_map.strict_slashes = False`

> There are too many ways to access parameters

This is a bad thing? The framework gives you flexibility to build your program the way you want, and the way your specific problems might require.

> You will need much more than included

Well... yeah. This is not a batteries included tool for building websites end-to-end ... it's a simple way to map an HTTP request to Ruby.

> Rewriting www.yourpage.com to yourpage.com, or the other way around

> Serving your site over https

> Redirecting all http:// pages to https://

> Running tasks in the background

> Saving data in a database, and reading from it

These are definitely not responsibilities for an HTTP -> Ruby library and the first 3 are certainly responsibilities for your public-facing HTTP server (Nginx, etc...)


These complaints aren't even consistent. The author complains about being given too many options for accessing parameters but also complains about not having enough options for templating, internationalization, various devops things, ORM...

Sinatra is a minimalist framework. It even describes itself as being a domain specific language, not a framework. IMO author should have just gone with Rails if they wanted an easily extendable, batteries-included framework with a massive community.


It's supposed to be more than that. Sinatra is described as "a DSL for quickly creating web applications in Ruby with minimal effort".

Entire web applications, with minimal effort. I have not found this to be the case with Sinatra.


The application is not the server.


Of course, I agree. But though in practice Sinatra may be just a library for routing HTTP requests to a Ruby application, it is marketed as a relatively complete environment for building web applications, making a bunch of these criticisms valid.


>And besides: If Sinatra starts a server listening for incoming traffic, why does it still seem common to run a regular webserver like nginx in front of that server?

This is a common technique used for tls-termination and management of 'virtual hosts', among other things.

The mentioned "issues" don't seem to be Sinatra-specific, but rather about the authors shallow understanding of the topic as a whole


> This is a common technique used for tls-termination and management of 'virtual hosts', among other things.

Yes, usually you're not hosting only one application in a server, it's very useful to have the abstraction of a layer on top of many apps.

Also, to serve static content.


One of these "other things" is buffering of slow HTTP requests. Application servers are usually not designed to deal with that and are meant to be run behind a proper load balancer/reverse proxy. This is usually very explicitly mentioned in their documentation, for example in https://bogomips.org/unicorn/PHILOSOPHY.html


puma does pretty good at buffering slow HTTP requests though.

I honestly _don't know_ why we usually put apache or nginx in front of our ruby 'web servers'. But I keep doing it anyway, cause 'everyone else' does, and I don't want to take the time to be sure I don't need to, it works.

However, I believe rails deployments to Heroku generally _don't_ put another web server in front. Which, per your point, slow clients is quite exactly why they explained the switch from unicorn to puma as a default web server. https://devcenter.heroku.com/changelog-items/594

It is true that in 2018 web dev has gotten complicated (in a variety of different axes), and if there's a framework/platform that will allow you to not know it is, I don't know what it is! It would probably be one that made a lot more choices for you though (like, say, ruby web server, so you don't need to think about 'oh, unicorn can't handle slow clients but puma can') -- which is the opposite of Sinatra's philosophy -- but then again Rails approach to try to do that has not resulted in something people find easy either. shrug.


I've stopped using Nginx in my Docker containers, and I just run puma directly. It's still behind a load balancer that also handles TLS termination. I also serve all the assets from the Rails server, but they're cached with a CDN, so it only needs to serve each file once.

If you're using Docker with Kubernetes, Convox, Docker Swarm, Rancher, etc., then I don't think you need to run Nginx or Apache. I ran some load tests on my staging environment with and without Nginx, and it didn't make any difference.

This was a really good article that helped me understand request routing a bit better: https://www.speedshop.co/2015/07/29/scaling-ruby-apps-to-100...


If you weren't caching with CDN, would serving those static assets as efficiently as possible be a good reason to keep using (eg) nginx, do you think?

Oh, I guess load balancing (with a multiple-host scale) is another good reason, if you don't have heroku doing it for you, nginx is a convenient way to do it just fine.


I don't know if there's any middle ground where you'd want to use Nginx instead of a CDN. If it's an internal app then it doesn't really matter. But if you have any reason to worry about the performance of serving static assets, then you should always be using a CDN like CloudFront or CloudFlare, etc.

But yeah, Nginx can be a great solution for load balancing and TLS termination.


I always used to do it for static asset serving, where anything standing between the socket and a sendfile() call is a waste of space. I honestly don't know how good Puma is at sending files, but I wouldn't be shocked to learn it was still worthwhile there.


Apache httpd mod_proxy -> a smattering of docker containers


If people are looking for a lightweight Ruby webserver, I'd highly recommend checking out Roda https://github.com/jeremyevans/roda

More related to the article, the author mentions that routing order is a problem with Sinatra. Is this not common to all route matchers?

What different behaviour would make sense? Obviously once a route is matched it should be executed, so is the issue here the way route ordering is done? What order would make more sense than ordering in the way the routes are defined?


Some routers- I think hapi in node might be one- match in order of specificity, not definition.

Edit: here we go- https://hapijs.com/api#path-matching-order


The author is claiming that the order ends up NOT being the order they're defined, but without examples it's hard to tell if the problem is that or that the regular expressions being defined are not working as intended


"Now imagine not having a classical style Sinatra app, but a modular one."

I think the author ran into an issue where files were loaded differently in development mode than in production mode. Likely in development mode only the required files were loaded and the file with the more important routes wasn't. I wonder why that didn't show up in their tests.

But I also wonder if they aren't using Sinatra for too large projects. If you have routes defined over several files, it's likely to get problematic. Maybe they should be using a more complete web framework like Padrino or go all the way with Rails.


It wasn't that setup related. pc-kombo does not need that many routes that I did spread it over multiple files. I generally do avoid that. I wanted to give the specific example at first, but did not find the related commits anymore when writing the article, and in the end if it was really related to some behavior change linked to a version change (or x86 vs ARM) it would not be too useful now. It's meant as an example for the rough edges one can encounter.

But the essence is that I had some routes defined using regexpressions, probably like the URL definition for a page in the blog (not saying it was exactly this one):

    get  %r{/([0-9]+)/([\w]+)} do |id, title|
And I ran into a situation where on my local dev platform this did not trigger when calling a similar looking route, but on the production server it did. I just remember being surprised that the specificity of the regex-evaluation seemed to change.


Even in a modular project, this seems like a smell. I can certainly imagine load order making things confusing when routes are required in, but I can't think of a good reason why modular projects should have any contention at all between what routes they handle.


With micro web frameworks you're doomed to implement a subset of Rails, or have to dig around to find and configure one of the gems usually associated to it.

For those that are ok with this, or already have an understanding of the limited scope of your app, I strongly recommend Roda [0], it's a much more straightforward routing system with better memory/cpu usage.

I can't find a single instance for which Sinatra can be used where Roda is not a better choice.

[0] - http://github.com/jeremyevans/roda


Agreed. Until I wrote Modern[0] - and that was in many ways more of an exercise in OpenAPI/Swagger than anything else - there were few reasons I could find to not use Roda over Sinatra.

Grape's also pretty good, but I prefer Roda over it too. Something about writing it feels better.

[0] - https://github.com/modern-project/modern-ruby


For me Sinatra is great. It’s the minimum you can get away with in a lot of situations. The whole thing is like 1-2000 lines of code (so totally feasible to actually read all of the code). The limits the author has run into are not necessarily Sinatra limits. The author also wants to do 120mph on a bike which most people will recognize it’s a bad idea. Use the right tool for the job.


My company runs its entire backend on Sinatra, and has since 2011. We do ~200K requests/min on AWS c5.4xlarges and r5.2xlarges, so we are a fairly significant deployment of Sinatra/ruby.

As the sole and still primary maintainer of our production infrastructure, I've been very happy with this choice of tech. I think our problem was made much simpler because, as a mobile app, we basically were building an API. We did integrate ActiveRecord/Support, and today have a massive suite of tests we run in rspec (it's pretty cool, Jenkins will spin up ~50 concurrent EC2 instances running docker, run the tests, and report back in about 15 min).

To this authors complaints, I suppose I feel that every framework has idiosyncrasies that you just accept and figure out, and many of the issues cited are felt only once and then not again. Today, running at scale, the main challenges have to do with building and supporting new tests, which rspec is amazing for, and handling application complexity, which we have been addressing using Service Objects.


Part of the issues are with the separation of responsibilities between rack, sinatra, and whatever you use to run rack (e.g. puma is just one of many options here). We had similar issues a few years ago when we still used sinatra (on top of jruby).

One of the assumptions baked into rack is that parameters can only occur once. It returns a dictionary of values instead of a dictionary of lists of values (like e.g. the servlet API does in java). So we had some issues with request parameters that could occur more than once because of that. Ultimately we had to grab the raw request and do our own parsing to get that working. Likewise headers are treated the same way.

Another issue that was annoying was that rack, sinatra, AND the rack server we used (something on top of jetty that I forgot the name of) each had their own wonky logging primitives and configuration that interfered with each other. So we were getting duplicate log messages with very inconsistent formatting from different frameworks that each assumed to be in control of stdout. In the end some monkey patching (mainly to make rack log calls do nothing) allowed us to log via jruby/java via a proper logging framework on Java so that we could send properly formatted json messages to our logging cluster instead of dealing with unparsable garbage emitted via puts. We even managed to use the MDC, which is a good reason to use Java logging frameworks if you are running on top of jruby anyway. I don't think MDC is a thing in the ruby world.

Then there were loads of fun issues with cross site scripting protection in sinatra breaking stuff in subtle ways, double session initialization (i.e. two frameworks trying to set the session cookie), and a few more such issues.

I'm assuming some of these things may have improved over the years.


> So we had some issues with request parameters that could occur more than once because of that. Ultimately we had to grab the raw request and do our own parsing to get that working.

I had to do the same multiple times. Not for this project, but for different ones. Thanks for mentioning it. I should've mentioned it in the article!


Someone on HN previously mentioned that every project they've started with Sinatra, they've regretted down the road that they didn't use Rails.

I think the complaints about Sinatra are vaild, in that it needs to limit how params are accessed, better startup documentation, etc... But ultimately Sinatra is for building super simple crud apps. I don't even use it for anything that accesses something more than a SQLlite DB. I often use it for serving up basic front end assets and running single page applications that have some small server side component to them.

It's footprint is lighter than rails, but so is it's intended functionality.


I suppose it's a selection bias. You likely heard about projects that achieved some significance, which requires some sophistication which Rails provides.

I suppose there's a number of very small, non-public projects that have, say, 10-20 users doing a relatively trivial thing, and are just fine on Sinatra (or web.py, or maybe even a plain CGI script). But they never catch much attention.


For years I have never looked back. If I had a project that outgrew its purpose I wrote it again, and dropped half of its functionality without looking back.


How does Elixir's Phoenix framework compare for the shortcomings of Sinatra noted here?

Give Elixir's commitment to immutability and functional programming, I'm thinking it might be more explicit in some of the places Sinatra has implicit or inconsistent behavior.


I've been a long-time user of Sinatra as well as Rails. About 8 years ago or so I decided to move all of my sites onto Sinatra and made a simple file-backed CMS that has scaled quite well over the years.

The drawbacks of the approach have been having to write a lot of methods and classes from scratch to handle certain processing data (there is no actual database) but the end result has been something quite bulletproof and scalable to a point, and the experience implementing those features (like TLS, sessions, helpers, etc) has been quite valuable.

However, my needs are starting to grow and I think soon we will have to move off of the old framework onto something new as our business requirements grow more complex.


Sinatra is a great way to build test programs and demos. Once scaling up to a real application I have found that Rails is a better choice because it has a lot of the things built in that feel missing from Sinatra.

For example if you are trying to figure out how to add things like flexible initialization logic, security protections, clever MIME type handling, Cucumber testing, and better routing, you’ll find that Rails has these things built in.

What you can’t get from Rails is the “one file application” that’s the killer feature of Sinatra. But all those files and directories in Rails are doing stuff that you’ll eventually want.


The answer to points 5 - 8 is basically to use Rails/ActiveSupport. If you're writing an API for a SPA, then Sinatra hardly falls short.


Biggest thing is something else. Performance of Ruby/Sinatra plus gems needed to make it workable is worse than actual Rails. Might as well directy work with Rails. Better support and more features on top of better performance.


Ruby/Sinatra is really nice. I wrote a little webapp 10 years ago using it. Faced with the alternative of doing everything in C++, it really made things easier.


> And a small issue on top of that: If you define /route, /route/ will still throw a 404. The right solution would be to serve the page on the two routes but set one as canonical source. But you have to do that manually.

In most cases you can get away with wholesale removing and redirecting all trailing slashes in middleware.


To be honest I always just define:

   get '/foo/?' ...
That will handle both, although I appreciate I should probably stick a redirect in place to make one the canonical link.


Rack gets you out of a lot of jams if you know how to use it effectively.


That's the truth. I had a thing come up the other day where a client wasn't including a content type header and thus the body was being interpreted as application/x-www-form-urlencoded. Rack middleware to the rescue on that one.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: