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.