@@ -51,7 +51,10 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
5151 connection_mod =
5252 Keyword . get ( opts , :connection_mod , { Postgrex.SimpleConnection , :start_link , [ ] } )
5353
54- { :ok , % { connection_mod: connection_mod , stack_id: stack_id } , { :continue , :notify_ready } }
54+ tcp_opts = Keyword . get ( opts , :tcp_opts , [ ] )
55+
56+ { :ok , % { connection_mod: connection_mod , stack_id: stack_id , tcp_opts: tcp_opts } ,
57+ { :continue , :notify_ready } }
5558 end
5659
5760 @ impl GenServer
@@ -64,7 +67,7 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
6467 @ impl GenServer
6568 def handle_call ( { :validate , connection } , _from , state ) do
6669 # convert to postgrex style for return to conn.manager
67- connection = populate_connection_opts ( connection )
70+ connection = populate_connection_opts ( connection , state . tcp_opts )
6871
6972 result = attempt_connection ( { :cont , connection } , state )
7073
@@ -100,7 +103,7 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
100103
101104 { :error , error } ->
102105 error
103- |> mutate_based_on_error ( conn_opts )
106+ |> mutate_based_on_error ( conn_opts , state . tcp_opts )
104107 |> attempt_connection ( state )
105108 end
106109 end
@@ -109,8 +112,8 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
109112 { :error , error }
110113 end
111114
112- defp populate_connection_opts ( conn_opts ) do
113- conn_opts |> populate_ssl_opts ( ) |> populate_tcp_opts ( )
115+ defp populate_connection_opts ( conn_opts , tcp_opts ) do
116+ conn_opts |> populate_ssl_opts ( ) |> populate_tcp_opts ( tcp_opts )
114117 end
115118
116119 defp populate_ssl_opts ( connection_opts ) do
@@ -172,36 +175,87 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
172175 ]
173176 end
174177
175- defp populate_tcp_opts ( connection_opts ) do
176- tcp_opts =
178+ defp populate_tcp_opts ( connection_opts , tcp_opts ) do
179+ inet_opts =
177180 if connection_opts [ :ipv6 ] do
178181 [ :inet6 ]
179182 else
180183 [ ]
181184 end
182185
183- Keyword . put ( connection_opts , :socket_options , tcp_opts )
186+ Keyword . put (
187+ connection_opts ,
188+ :socket_options ,
189+ inet_opts ++ tcp_liveness_opts ( tcp_opts )
190+ )
184191 end
185192
186- defp mutate_based_on_error ( % Postgrex.Error { message: "ssl not available" } = error , conn_opts ) do
193+ # Options for configuring TCP keepalives and TCP user timeout.
194+ #
195+ # SO_KEEPALIVE makes the kernel probe an idle connection, and TCP_USER_TIMEOUT
196+ # caps how long unacknowledged data may stay outstanding before the connection
197+ # is dropped -- the latter also bounds detection while data *is* being sent,
198+ # which keepalive alone does not.
199+ #
200+ # Everything here is opt-in: with no configuration we emit no options and
201+ # inherit the OS defaults. Platform support is checked upstream, in
202+ # Electric.Application, before these options reach us.
203+ #
204+ # Time values are in milliseconds, as produced by parse_human_readable_time!.
205+ # TCP_KEEPIDLE and TCP_KEEPINTVL are expressed in seconds by the kernel, while
206+ # TCP_USER_TIMEOUT takes milliseconds.
207+ @ doc false
208+ def tcp_liveness_opts ( config ) do
209+ keepalive_idle = Keyword . get ( config , :keepalive_idle )
210+ keepalive_interval = Keyword . get ( config , :keepalive_interval )
211+ keepalive_count = Keyword . get ( config , :keepalive_count )
212+ user_timeout = Keyword . get ( config , :user_timeout )
213+
214+ keepalive_opt =
215+ if is_nil ( keepalive_idle ) and is_nil ( keepalive_interval ) and is_nil ( keepalive_count ) do
216+ [ ]
217+ else
218+ [ { :keepalive , true } ]
219+ end
220+
221+ keepalive_opt ++
222+ tcp_opt ( :keepidle , ms_to_sec ( keepalive_idle ) ) ++
223+ tcp_opt ( :keepintvl , ms_to_sec ( keepalive_interval ) ) ++
224+ tcp_opt ( :keepcnt , keepalive_count ) ++
225+ tcp_opt ( :user_timeout , user_timeout )
226+ end
227+
228+ defp tcp_opt ( _opt , nil ) , do: [ ]
229+ defp tcp_opt ( opt , value ) when is_integer ( value ) , do: [ { opt , value } ]
230+
231+ defp ms_to_sec ( nil ) , do: nil
232+ defp ms_to_sec ( ms ) when is_integer ( ms ) , do: max ( div ( ms , 1000 ) , 1 )
233+
234+ defp mutate_based_on_error (
235+ % Postgrex.Error { message: "ssl not available" } = error ,
236+ conn_opts ,
237+ _tcp_opts
238+ ) do
187239 maybe_fallback_to_no_ssl ( conn_opts , error )
188240 end
189241
190242 defp mutate_based_on_error (
191243 % DBConnection.ConnectionError { message: "ssl connect: closed" } = error ,
192- conn_opts
244+ conn_opts ,
245+ _tcp_opts
193246 ) do
194247 maybe_fallback_to_no_ssl ( conn_opts , error )
195248 end
196249
197250 defp mutate_based_on_error (
198251 % DBConnection.ConnectionError { severity: :error } = error ,
199- conn_opts
252+ conn_opts ,
253+ tcp_opts
200254 ) do
201- maybe_fallback_to_ipv4 ( error , conn_opts )
255+ maybe_fallback_to_ipv4 ( error , conn_opts , tcp_opts )
202256 end
203257
204- defp mutate_based_on_error ( error , _conn_opts ) do
258+ defp mutate_based_on_error ( error , _conn_opts , _tcp_opts ) do
205259 { :halt , error }
206260 end
207261
@@ -225,7 +279,8 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
225279
226280 defp maybe_fallback_to_ipv4 (
227281 % DBConnection.ConnectionError { message: message , severity: :error } = error ,
228- conn_opts
282+ conn_opts ,
283+ tcp_opts
229284 ) do
230285 # If network is unreachable, IPv6 is not enabled on the machine
231286 # If domain cannot be resolved, assume there is no AAAA record for it
@@ -239,7 +294,7 @@ defmodule Electric.Connection.Manager.ConnectionResolver do
239294 "Database connection failed to find valid IPv6 address for #{ conn_opts [ :hostname ] } - falling back to IPv4"
240295 )
241296
242- { :cont , conn_opts |> Keyword . put ( :ipv6 , false ) |> populate_tcp_opts ( ) }
297+ { :cont , conn_opts |> Keyword . put ( :ipv6 , false ) |> populate_tcp_opts ( tcp_opts ) }
243298 else
244299 { :halt , error }
245300 end
0 commit comments