Skip to content

Commit 9b6a5f7

Browse files
committed
Add rate-limiter mail delivery strategy
1 parent c9d44a4 commit 9b6a5f7

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

config/prod.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ config :prison_rideshare, PrisonRideshare.Repo,
1515
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
1616
ssl: true
1717

18+
config :prison_rideshare, PrisonRideshare.Mailer,
19+
deliver_later_strategy: PrisonRideshare.MailerRateLimiter,
20+
rate_limit_ms: String.to_integer(System.get_env("MAILER_RATE_LIMIT_MS") || "30000")
21+
1822
config :prison_rideshare, gas_price_endpoint: System.get_env("GAS_PRICE_ENDPOINT")

lib/prison_rideshare/application.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule PrisonRideshare.Application do
1010
{Phoenix.PubSub, name: PrisonRideshare.PubSub},
1111
# Start the Ecto repository
1212
PrisonRideshare.Repo,
13+
PrisonRideshare.MailerRateLimiter,
1314
# Start the endpoint when the application starts
1415
PrisonRideshareWeb.Endpoint,
1516
# Start your own worker by calling: PrisonRideshare.Worker.start_link(arg1, arg2, arg3)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
defmodule PrisonRideshare.MailerRateLimiter do
2+
@behaviour Bamboo.DeliverLaterStrategy
3+
use GenServer
4+
5+
require Logger
6+
7+
@default_interval_ms 30000
8+
9+
def start_link(opts \\ []) do
10+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
11+
end
12+
13+
@impl Bamboo.DeliverLaterStrategy
14+
def deliver_later(adapter, email, config) do
15+
GenServer.cast(__MODULE__, {:enqueue, adapter, email, config})
16+
end
17+
18+
@impl GenServer
19+
def init(_opts) do
20+
{:ok,
21+
%{
22+
queue: :queue.new(),
23+
interval_ms: delivery_interval_ms(),
24+
in_flight: false
25+
}}
26+
end
27+
28+
@impl GenServer
29+
def handle_cast({:enqueue, adapter, email, config}, state) do
30+
state = %{state | queue: :queue.in({adapter, email, config}, state.queue)}
31+
{:noreply, maybe_schedule(state)}
32+
end
33+
34+
@impl GenServer
35+
def handle_info(:deliver_next, state) do
36+
case :queue.out(state.queue) do
37+
{{:value, {adapter, email, config}}, queue} ->
38+
deliver(adapter, email, config)
39+
state = %{state | queue: queue}
40+
41+
if :queue.is_empty(queue) do
42+
{:noreply, %{state | in_flight: false}}
43+
else
44+
Process.send_after(self(), :deliver_next, state.interval_ms)
45+
{:noreply, state}
46+
end
47+
48+
{:empty, _queue} ->
49+
{:noreply, %{state | in_flight: false}}
50+
end
51+
end
52+
53+
defp maybe_schedule(%{in_flight: true} = state), do: state
54+
55+
defp maybe_schedule(state) do
56+
send(self(), :deliver_next)
57+
%{state | in_flight: true}
58+
end
59+
60+
defp deliver(adapter, email, config) do
61+
try do
62+
case adapter.deliver(email, config) do
63+
{:error, error} ->
64+
Logger.error("Email delivery failed: #{inspect(error)}")
65+
66+
_ ->
67+
:ok
68+
end
69+
rescue
70+
exception ->
71+
Logger.error(Exception.format(:error, exception, __STACKTRACE__))
72+
catch
73+
kind, reason ->
74+
Logger.error(Exception.format(kind, reason, __STACKTRACE__))
75+
end
76+
end
77+
78+
defp delivery_interval_ms do
79+
config = Application.get_env(:prison_rideshare, PrisonRideshare.Mailer, [])
80+
config[:rate_limit_ms] || @default_interval_ms
81+
end
82+
end

0 commit comments

Comments
 (0)