@@ -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) ->
474474log_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 }};
568598handle_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 .
610640cancel_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+
614664finish_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 .
0 commit comments