3636 terminate /2 , code_change /3 ]).
3737
3838% % Used by eredis_sub_client.erl
39- -export ([read_database /1 , get_auth_command /2 , connect /7 , reconnect_loop / 9 ]).
39+ -export ([read_database /1 , get_auth_command /2 , connect /7 ]).
4040
4141-record (state , {
4242 host :: string () | {local , string ()} | undefined ,
5050
5151 transport :: gen_tcp | ssl ,
5252 socket :: gen_tcp :socket () | ssl :sslsocket () | undefined ,
53+ reconnect_timer :: reference () | undefined ,
5354 parser_state :: # pstate {} | undefined ,
5455 queue :: eredis_queue () | undefined
5556 }).
@@ -156,18 +157,15 @@ handle_info({Type, Socket, Data},
156157 ok ->
157158 {noreply , State };
158159 {error , Reason } ->
159- Transport :close (Socket ),
160160 maybe_reconnect (Reason , State )
161161 end ;
162162
163163% % Socket errors. If the network or peer is down, the error is not
164164% % always followed by a tcp_closed.
165165% %
166166% % TLS 1.3: Called after a connect when the client certificate has expired
167- handle_info ({Error , Socket , Reason },
168- # state {socket = Socket , transport = Transport } = State )
167+ handle_info ({Error , Socket , Reason }, # state {socket = Socket } = State )
169168 when Error =:= tcp_error ; Error =:= ssl_error ->
170- Transport :close (Socket ),
171169 maybe_reconnect (Reason , State );
172170
173171% % Socket got closed, for example by Redis terminating idle
@@ -179,7 +177,7 @@ handle_info({Error, Socket, Reason},
179177handle_info ({Closed , Socket }, # state {socket = OurSocket } = State )
180178 when Closed =:= tcp_closed orelse Closed =:= ssl_closed ,
181179 Socket =:= OurSocket orelse Socket =:= fake_socket ->
182- maybe_reconnect (Closed , State );
180+ maybe_reconnect (Closed , State # state { socket = undefined } );
183181
184182% % Ignore messages and errors for an old socket.
185183handle_info ({Type , Socket , _ }, # state {socket = OurSocket } = State )
@@ -198,9 +196,7 @@ handle_info({Type, Socket}, #state{socket = OurSocket} = State)
198196
199197% % Errors returned by gen_tcp:send/2 and ssl:send/2 are handled
200198% % asynchronously by message passing to self.
201- handle_info ({send_error , Socket , Reason },
202- # state {transport = Transport , socket = Socket } = State ) ->
203- Transport :close (Socket ),
199+ handle_info ({send_error , Socket , Reason }, # state {socket = Socket } = State ) ->
204200 maybe_reconnect (Reason , State );
205201
206202handle_info ({send_error , _Socket , _Reason }, State ) ->
@@ -217,22 +213,20 @@ handle_info({connection_ready, Socket}, #state{socket = undefined} = State) ->
217213handle_info (stop , State ) ->
218214 {stop , shutdown , State };
219215
220- handle_info (initiate_connection ,
221- # state {socket = undefined ,
222- reconnect_sleep = ReconnectSleep } = State ) ->
216+ handle_info (initiate_connection , # state {socket = undefined } = State ) ->
223217 case connect (State ) of
224218 {ok , NewState } ->
225219 {noreply , NewState };
226- {error , _Reason } when ReconnectSleep =:= no_reconnect ->
227- {stop , normal , State };
228220 {error , Reason } ->
229- erlang :send_after (ReconnectSleep , self (), {reconnect , Reason }),
230- {noreply , State }
221+ {noreply , schedule_reconnect (Reason , State )}
231222 end ;
232223
233224handle_info ({reconnect , Reason }, # state {socket = undefined } = State ) ->
234- % % Scheduled reconnect
235- maybe_reconnect (Reason , State );
225+ % % Scheduled reconnect, if disconnected.
226+ maybe_reconnect (Reason , State # state {reconnect_timer = undefined });
227+ handle_info ({reconnect , _Reason }, State ) ->
228+ % % Already connected.
229+ {noreply , State # state {reconnect_timer = undefined }};
236230
237231handle_info (_Info , State ) ->
238232 {noreply , State }.
@@ -363,7 +357,9 @@ safe_send(Pid, Value) ->
363357 end .
364358
365359% % @doc: Helper for connecting to Redis, authenticating and selecting
366- % % the correct database synchronously.
360+ % % the correct database synchronously. On successful connect, a reconnect
361+ % % is scheduled, just in case the connection breaks immediately afterwards,
362+ % % so we don't reconnect until reconnect_sleep milliseconds has elapsed.
367363% % Returns: {ok, State} or {error, Reason}.
368364connect (# state {host = Host ,
369365 port = Port ,
@@ -375,7 +371,11 @@ connect(#state{host = Host,
375371 case connect (Host , Port , SocketOptions , TlsOptions ,
376372 ConnectTimeout , AuthCmd , Db ) of
377373 {ok , Socket } ->
378- {ok , State # state {socket = Socket }};
374+ % % In case the connection terminates immediately (this happens with
375+ % % an expired certificate with TLS 1.3) schedule a reconnect already
376+ % % so that we don't try to reconnect if an error is received before
377+ % % reconnect_sleep milliseconds has elapsed.
378+ {ok , schedule_reconnect (unknown , State # state {socket = Socket })};
379379 Error ->
380380 Error
381381 end .
@@ -529,64 +529,53 @@ transport_module(_) -> ssl.
529529setopts (Socket , _Transport = gen_tcp , Opts ) -> inet :setopts (Socket , Opts );
530530setopts (Socket , _Transport = ssl , Opts ) -> ssl :setopts (Socket , Opts ).
531531
532+ close_socket (# state {socket = undefined } = State ) ->
533+ State ;
534+ close_socket (# state {socket = Socket , transport = Transport } = State ) ->
535+ Transport :close (Socket ),
536+ State # state {socket = undefined }.
537+
538+ % % @doc Schedules a reconnect attempt, if reconnect is enabled.
539+ -spec schedule_reconnect (Reason :: any (), # state {}) -> # state {}.
540+ schedule_reconnect (_Reason , # state {reconnect_sleep = no_reconnect } = State ) ->
541+ State ;
542+ schedule_reconnect (Reason , # state {reconnect_sleep = ReconnectSleep ,
543+ reconnect_timer = undefined } = State ) ->
544+ TRef = erlang :send_after (ReconnectSleep , self (), {reconnect , Reason }),
545+ State # state {reconnect_timer = TRef }.
546+
547+ % % @doc Reconnects, but not if a reconnect has been scheduled or if reconnect is
548+ % % disabled. The socket in the state is closed, if any. Returns {noreply, State}
549+ % % or {stop, ExitReason, State} like handle_info.
532550maybe_reconnect (Reason , # state {reconnect_sleep = no_reconnect , queue = Queue } = State ) ->
533551 reply_all ({error , Reason }, Queue ),
534552 % % If we aren't going to reconnect, then there is nothing else for
535553 % % this process to do.
536- {stop , normal , State # state {socket = undefined }};
554+ {stop , normal , close_socket (State )};
555+ maybe_reconnect (Reason , # state {queue = Queue , reconnect_timer = TRef } = State )
556+ when is_reference (TRef ) ->
557+ % % Reconnect already scheduled.
558+ reply_all ({error , Reason }, Queue ),
559+ {noreply , close_socket (State # state {queue = queue :new ()})};
537560maybe_reconnect (Reason ,
538561 # state {queue = Queue ,
539562 host = Host ,
540563 port = Port ,
541- socket_options = SocketOptions ,
542- tls_options = TlsOptions ,
543- connect_timeout = ConnectTimeout ,
544- reconnect_sleep = ReconnectSleep ,
545- auth_cmd = AuthCmd ,
546- database = Db } = State ) ->
564+ reconnect_timer = undefined } = State ) ->
547565 error_logger :error_msg (" eredis: Re-establishing connection to ~p :~p due to ~p " ,
548566 [Host , Port , Reason ]),
549- Self = self (),
550- spawn_link (fun () ->
551- process_flag (trap_exit , true ),
552- reconnect_loop (Self , ReconnectSleep , Host , Port ,
553- SocketOptions , TlsOptions , ConnectTimeout ,
554- AuthCmd , Db )
555- end ),
556-
557- % % tell all of our clients what has happened.
567+ % % Tell all of our clients what has happened.
558568 reply_all ({error , Reason }, Queue ),
559569
560570 % % Throw away the socket and the queue, as we will never get a
561571 % % response to the requests sent on the old socket. The absence of
562572 % % a socket is used to signal we are "down"
563- {noreply , State # state {socket = undefined , queue = queue :new ()}}.
564-
565- % % @doc: Loop until a connection can be established, this includes
566- % % successfully issuing the auth and select calls. When we have a
567- % % connection, send the socket to Client in a message on the form
568- % % `{connection_ready, Socket}'.
569- reconnect_loop (Client , ReconnectSleep , Host , Port , SocketOptions ,
570- TlsOptions , ConnectTimeout , AuthCmd , Db ) ->
571- Client ! reconnect_attempt ,
572- case connect (Host , Port , SocketOptions , TlsOptions , ConnectTimeout ,
573- AuthCmd , Db ) of
574- {ok , Socket } ->
575- Client ! {connection_ready , Socket },
576- Transport = transport_module (TlsOptions ),
577- Transport :controlling_process (Socket , Client ),
578- Msgs = get_all_messages ([]),
579- [Client ! M || M <- Msgs ];
573+ State1 = close_socket (State # state {queue = queue :new ()}),
574+ case connect (State1 ) of
575+ {ok , State2 } ->
576+ {noreply , State2 };
580577 {error , Reason } ->
581- Client ! {reconnect_failed , Reason },
582- receive
583- {'EXIT' , Client , Reason } -> exit (Reason )
584- after
585- ReconnectSleep ->
586- reconnect_loop (Client , ReconnectSleep , Host , Port ,
587- SocketOptions , TlsOptions , ConnectTimeout ,
588- AuthCmd , Db )
589- end
578+ {noreply , schedule_reconnect (Reason , State1 )}
590579 end .
591580
592581read_database (undefined ) ->
@@ -605,11 +594,3 @@ get_auth_command(undefined, Password) ->
605594 eredis :create_multibulk ([<<" AUTH" >>, Password ]);
606595get_auth_command (Username , Password ) ->
607596 eredis :create_multibulk ([<<" AUTH" >>, Username , Password ]).
608-
609- get_all_messages (Acc ) ->
610- receive
611- M ->
612- get_all_messages ([M | Acc ])
613- after 0 ->
614- lists :reverse (Acc )
615- end .
0 commit comments