distefam

distefam

Structuring supervisor tree for newsletter application

I am extending an existing application to allow users to be able to write and send newsletters and struggling with determining a proper supervision tree (I’m relatively new to OTP so please bear with me).

Overview

Users can create a single Newsletter, which has “subscribers” via a Subscription schema. A user may write multiple Letters within a given newsletter, each of which may be sent to a subset of their subscribers, which I’ll call “recipients”.

After a user writes a Letter and goes to send it, the plan is to kick off a background job to handle the sending of letters to all recipients.

First Attempt

  • Create a Task.Supervisor called App.MailmanSupervisor and add it to the top-level Application supervisor as a child.
  • Create a App.LetterDeliveryDispatcher, which is a DynamicSupervisor that will supervise individual App.LetterDeliveryServices, one for each Letter to send. This is also started by the top level Application.
  • App.LetterDeliveryService is a GenServer that manages sending a single Letter to all specified recipients. These are dynamically created (by calling App.LetterDeliveryDispatcher.add_child when it is time to send a given letter.

The App.LetterDeliveryService

This GenServer would take the Letter.id as a parameter to start_link/1 and add a Map called deliveries to its state in init/1 to keep track of the status of individual deliveries.

Then, to handle the sending of letters to individual recipients, use Task.Supervisor.async_stream_nolink/4 from a handle_continue/2 callback. Process and filter the results, handling {:exit, reason} tuples (I’m using zip_input_on_exit with async_stream_nolink) so that they include their result. Ultimately, update the deliveries map in the state with: {:stop, :normal, %{state | deliveries: deliveries} based on these return values, which would yield a map looking like:

%{
1 => :pending,
2 => {:delivered, 34},
3 => {:failed, :timeout},
}

The individual Task workers would receive a recipient id and they would:

  1. Query to get the full Subscription record using the recipient id
  2. Determine if and how to send to this subscriber based on some business logic
  3. Send the letter to the recipient if applicable
  4. Create a NewsletterEmail database record with foreign keys to both the Subscription and the Letter along with some other metadata related to the sent email.

Retries

One disadvantage of using a TaskSupervisor is that there isn’t any retry logic for individual worker tasks (the ones sending emails and doing database queries and updates). So, I was thinking of using the retry logic of the LetterDeliveryService genserver by setting it to :transient and exiting without :normal if any of its child Tasks return {:exit, _} so that it is restarted by the LetterDeliveryDispatcher.

The key here is that I’d save the deliveries state with this exit signal in terminate/2. I was thinking of doing this in a jsonb column on Letter called mail_order, which would hold information about the current delivery.

When the LetterDeliveryService is restarted (recall that it takes a Letter.id) I would then check the letter’s mail_order field and see if any of the letters still need to be sent. If not, I’d terminate with :normal and if so, I’d try to send those letters again, repeating the process.

I could also easily amend deliveries (which are persisted in the mail_order column on a Letter) to hold information on number of retries so I could control the number of retries.

Questions

  1. First, this is my first serious attempt at using various OTP behaviours and so I’m not sure whether this is a good approach or whether I’m overthinking things. Would you tackle this in another way?

    I thought I should have a long-running GenServer (LetterDeliveryService) per-letter to track the sending to all of its recipients, but I could also imagine a scenario in which the mailing to individual recipients are handled in one sort of “queue” irrespective of the Letter they are responsible for delivering.

  2. If the approach is mostly correct, should I use the async_stream_nolink method I described above, handling all results once they come in, or would it be better to use async_nolink and use the handle_info callbacks of the LetterDeliveryService GenServer?

  3. Is there a better way to handle retry logic?

  4. Anything else I’m missing?

Finally, I am aware that there are third-party libraries like Oban and Parent but I’d like to try to do this without relying on third-party libraries unless it really makes sense to include them. I want to make sure I understand how to architech such a system with OTP before integrating such libraries. But, if there is a very strong case to be had that one of those (or another) library is really what I should be using then of course I will use it.

Most Liked

distefam

distefam

I ended up going with Oban Pro.

Where Next?

Popular in Questions Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

We're in Beta

About us Mission Statement