Phoenix.Swoosh approaching 1.0

Hi all,

I’m close to making a 1.0 release for Phoenix.Swoosh.

With this release, Phoenix.Swoosh will work in any project in addition to Phoenix projects, thanks to the Phoenix.View extraction.

It’s a little awkward to ask for feedback right now, because at this moment the lib most likely has conflicts with current Phoenix apps (< 1.6), as the original Phoenix.View is still there.

However, I’d ask anyway. If you are interested, please take a look at the code changes and let me know what you think. Thanks!

Also, if you are thinking about starting a new project that isn’t a phoenix app, and need to send emails with some templates, it’s now the perfect time to test it.

11 Likes

Great work!

How does this compare to Bamboo? Is Swoosh a drop-in replacement for Bamboo? What can one library do that the other can’t?

They are very similar. One is almost a drop in replacement for the other.

I don’t like some of the ideas in bamboo, but I won’t discuss them here as it’s not the topic of the thread. Also I’m heavily biased for obvious reasons :slight_smile:

If anyone else would like to chime in about the questions, they are more than welcome to. They would be a lot more objective than I am.

3 Likes

Back when I compared both libraries (about 2 years ago), one of the main reasons I picked Bamboo was it had a deliver_later function built into the library. At the time this felt nice because it was convenient to call that instead of wrapping all of your emails in Task.start or some async solution which is what Swoosh suggests to do.

If you’re coming to Elixir from Ruby or Python you might already have things engrained into your brain like “yes, I want to send all emails in a background task” and Bamboo made that easier out of the box.

But if your project happens to be using Oban already then it’s not a huge amount of extra effort to send mails through that instead of an adhoc Elixir task, which makes both Bamboo and Swoosh pretty even in this regard.

With Phoenix officially including Swoosh now, the decision to use Swoosh is much easier. It’s what I would use in an app being developed today.

2 Likes

Swoosh doesn’t really suggest “Task.start”. It’s just an example. GitHub - swoosh/swoosh: Compose, deliver and test your emails easily in Elixir

My belief is that, much like authentication, async job and job queue (more specifically, error handling and retry strategy) are app specific and should be considered thoroughly and made to fit the business logic. And I believe that the job queueing code should be the one that drives the email sending code, not the other way around. Bamboo being too much like rails is one of the reasons I don’t love it. Just like Phoenix is not your application, Swoosh is not your application, or your async driver.

Oban is great. Task.start is also great. Even sync email delivery in request handling process is good enough sometimes. Which one to use is really a matter about what you want to happen when things fail.

5 Likes

Imagine someone who is fairly new to Elixir and hasn’t fully understood the inner workings of the Task module or supervisors and many other things that folks even with a year of Elixir experience might not have touched.

The Swoosh docs puts:

# To send asynchronous emails in Swoosh, one can simply leverage Elixir's standard library:

Task.start(fn ->
  %{name: "Tony Stark", email: "tony.stark@example.com"}
  |> Sample.UserEmail.welcome()
  |> Sample.Mailer.deliver()
end)

It’s directly saying to use this example. Sure it links to the docs for Task and mentions how it’s not really a good idea for mission critical applications but at the same time it doesn’t say what to do when you want these things, but neither of things are approachable to someone new to Elixir.

I very much remember reading this as a newcomer to Elixir and being put off by this because I wasn’t sure why a library would recommend doing something that’s considered questionable, especially since I was using robust queueing libraries for 5+ years in Ruby and Python beforehand.

Whether or not Bamboo’s deliver_later is better than Task.start isn’t even taken into account here. It’s the message sent from the docs to the user’s brain. Bamboo’s docs didn’t leave me thinking its deliver_later isn’t adequate in a real application.

1 Like

Exactly. Swoosh’s default of sending emails in process is saner for the smaller sites. I have a SMTP server on the same host and it can queue/resend emails.

1 Like

The whole point was to lead the reader to explore more about how to handle async tasks properly in Elixir, and build what’s most suitable for themselves.

Please take a look at the official docs for Task and Task.Supervisor for further options.

Note: it is not to say that Task.start is enough to cover the whole async aspect of sending emails. It is more to say that the implementation of sending emails is very application specific. For example, the simple example above might be sufficient for some small applications but not so much for more mission critical applications. Runtime errors, network errors and errors from the service provider all need to be considered and handled, maybe differently as well. Whether to retry, how many times you want to retry, what to do when everything fails, these questions all have different answers in different context.

The “it depends” tone is intentional.

I guess the takeaway is, we didn’t suggest a good generic safe catch-all solution. I’ll mention job queue and Oban in the docs. So if one doesn’t know what they want, they can fallback to the safest way.

EDIT: I’ve updated the docs.

4 Likes

Thanks for creating this library. I’ve integrated it recently and it was very nice - everything is well thought out and the documentation is great.

