Skip to content

Commit 43d450e

Browse files
authored
Merge pull request #130 from amazon-mq/fix/gh-122-mailbox-flood
Fix remote reader mailbox flood from cancelled S3 requests
2 parents 10439e9 + 5a5273b commit 43d450e

4 files changed

Lines changed: 285 additions & 75 deletions

File tree

src/rabbitmq_stream_s3_api.erl

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ file-system operations. Use that in non-unit tests.
3030
delete/2,
3131
delete_prefix/1,
3232
delete_prefix/2,
33-
match_async/2,
33+
match_async/3,
3434
handle_async/3,
3535
cancel_async/2,
3636
request_duration_prometheus_format/0
@@ -79,13 +79,20 @@ examples.
7979
""".
8080
-callback delete(key() | [key()], request_opts()) -> ok | {error, any()}.
8181
-callback delete_prefix(key(), request_opts()) -> {ok, map()} | {error, any()}.
82-
-callback match_async(Msg :: term(), #{async_req() := async_state()}) ->
82+
-callback match_async(
83+
Msg :: term(),
84+
Reqs :: #{async_req() := async_state()},
85+
CancelledReqs :: #{async_req() => _}
86+
) ->
8387
{ok, async_req()}
88+
| {cancelled, async_req(), final | more}
8489
| error.
8590
-callback handle_async(Msg :: term(), async_req(), async_state()) ->
8691
{continue, async_state()}
8792
| {data, binary(), async_state() | done}
88-
| {done, ok | {error, any()}}.
93+
| {done, ok | {error, any()}}
94+
| {done_cancel, {error, any()}}
95+
| ignore.
8996
-callback cancel_async(async_req(), async_state()) -> ok.
9097

9198
-define(C_GET, 1).
@@ -240,15 +247,21 @@ delete_prefix(Prefix, Opts) when is_binary(Prefix) andalso is_map(Opts) ->
240247
counters:add(counter(), ?C_LIST, 1),
241248
observe(write, fun() -> (backend()):delete_prefix(Prefix, Opts) end).
242249

243-
-spec match_async(Msg :: term(), #{async_req() := async_state()}) ->
244-
{ok, async_req()} | error.
245-
match_async(Msg, Reqs) ->
246-
(backend()):match_async(Msg, Reqs).
250+
-spec match_async(
251+
Msg :: term(),
252+
Reqs :: #{async_req() := async_state()},
253+
CancelledReqs :: #{async_req() => _}
254+
) ->
255+
{ok, async_req()} | {cancelled, async_req(), final | more} | error.
256+
match_async(Msg, Reqs, CancelledReqs) ->
257+
(backend()):match_async(Msg, Reqs, CancelledReqs).
247258

248259
-spec handle_async(Msg :: term(), async_req(), AsyncState :: term()) ->
249260
{continue, AsyncState :: term()}
250261
| {data, binary(), async_state()}
251-
| {done, ok | {error, any()}}.
262+
| {done, ok | {error, any()}}
263+
| {done_cancel, {error, any()}}
264+
| ignore.
252265
handle_async(Msg, Req, {StartTs, BackendState0}) ->
253266
case (backend()):handle_async(Msg, Req, BackendState0) of
254267
{continue, BackendState} ->
@@ -262,7 +275,12 @@ handle_async(Msg, Req, {StartTs, BackendState0}) ->
262275
{data, Data, {StartTs, BackendState}};
263276
{done, _} = Done ->
264277
ok = finish_async(StartTs),
265-
Done
278+
Done;
279+
{done_cancel, _} = DoneCancel ->
280+
ok = finish_async(StartTs),
281+
DoneCancel;
282+
ignore ->
283+
ignore
266284
end.
267285

268286
-spec cancel_async(async_req(), async_state()) -> ok.

src/rabbitmq_stream_s3_api_aws.erl

Lines changed: 163 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ A wrapper around the AWS S3 HTTP API.
2626
delete/2,
2727
delete_prefix/2,
2828
list/3,
29-
match_async/2,
29+
match_async/3,
3030
handle_async/3,
3131
cancel_async/2
3232
]).
@@ -474,9 +474,16 @@ log_unexpected_status(Function, Status) ->
474474
log_unexpected_status(Function, Status, Key) ->
475475
?LOG_DEBUG("~ts unexpected HTTP status ~b for key ~ts", [Function, Status, Key]).
476476

477-
-spec match_async(Msg :: term(), #{async_req() := async_state()}) ->
478-
{ok, async_req()} | error.
479-
match_async({gun_error, Conn, _Reason}, Reqs) ->
477+
-spec match_async(
478+
Msg :: term(),
479+
Reqs :: #{async_req() := async_state()},
480+
CancelledReqs :: #{async_req() => _}
481+
) ->
482+
{ok, async_req()} | {cancelled, async_req(), final | more} | error.
483+
match_async({gun_error, Conn, _Reason}, Reqs, _CancelledReqs) ->
484+
%% Connection-level error: match any active request on this connection.
485+
%% We intentionally ignore cancelled requests - a dying connection only
486+
%% needs to notify live requests.
480487
maps:fold(
481488
fun
482489
(Req, #{conn := C}, error) when C =:= Conn -> {ok, Req};
@@ -485,18 +492,25 @@ match_async({gun_error, Conn, _Reason}, Reqs) ->
485492
error,
486493
Reqs
487494
);
488-
match_async(Msg, Reqs) ->
489-
Req =
495+
match_async(Msg, Reqs, CancelledReqs) ->
496+
{Req, Final} =
490497
case Msg of
491-
{gun_error, _, StreamRef, _} -> StreamRef;
492-
{gun_response, _, StreamRef, _, _, _} -> StreamRef;
493-
{gun_data, _, StreamRef, _, _} -> StreamRef;
494-
{request_timeout, StreamRef} -> StreamRef;
495-
_ -> undefined
498+
{gun_error, _, StreamRef, _} -> {StreamRef, final};
499+
{gun_response, _, StreamRef, fin, _, _} -> {StreamRef, final};
500+
{gun_response, _, StreamRef, nofin, _, _} -> {StreamRef, more};
501+
{gun_data, _, StreamRef, fin, _} -> {StreamRef, final};
502+
{gun_data, _, StreamRef, nofin, _} -> {StreamRef, more};
503+
{request_timeout, StreamRef} -> {StreamRef, final};
504+
_ -> {undefined, final}
496505
end,
497506
case Reqs of
498-
#{Req := _} -> {ok, Req};
499-
_ -> error
507+
#{Req := _} ->
508+
{ok, Req};
509+
_ ->
510+
case CancelledReqs of
511+
#{Req := _} -> {cancelled, Req, Final};
512+
_ -> error
513+
end
500514
end.
501515

502516
-spec handle_async(Msg :: term(), async_req(), async_state()) ->
@@ -552,19 +566,35 @@ handle_async(
552566
206 ->
553567
State = State0#{data => [], pending_bytes => 0},
554568
{continue, State};
555-
404 ->
556-
finish_async(State0),
557-
{done, {error, not_found}};
558-
500 ->
559-
finish_async(State0),
560-
{done, {error, internal_error}};
561-
503 ->
562-
finish_async(State0),
563-
{done, {error, slow_down}};
564569
_ ->
565-
finish_async(State0),
566-
{done, {error, #{status => Status, headers => Headers}}}
570+
%% Non-success response with a body. Cancel the request timer
571+
%% and drain the body before reporting the error so that the
572+
%% remaining gun_data frames do not orphan in the caller's
573+
%% mailbox.
574+
Reason =
575+
case Status of
576+
404 -> not_found;
577+
500 -> internal_error;
578+
503 -> slow_down;
579+
_ -> #{status => Status, headers => Headers}
580+
end,
581+
State = cancel_request_timer(State0),
582+
{continue, State#{draining => Reason}}
567583
end;
584+
handle_async(
585+
{gun_data, Conn, StreamRef, nofin, _Data},
586+
StreamRef,
587+
#{conn := Conn, stream_ref := StreamRef, draining := _}
588+
) ->
589+
%% Discard body data from a non-success response being drained.
590+
ignore;
591+
handle_async(
592+
{gun_data, Conn, StreamRef, fin, _Data},
593+
StreamRef,
594+
#{conn := Conn, stream_ref := StreamRef, draining := Reason} = State
595+
) ->
596+
finish_async(State),
597+
{done, {error, Reason}};
568598
handle_async(
569599
{gun_data, Conn, StreamRef, nofin, Data},
570600
StreamRef,
@@ -604,13 +634,33 @@ handle_async(
604634
counters:add(counter(), ?C_REQUEST_TIMEOUTS, 1),
605635
gun:cancel(Conn, StreamRef),
606636
finish_async(State),
607-
{done, {error, timeout}}.
637+
{done_cancel, {error, timeout}}.
608638

609639
-spec cancel_async(async_req(), async_state()) -> ok.
610640
cancel_async(StreamRef, #{conn := Conn, stream_ref := StreamRef} = State) ->
611641
gun:cancel(Conn, StreamRef),
612642
ok = finish_async(State).
613643

644+
%% Cancel the request timer without checking in the connection or
645+
%% decrementing the active-request counter. Used when the response
646+
%% body still needs to be drained before the request is complete.
647+
cancel_request_timer(State) ->
648+
case State of
649+
#{timer_ref := TimerRef, stream_ref := StreamRef} ->
650+
case erlang:cancel_timer(TimerRef) of
651+
false ->
652+
receive
653+
{request_timeout, StreamRef} -> ok
654+
after 0 -> ok
655+
end;
656+
_ ->
657+
ok
658+
end,
659+
maps:remove(timer_ref, State);
660+
_ ->
661+
State
662+
end.
663+
614664
finish_async(#{conn := Conn, pool := Pool} = State) ->
615665
case State of
616666
#{timer_ref := TimerRef, stream_ref := StreamRef} ->
@@ -1560,4 +1610,92 @@ sign_test() ->
15601610

15611611
ok.
15621612

1613+
match_async_active_request_test() ->
1614+
Ref = make_ref(),
1615+
Conn = self(),
1616+
Reqs = #{Ref => #{conn => Conn, stream_ref => Ref}},
1617+
?assertEqual(
1618+
{ok, Ref},
1619+
match_async({gun_data, Conn, Ref, nofin, <<"data">>}, Reqs, #{})
1620+
),
1621+
?assertEqual(
1622+
{ok, Ref},
1623+
match_async({gun_response, Conn, Ref, nofin, 200, []}, Reqs, #{})
1624+
),
1625+
?assertEqual(
1626+
{ok, Ref},
1627+
match_async({gun_error, Conn, Ref, some_reason}, Reqs, #{})
1628+
),
1629+
?assertEqual(
1630+
{ok, Ref},
1631+
match_async({request_timeout, Ref}, Reqs, #{})
1632+
),
1633+
ok.
1634+
1635+
match_async_cancelled_request_test() ->
1636+
Ref = make_ref(),
1637+
Conn = self(),
1638+
CancelledReqs = #{Ref => ok},
1639+
%% Terminal frames: fin data, fin response, stream-level gun_error,
1640+
%% request_timeout.
1641+
?assertEqual(
1642+
{cancelled, Ref, final},
1643+
match_async({gun_data, Conn, Ref, fin, <<"stale">>}, #{}, CancelledReqs)
1644+
),
1645+
?assertEqual(
1646+
{cancelled, Ref, final},
1647+
match_async({gun_response, Conn, Ref, fin, 200, []}, #{}, CancelledReqs)
1648+
),
1649+
?assertEqual(
1650+
{cancelled, Ref, final},
1651+
match_async({gun_error, Conn, Ref, some_reason}, #{}, CancelledReqs)
1652+
),
1653+
?assertEqual(
1654+
{cancelled, Ref, final},
1655+
match_async({request_timeout, Ref}, #{}, CancelledReqs)
1656+
),
1657+
%% Non-terminal frames: nofin data, nofin response.
1658+
?assertEqual(
1659+
{cancelled, Ref, more},
1660+
match_async({gun_data, Conn, Ref, nofin, <<"stale">>}, #{}, CancelledReqs)
1661+
),
1662+
?assertEqual(
1663+
{cancelled, Ref, more},
1664+
match_async({gun_response, Conn, Ref, nofin, 200, []}, #{}, CancelledReqs)
1665+
),
1666+
ok.
1667+
1668+
match_async_unknown_request_test() ->
1669+
Ref = make_ref(),
1670+
Conn = self(),
1671+
?assertEqual(
1672+
error,
1673+
match_async({gun_data, Conn, Ref, nofin, <<"x">>}, #{}, #{})
1674+
),
1675+
?assertEqual(
1676+
error,
1677+
match_async(some_other_message, #{}, #{})
1678+
),
1679+
ok.
1680+
1681+
match_async_connection_error_ignores_cancelled_test() ->
1682+
%% A connection-level gun_error (3-tuple) should match an active request
1683+
%% on that connection, not a cancelled one - even if the only request on
1684+
%% the connection is cancelled, we return `error` (the connection dying
1685+
%% has no active request to notify).
1686+
Conn = self(),
1687+
Ref = make_ref(),
1688+
?assertEqual(
1689+
error,
1690+
match_async({gun_error, Conn, some_reason}, #{}, #{Ref => ok})
1691+
),
1692+
%% With an active request on the connection, match_async returns it.
1693+
ActiveRef = make_ref(),
1694+
Reqs = #{ActiveRef => #{conn => Conn, stream_ref => ActiveRef}},
1695+
?assertEqual(
1696+
{ok, ActiveRef},
1697+
match_async({gun_error, Conn, some_reason}, Reqs, #{Ref => ok})
1698+
),
1699+
ok.
1700+
15631701
-endif.

src/rabbitmq_stream_s3_api_fs.erl

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ associated file in that folder.
2323
stream_finish/2,
2424
delete/2,
2525
delete_prefix/2,
26-
match_async/2,
26+
match_async/3,
2727
handle_async/3,
2828
cancel_async/2
2929
]).
@@ -203,11 +203,17 @@ delete_prefix(Prefix, Opts) when is_binary(Prefix) andalso is_map(Opts) ->
203203
end
204204
end).
205205

206-
-spec match_async(Msg :: term(), #{async_req() := async_state()}) ->
207-
{ok, async_req()} | error.
208-
match_async({'$async', Req, _Msg}, Reqs) when is_map_key(Req, Reqs) ->
206+
-spec match_async(
207+
Msg :: term(),
208+
Reqs :: #{async_req() := async_state()},
209+
CancelledReqs :: #{async_req() => _}
210+
) ->
211+
{ok, async_req()} | {cancelled, async_req(), final | more} | error.
212+
match_async({'$async', Req, _Msg}, Reqs, _CancelledReqs) when is_map_key(Req, Reqs) ->
209213
{ok, Req};
210-
match_async(_Msg, _Reqs) ->
214+
match_async({'$async', Req, _Msg}, _Reqs, CancelledReqs) when is_map_key(Req, CancelledReqs) ->
215+
{cancelled, Req, final};
216+
match_async(_Msg, _Reqs, _CancelledReqs) ->
211217
error.
212218

213219
-spec handle_async(Msg :: term(), async_req(), async_state()) ->
@@ -330,3 +336,16 @@ range_spec_to_location_number(FileSize, {StartByte, EndByte}) ->
330336
_ -> EndByte - StartByte + 1
331337
end,
332338
{StartByte, Number}.
339+
340+
-ifdef(TEST).
341+
-include_lib("eunit/include/eunit.hrl").
342+
343+
match_async_test() ->
344+
Ref = make_ref(),
345+
?assertEqual({ok, Ref}, match_async({'$async', Ref, data}, #{Ref => state}, #{})),
346+
?assertEqual({cancelled, Ref, final}, match_async({'$async', Ref, data}, #{}, #{Ref => ok})),
347+
?assertEqual(error, match_async({'$async', Ref, data}, #{}, #{})),
348+
?assertEqual(error, match_async(other_message, #{}, #{})),
349+
ok.
350+
351+
-endif.

0 commit comments

Comments
 (0)