Search code examples
erlangerlang-otpgun

how to use gun:open in a gen_server module


I have a gen_server module, I use gun as http client to make a long pull connection with a http server, so I call gun:open in my module's init, but if gun:open fail, my module fail, so my application fail to start. What is the proper way to do this. The following is my code:

init() ->
    lager:debug("http_api_client: connecting to admin server...~n"),
    {ok, ConnPid} = gun:open("localhost", 5001),
    {ok, Protocol} = gun:await_up(ConnPid),
    {ok, #state{conn_pid = ConnPid, streams = #{},protocol =  Protocol}}.

Solution

  • Basically you have two options: either your process requires the HTTP server to be available (your current solution), or it doesn't, and handles requests while the connection to the HTTP server is down gracefully (by returning error responses). This blog post presents this idea more eloquently: https://ferd.ca/it-s-about-the-guarantees.html

    You could do that by separating this code out into a separate function, that doesn't crash if the connection fails:

    try_connect(State) ->
        lager:debug("http_api_client: connecting to admin server...~n"),
        case gun:open("localhost", 5001) of
            {ok, ConnPid} ->
                {ok, Protocol} = gun:await_up(ConnPid),
                State#state{conn_pid = ConnPid, streams = #{},protocol =  Protocol};
            {error, _} ->
                State#state{conn_pid = undefined}
        end.
    

    And call this function from init. That is, regardless of whether you can connect, your gen_server will start.

    init(_) ->
        {ok, try_connect(#state{})}.
    

    Then, when you make a request to this gen_server that requires the connection to be present, check whether it is undefined:

    handle_call(foo, _, State = #state{conn_pid = undefined}) ->
        {reply, {error, not_connected}, State};
    handle_call(foo, _, State = #state{conn_pid = ConnPid}) ->
        %% make a request through ConnPid here
        {reply, ok, State};
    

    Of course, that means that if the connection fails at startup, your gen_server will never try to connect again. You could add a timer, or you could add an explicit reconnect command:

    handle_call(reconnect, _, State = #state{conn_pid = undefined}) ->
        NewState = try_connect(State),
        Result = case NewState of
                     #state{conn_pid = undefined} ->
                         reconnect_failed;
                     _ ->
                         ok
                 end,
        {reply, Result, NewState};
    handle_call(reconnect, _, State) ->
        {reply, already_connected, State}.
    

    The code above doesn't handle the case when the connection goes down while the gen_server is running. You could handle that explicitly, or you could just let your gen_server process crash in that case, so that it restarts into the "not connected" state.