One thing I found myself wanting is a way to combine the Logger and Local adapters. I ended up doing it by having a function that debug-logs the email body always before sending the email, and configuring the app to use the Local adapter in dev, which worked well enough, for others who may want the same.

1 Like

Thank you for changing the docs! :heart:

2 Likes

@princemaple - One surprise when I was checking out the 1.6-dev branch of phoenix is, it uses only swoosh - not phoenix.swoosh. Any specific reason, for this half-way integration?

Hi @cvkmohan I am not involved in integrating Swoosh into Phoenix. It was a surprise to me as well (that swoosh is entering Phoenix eco system, not that not using Phoenix.Swoosh)

I suppose it’s easy enough to swap swoosh for phoenix_swoosh. And, not everyone uses phoenix_html, which phoenix_swoosh depends on.

2 Likes

Looks good. The extra context about using a dedicated job queue helps a lot.

I’ve added a PR to fix a few minor typos btw Fix a few typos in the docs by nickjj · Pull Request #617 · swoosh/swoosh · GitHub.

2 Likes

FYI José added more reasoning in Build upon async emails section by josevalim · Pull Request #620 · swoosh/swoosh · GitHub

5 Likes

Personally I don’t agree with his latest stance and I think it sets you up for a poor user experience if you follow it. It’s coming at it from the POV of server performance when it’s really both a human and server problem – both are important and I would go as far as saying the human problem is even more important to solve.

For #1, you can’t control how fast or slow another service will respond. Waiting 3 seconds for a response with a busy mouse cursor is a really bad user experience and that’s the user experience you’re going to get if you follow the new docs and run into a situation where your email provider doesn’t respond immediately for reasons you can’t control. Even waiting a few hundred milliseconds is going to be noticeable.

For #2, even with Rails or any Python web framework you can fare pretty well if you run a few processes of your app (web servers in all major frameworks will handle this for you). Even if you were reckless enough to send emails in the request / response cycle, as long as you did it sparingly and you weren’t running at massive scale your server would likely be fine.

I would still never do it tho because I want to be in control for when my web server responds and sending the email in the background is how you get that control.

By the way, with the new docs it reads kind of weirdly now because the new paragraph under the Task example tries very hard to talk the reader out of sending emails asynchronously but then the very next paragraph says if you have a mission critical app and you want to ensure your email gets delivered you should use Oban or Exq.

It basically says “by the way, sending emails in the background is a bad idea because you can’t show anything to the user if it fails but also by the way if you care about sending emails you should send it in the background using something other than the example we just covered”.

It conflicts itself, especially since sending email is almost always very important. That and you could absolutely show error messages to the user while sending emails in the background, it would just need to be done asynchronously. There’s lots of ways to do that.

I think someone who reads this while having this knowledge will gain less trust in your docs because it says a downside of sending emails in the background is you can’t report errors to a user, but you can.

My 2¢

If UI/UX is a problem, it should be solved at the UI/UX level. I always have separated frontend, my request is already async. I’m not super familiar with LV, but I believe there should be ways around bad UX.

RoR and Python (when not using Event loop architecture), on an average server, can work with 10s, maybe 100s concurrent requests. In Elixir, it starts with 1000s.

There are two parts. 1. when async is not necessary, you can do it in sync fashion and can also handle error in sync. 2. If you do do it in async, AND it’s mission critical, you should use a job queue. Maybe the docs can use some further touch up.

See the above section. It can be important AND not async.

This refers to the Task module implementation. With job queues you sure can. Even with Task module, with some dirty tricks you can also report error, though not reliably. Either way there is extra effort required. While in sync fashion, it’s super easy and straightforward.

What people really often miss is, you can do it in sync in Elixir (given it already has a parallel model, like requests), and it can take you pretty far.

4 Likes

If it is a service outage you better show the user for what it is and not hiding it. If your service provider is this bad every once in a while then maybe you should find another.

For small sites the only config making sense is either to use a well known transactional email provider near you or to run your SMTP server on the same host, or at least in the same data center. Either way it is very fast and sending async does not buy you anything. As a library, Swoosh should make the easy things easy, and the hard things possible.

1 Like

It’s all out of our control. Things happen even to reputable services. DigitalOcean had a few minutes of downtime the other week in NYC3. S3 has gone down in the past, etc. I’m sure certain transactional email providers have have disrupted service once in a while.

A quick search on SendGrid’s status page shows they had mail delays in April 2021 SendGrid Status - Incident History and again in March 2021.

That’s why I think it’s worth it to design your system to be fault tolerant against these things and an easy win there is to send all emails in the background, preferably with a solution that knows how to do intelligent re-trying such as Oban. It gives you the highest chance of success that your email will be delivered and it ensures users get a fast page response since you control the request / response cycle.

4 Likes

Phoenix 1.6.0 is finally out with an RC version. I’ve released phoenix_swoosh 1.0.0-rc.0.

It works with Phoenix 1.6+. And, works with non-phoenix apps!

6 Likes