Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Adds Retry-After headers to throttled requests.

  • Loading branch information...
commit eda4e40da982b0e00cc26522e25b348968856200 1 parent 81d337e
Moritz authored
6 README.md
Source Rendered
@@ -20,13 +20,9 @@ To play around with it on localhost, just clone, `make compile`, `./start.sh` an
20 20
21 21 ## todo
22 22
23   -security:
24   -
25   -+ add a `retry-after` header on throttled requests
26   -
27 23 usability:
28 24
29   -+ timeline of visits by hour
  25 ++ timeline of visits by (UTC) time-range: morning (6-11), mid-day (11-16), evening (16-22), night (22-6)
30 26 + thumbnails on the landing pages?
31 27 + ensure that no matter the `STATS_COLLECT_INTERVAL`, statistics don't get corrupted
32 28
36 src/erli_throttle.erl
@@ -24,7 +24,7 @@
24 24
25 25 -include("erli.hrl").
26 26
27   --record(state, {reqs_handled=0, span}).
  27 +-record(state, {reqs_handled=0, span, last_reset}).
28 28
29 29 start_link() ->
30 30 case whereis(erli_throttle) of
@@ -45,28 +45,35 @@ throttle_req() ->
45 45 %%% gen_server callbacks
46 46 %%%=============================================================================
47 47 init([?THROTTLE_TIME_SPAN]) when is_list(?THROTTLE_TIME_SPAN) ->
48   - {_Date, {H, M, S}} = calendar:universal_time(),
  48 + {_Date, {H, M, S} = Time} = calendar:universal_time(),
49 49 % calculate the initial offset to make the interval 'nice'
50 50 case ?THROTTLE_TIME_SPAN of % for now these are hard-coded
51 51 "hour" ->
52 52 S2Go = 60 - S,
53 53 M2Go = 60 - M,
54   - T = (M2Go * 60 + S2Go) * 1000,
55   - {ok, #state{span=3600000}, T};
  54 + T = M2Go * 60 + S2Go,
  55 + erlang:send_after(T * 1000, self(), reset), % trigger the reset cycle
  56 + {ok, #state{span=3600,
  57 + last_reset=calendar:time_to_seconds(Time) - S - M * 60}};
56 58 "day" ->
57   - T = (86400 - calendar:time_to_seconds({H, M, S})) * 1000,
58   - {ok, #state{span=86400000}, T}
  59 + T = 86400 - calendar:time_to_seconds({H, M, S}),
  60 + erlang:send_after(T * 1000, self(), reset),
  61 + {ok, #state{span=86400, last_reset=86400 - calendar:time_to_seconds(Time)}}
59 62 end;
60 63 init([?THROTTLE_TIME_SPAN]) ->
61 64 % you're on your own here...
62 65 {ok, #state{span=?THROTTLE_TIME_SPAN}, 0}.
63 66
64   -handle_call(is_throttled, _From, #state{reqs_handled=RH, _=_}= State) ->
65   - C = RH+1,
  67 +handle_call(is_throttled, _From, #state{reqs_handled=RH,
  68 + span=Span,
  69 + last_reset=LR}=State) ->
  70 + C = RH + 1,
66 71 NewState = State#state{reqs_handled=C},
67 72 if
68 73 C > ?REQ_LIMIT ->
69   - {reply, true, NewState};
  74 + {_, CurTime} = calendar:universal_time(),
  75 + RetryAfter = LR + Span - calendar:time_to_seconds(CurTime),
  76 + {reply, {true, RetryAfter}, NewState};
70 77 C =< ?REQ_LIMIT ->
71 78 {reply, false, NewState}
72 79 end.
@@ -78,13 +85,12 @@ handle_cast(_Req, State) ->
78 85 handle_info(reset, State) ->
79 86 error_logger:info_msg("[ERLI] handled a total of ~p requests",
80 87 [State#state.reqs_handled]),
81   - erlang:send_after(State#state.span, self(), reset),
82   - {noreply, State#state{reqs_handled=0}};
  88 + {_, T} = calendar:universal_time(),
  89 + Time = calendar:time_to_seconds(T),
  90 + erlang:send_after(State#state.span * 1000, self(), reset),
  91 + {noreply, State#state{reqs_handled=0, last_reset=Time}};
83 92 handle_info(timeout, State) ->
84   - error_logger:info_msg("[ERLI] handled a total of ~p requests",
85   - [State#state.reqs_handled]),
86   - erlang:send_after(State#state.span, self(), reset),
87   - {noreply, State#state{reqs_handled=0}}.
  93 + {noreply, State}.
88 94
89 95 terminate(_Reason, _State) ->
90 96 ok.
8 src/path_resource.erl
@@ -27,7 +27,13 @@ init([]) ->
27 27 {ok, #target{}}.
28 28
29 29 service_available(RD, Ctx) ->
30   - {not erli_throttle:throttle_req(), RD, Ctx}.
  30 + case erli_throttle:throttle_req() of
  31 + false ->
  32 + {true, RD, Ctx};
  33 + {true, RetryAfter}->
  34 + NRD = wrq:set_resp_header("Retry-After", integer_to_list(RetryAfter), RD),
  35 + {false, NRD, Ctx}
  36 + end.
31 37
32 38 allowed_methods(RD, Ctx) ->
33 39 case wrq:disp_path(RD) of
8 src/root_resource.erl
@@ -22,7 +22,13 @@ init([]) ->
22 22 {ok, #target{}}.
23 23
24 24 service_available(RD, Ctx) ->
25   - {not erli_throttle:throttle_req(), RD, Ctx}.
  25 + case erli_throttle:throttle_req() of
  26 + false ->
  27 + {true, RD, Ctx};
  28 + {true, RetryAfter}->
  29 + NRD = wrq:set_resp_header("Retry-After", integer_to_list(RetryAfter), RD),
  30 + {false, NRD, Ctx}
  31 + end.
26 32
27 33 allowed_methods(RD, Ctx) ->
28 34 {['GET', 'POST'], RD, Ctx}.
0  templates/error_handlers/badgateway.dtl → templates/badgateway.dtl
File renamed without changes
0  templates/error_handlers/badrequest.dtl → templates/badrequest.dtl
File renamed without changes
0  templates/error_handlers/forbidden.dtl → templates/forbidden.dtl
File renamed without changes
0  templates/error_handlers/notfound.dtl → templates/notfound.dtl
File renamed without changes
0  templates/error_handlers/notimplemented.dtl → templates/notimplemented.dtl
File renamed without changes
0  templates/error_handlers/serverfault.dtl → templates/serverfault.dtl
File renamed without changes
0  templates/error_handlers/unavailable.dtl → templates/unavailable.dtl
File renamed without changes

0 comments on commit eda4e40

Please sign in to comment.
Something went wrong with that request. Please try again.