Thanks!
> What we have implemented was a Python decorator to have the tasks actually fired after the function completion (thus after the transaction commit). In Go we achieve the same result in a more simple way using the defer statement. In my experience, all these solutions are local to the process that fires the task, so there is less risk of interaction with other process, easy to implement and more robust compared to other solution.
Oh yes, totally. I've seen this same pattern in Ruby before whereby a job enqueue is put on something like an ActiveRecord `after_commit` hook.
One (overly) pedantic observation is that this still leaves you with the possibility of having your transaction and data safely committed in your database, but with your job not enqueued if something happens to your process between the time of commit and time of enqueue. Admittedly though, this probably doesn't happen all that often in real life.
Probably the best answer I have is that we take this approach for the sheer convenience. We can do things like this:
def create_user(email, password)
User.transaction do
user = User.new email: email
user.set_password_hash(password)
check_for_abuse!
# make an account ID queue job to create record in billing system
user.billing_account_id = uuid()
async_create_account_in_billing_system(user)
# queue job to send an e-mail invite
async_send_invite_email(user)
create_auditing_record
...
end
end
You can build out very explicit chains of actions that may enqueue jobs or call into other chains that may enqueue their own jobs and all the while never have to worry about any kind of ordering problem while working anything. There are no hidden callbacks anywhere, but you still get to keep perfect correctness: if anything fails at any point, the whole system rolls back like it never happened.If you're using an external queue such as RabbitMQ, there's basically no way to guarantee a queue message being fired without a "two-phase" transactional approach.
Let's say your use case is creating a user and sending an email verification email. You need to:
1. Start transaction.
2. Create the user.
3. Insert a tuple into an "events to fire" table.
4. Commit.
5. Fire the event.
6. Delete the tuple.
This ensures that if the transaction fails, nothing is fired; if the transaction succeeds, we guarantee that the user now exists, and that the command to fire the event also exists. Thus, if step 5 fails, we still have the "events to fire" tuple, and we can have a cron job running every minute that picks up stragglers. (In practice, you'll need to run steps 5-6 in a transaction that exclusively locks the tuple so that a concurrent cron job doesn't do it.)
This way, you're risking duplicate events, but you'll never lose the event, unless RabbitMQ falls over. Duplicate events are of course unfortunate and undesirable, but missing events are worse.
Of course, with the above solution, it could be argued that your "events to fire" table is a queue itself. The good news is that RabbitMQ is much better at scaling a queue than Postgres, so the duplication of work isn't actually that much of a problem. The "events to fire" table can be optimized in various ways; Postgres supports "unlogged" tables, for example.
Another approach which will be available soon is Bottled Water [2], which gives you pretty much the above, except without needing to maintain the "events to fire" table. But it's definitely more complicated.
[1] https://news.ycombinator.com/item?id=9490583
[2] http://blog.confluent.io/2015/04/23/bottled-water-real-time-...