Transactional user registration

Code like your example is likely the reason, why all those people in this thread warn you about the issues of not having transactional guarantees with external services. In elixir you can just as well do this, which is about the same as your example:

{:ok, user} = MyApp.Repo.insert(user)
case user |> Email.registration |> Mailer.deliver do
  :ok -> :ok
  error -> …
end

But as mentioned before this code no matter the language does have problems. For example saving the user could succeed, but the machine/service crashes before Mailer.deliver is called. I’m not sure what MTA means, but if it’s your mailing provider, there’s nothing for them to do about this. They can’t see the crash. In this specific case even your mailing library won’t help you, as even that wasn’t yet called.

Generally I feel in the elixir community there are many people of experience, which might have been burned by exactly such problems unlikely as one might imagine them. Given the beam and the ecosystem also provides a bunch of very nice tooling – like e.g. oban for persistent queues in most people’s default postgres db – which allows you to model the problem in a way, which acknowledges the issue of the task we tend do suggest using those instead of the straight forward, but problematic solution.

In this case a persistent queue – as mentioned before – makes the transaction of saving the user simpler and actually transactional (so in case of a crash you either have no user or a user and a persisted task for sending an email), while sending the email by executing the queue is the only place where you have to deal with the external service. Only here you’ll deal with timeouts, retrys and eventually stop trying if things keep failing. It’s still the same issues as without the persistent queue, but it’s separated from saving the user (doesn’t affect more code than it needs) and you’ll get a proper history of how many retrys you had, what the error(s) were and you can e.g. be notified if you need to manually resolve something. For the proposed problem in the start of this post nobody would even know that a mail wasn’t sent.

2 Likes

@LostKobrakai - thank you for your clear explanations.

It’s Mail Transport Agent. In the case I mentioned it’s local sendmail (alternative). It has its own persistent queue, work schedule, failure notification, etc.). If that’s not available or an external mail dispatch service is to be employed, I’ve also been using other persistent queue/deferred job implementations, Sidekiq as an example.

Just to make sure - the way I mentioned does have persistent queue and does notify once it gives up trying (email bounces to appropriate address and is distributed among those who are supposed to care), it also keeps logs, and all. The main difference as I see it now that in my example a) the responsibility is delegated to the MTA, which is a good thing - less code in the application, and b) it is not really transactional, which is a bad thing as the queue insertion may fail once the record is saved. Yes, I handle the errors but it’s not guaranteed to cover all situations. Which brings me back to where (and why) I started the thread. While putting non-DB operations in a DB transaction block is possible, it has its own problems. From the discussion here I understand that maintaining own queue and using a transactional queue insertion with Oban is the way to go with this kind of problem.

1 Like