Surprises with Nested Transactions, Rollbacks and ActiveRecord

Lately I acquired a new hobby. I went around and asked experience Rails developers, whom I respect and value a lot, how many users the following script would create:


User.transaction do
User.create(name: 'Kotori')
User.transaction do
User.create(name: 'Nemu')
raise ActiveRecord::Rollback
end
end

The result should be the same on pretty much any database and any Rails version. For the sake of argument you can assume Rails 5.1 and Postgres 9.6 (what I tested it with).

So, how many users does it create? No one from more than a hand full of people I asked got the answer right (including myself).

The answer is 2.

Wait, WHAT?

Yup you read that right. It creates 2 users, the rollback is effectively useless here. Ideally this should create one user (Kotori), but as some people know nested transactions isn’t really a thing that databases support (save for MS-SQL apparently). People, whom I asked and knew this, then guessed 0 because well if I can’t rollback a part of it, better safe than sorry and roll all of it back, right?

Well, sadly the inner transaction rescues the rollback and then the outer transaction happily commits all of it. 😦

Before you get all worried – if an exception is raised and not caught the outer transaction can’t commit and hence 0 users are created as expected.

A fix

So, what can we do? When opening a transaction, we can pass requires_new: true to the transaction which will emulate a “real” nested transaction using savepoints:


User.transaction do
User.create(name: 'Kotori')
User.transaction(requires_new: true) do
User.create(name: 'Nemu')
raise ActiveRecord::Rollback
end
end

As you’d expect this creates just one user.

Nah, doesn’t concern me I’d never write code like this!

Sure, you probably straight up won’t write code like this in a file. However, split across multiple files – I think so. You have one unit of business logic that you want to run in a transaction and then you start reusing it in another method that’s also wrapped in another transaction. Happens more often than you think.

Plus it can happen even more often than that as every save operation is wrapped in its own transaction (for good reasons). That means, as soon as you save anything inside a transaction or you save/update records as part of a callback you might run into this problem.

Here’s a small example highlighting the problem:


class User < ApplicationRecord
attr_accessor :rollback
after_save :potentially_rollback
def potentially_rollback
raise ActiveRecord::Rollback if rollback
end
end

view raw

my_user.rb

hosted with ❤ by GitHub


User.transaction do
User.create(name: 'Kotori')
User.create(name: "someone", rollback: true)
end

As you probably expect by now this creates 2 users. And yes, I checked – if you run create with rollback: true outside of the transaction no user is created. Of course, you shouldn’t raise rollbacks in callbacks but I’m sure that someone somewhere does it.

In case you want to play with this, all of these examples (+ more) are up at my rails playground.

The saddest part of this surprise…

Unless you stumbled across this before, chances are this is at least somewhat surprising to you. If you knew this before, kudos to you. The saddest part is that this shouldn’t be a surprise to anyone though. A lot of what is written here is part of the official documentation, including the exact example I used. It introduces the example with the following wonderful sentence:

For example, the following behavior may be surprising:

As far as I can tell this documentation with the example has been there for more than 9 years, and fxn added the above sentence about 7 years ago.

Why do I even blog about this when it’s in the official documentation all along? I think this deserves more attention and more people should know about it to avoid truly bad surprises. The fact that nobody I asked knew the answer encouraged me to write this. We should all take care to read the documentation of software we use more, we might find something interesting you know.

What do we learn from this?

READ THE DOCUMENTATION!!!!

Looking for a job!

Hello everyone – it’s finally that time. After finishing my Master’s and a bit of a break I’m finally looking for a job.

For the impatient, my CV/resume: online PDF

Who are you?

I’m Tobi, online I’m better known as PragTob. I’m a Berlin based full-stack engineer (with a bit of a tendency towards the backend) and deeply interested in agile methodologies, web technologies, software craftsmanship and teaching. I contribute to a variety of open source projects (e.g. Shoes, Hackety Hack and after_do), engage in a multitude of community events (e.g. Ruby User Group Berlin), enjoy attending and speaking at conferences.

My favorite programming language at the moment is Ruby and it’s the one I’m most experienced with, along with my favorite implementation JRuby. Naturally I’m also interested in other programming languages, these days mostly Clojure. As for frameworks, I’ve done quite some work with Ruby on Rails, writing applications and teaching it at workshops and courses.

You can check me out more online, including profiles etc., at my website. Of course, looking around this blog can also give you a pretty good idea of what I’m like.

What are you looking for?

A great job of course! Working on interesting problems with nice colleagues in an interesting area, where I can learn new things. I’m mostly interested in web development and related technologies. If the company supports the community and contributes back to open source (or even has an open source product!) – all the better!

I prefer working in an office with my co-workers, working together, pair programming and discussing options in front of the white board. That said, other options are possible. I love open source and am used to distributed working on open source projects, for instance.

At this point in my life I want to refrain from working for advertising companies, banks and the likes. Moreover, I really want to stay in Berlin for now, it’s a great city with many great companies. Something super awesome would have to come up, to make me move. Also, a job where I’m away from Berlin most of the time is probably not ideal for me.

What now?

Well if I sound like someone you want to work with or if you want to figure out if you want to work with me, please go ahead and get in touch 🙂 You can shoot me an email at pragtob@gmail.com

As a reminder, since you made it this far, you can find my CV here

2n edition of HU course – with blog and material

Hi everyone,

today was a great day! Today was the first day of the second edition of the course about the basics of web development I’m teaching at Humboldt University Berlin. I had a great deal of fun and am already looking forward to next week.

Anyhow, the course now has its own blog where I share presentations, homework etc. The course is held in German only, so the material is only German as well. The course is aimed at total beginners (e.g. minimum requirement: “You should know how to use a computer.”).  When looking through the material please keep in mind that it is not made for self-study – it is made to be presented. Also it is for real beginners and therefore sometimes makes abstractions/simplifications for the sake of productivity.

So without further ado, enjoy the blog: webanwendungsentwicklung.wordpress.com

Cheers,

Tobi

Slides: Introduction to Web Applications (RailsGirls Berlin)

Hi everyone,
here go the slides from my talk this morning at the RailsGirls Berlin December workshop. It’s a basic introduction to web applications and Ruby. Enjoy it and feel free to use it everywhere – if you got questions comment 🙂

So enjoy RailsGirls and enjoy coding! Hopefully see you around for the afterparty and feel free to grab me any time if you want to ask me something or just chat!

Cheers,

Tobi

Setting up PostgreSQL with TravisCI

TravisCI is an awesome free continuous integration system that just takes your github repositories and then runs all your tests – it is pure awesomeness and easy as cake.

However when testing a web application you also have to setup a database – they have got good docs for that, but still I ran into problems.

What was my problem? In their description the database name is “myapp_test” and I believed that it would not matter and it could be anything like “Tracketytrack Test” I wanted. I got proven wrong. Apparently it has to be “myapp_test”. Also for some reason I had to add a manual call to “rake db:migrate” – I may check into this later. For your reference, here the relevant parts of my .travis.yml as a gist.

And here they are as well for your reference:

postgres:
  adapter: postgresql
  database: myapp_test
  username: postgres
before_script:
  - "psql -c 'create database myapp_test;' -U postgres"
  - "rake db:migrate"