Skip to content

Commit ebfa104

Browse files
committed
Code for step 4
1 parent 4015721 commit ebfa104

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

lib/rate_limiters/token_bucket.ex

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
defmodule PaymentsClient.RateLimiters.TokenBucket do
2+
use GenServer
3+
4+
require Logger
5+
6+
alias PaymentsClient.RateLimiter
7+
8+
@behaviour RateLimiter
9+
10+
def start_link(opts) do
11+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
12+
end
13+
14+
@impl true
15+
def init(opts) do
16+
state = %{
17+
requests_per_timeframe: opts.timeframe_max_requests,
18+
available_tokens: opts.timeframe_max_requests,
19+
token_refresh_rate:
20+
RateLimiter.calculate_refresh_rate(opts.timeframe_max_requests, opts.timeframe, opts.timeframe_units),
21+
request_queue: :queue.new(),
22+
request_queue_size: 0,
23+
send_after_ref: nil
24+
}
25+
26+
{:ok, state, {:continue, :initial_timer}}
27+
end
28+
29+
# ---------------- Client facing function ----------------
30+
31+
@impl RateLimiter
32+
def make_request(request_handler, response_handler) do
33+
GenServer.cast(__MODULE__, {:enqueue_request, request_handler, response_handler})
34+
end
35+
36+
# ---------------- Server Callbacks ----------------
37+
38+
@impl true
39+
def handle_continue(:initial_timer, state) do
40+
{:noreply, %{state | send_after_ref: schedule_timer(state.token_refresh_rate)}}
41+
end
42+
43+
@impl true
44+
# No tokens available...enqueue the request
45+
def handle_cast({:enqueue_request, request_handler, response_handler}, %{available_tokens: 0} = state) do
46+
updated_queue = :queue.in({request_handler, response_handler}, state.request_queue)
47+
new_queue_size = state.request_queue_size + 1
48+
49+
{:noreply, %{state | request_queue: updated_queue, request_queue_size: new_queue_size}}
50+
end
51+
52+
# Tokens available...use one of the tokens and perform the operation immediately
53+
def handle_cast({:enqueue_request, request_handler, response_handler}, state) do
54+
async_task_request(request_handler, response_handler)
55+
56+
{:noreply, %{state | available_tokens: state.available_tokens - 1}}
57+
end
58+
59+
@impl true
60+
def handle_info(:token_refresh, %{request_queue_size: 0} = state) do
61+
# No work to do as the queue size is zero...schedule the next timer and increase the token count
62+
token_count =
63+
if state.available_tokens < state.requests_per_timeframe do
64+
state.available_tokens + 1
65+
else
66+
state.available_tokens
67+
end
68+
69+
{:noreply,
70+
%{
71+
state
72+
| send_after_ref: schedule_timer(state.token_refresh_rate),
73+
available_tokens: token_count
74+
}}
75+
end
76+
77+
def handle_info(:token_refresh, state) do
78+
{{:value, {request_handler, response_handler}}, new_request_queue} = :queue.out(state.request_queue)
79+
80+
async_task_request(request_handler, response_handler)
81+
82+
{:noreply,
83+
%{
84+
state
85+
| request_queue: new_request_queue,
86+
send_after_ref: schedule_timer(state.token_refresh_rate),
87+
request_queue_size: state.request_queue_size - 1
88+
}}
89+
end
90+
91+
def handle_info({ref, _result}, state) do
92+
Process.demonitor(ref, [:flush])
93+
94+
{:noreply, state}
95+
end
96+
97+
def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do
98+
{:noreply, state}
99+
end
100+
101+
defp async_task_request(request_handler, response_handler) do
102+
start_message = "Request started #{NaiveDateTime.utc_now()}"
103+
104+
Task.Supervisor.async_nolink(RateLimiter.TaskSupervisor, fn ->
105+
{req_module, req_function, req_args} = request_handler
106+
{resp_module, resp_function} = response_handler
107+
108+
response = apply(req_module, req_function, req_args)
109+
apply(resp_module, resp_function, [response])
110+
111+
Logger.info("#{start_message}\nRequest completed #{NaiveDateTime.utc_now()}")
112+
end)
113+
end
114+
115+
defp schedule_timer(token_refresh_rate) do
116+
Process.send_after(self(), :token_refresh, token_refresh_rate)
117+
end
118+
end

0 commit comments

Comments
 (0)