Where Ruby/Sinatra falls short
pc-kombo.com
pc-kombo.com
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.
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.
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.
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".
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.
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'In a number of cases, I've found that the Rails Way was smarter than my original assumptions.
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.
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.
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 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.
This is a very simple example of a common task that the separation between UI and backend fails.
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.
You are comparing apples to oranges.
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.
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...)
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.
Entire web applications, with minimal effort. I have not found this to be the case with Sinatra.
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
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.
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.
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...
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.
But yeah, Nginx can be a great solution for load balancing and TLS termination.
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?
Edit: here we go- https://hapijs.com/api#path-matching-order
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.
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.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.
Grape's also pretty good, but I prefer Roda over it too. Something about writing it feels better.
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.
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.
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!
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 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.
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.
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.
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.
In most cases you can get away with wholesale removing and redirecting all trailing slashes in middleware.
get '/foo/?' ...
That will handle both, although I appreciate I should probably stick a redirect in place to make one the canonical link.