Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconnect on send error option. #86

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.5
0.9.6
28 changes: 27 additions & 1 deletion lib/mllp/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule MLLP.ClientContract do
socket_opts: [:gen_tcp.option()],
telemetry_module: nil,
close_on_recv_error: boolean(),
reconnect_on_send_error: boolean(),
tls: [:ssl.tls_client_option()]
]

Expand Down Expand Up @@ -169,6 +170,7 @@ defmodule MLLP.Client do
tls_opts: [],
socket_opts: [],
close_on_recv_error: true,
reconnect_on_send_error: true,
backoff: nil,
caller: nil,
receive_buffer: [],
Expand Down Expand Up @@ -255,6 +257,12 @@ defmodule MLLP.Client do
client and a server. This functions similarly to the `:send_timeout` option provided by
[`:gen_tcp`](`:gen_tcp`). Defaults to `true`.

* `:reconnect_on_send_error` - A boolean value which dictates whether the client socket will be
closed when an error is returned by the transport when attempting to send data. This generally
useful in most MLLP.Client scenarios as the the MLLP protocol provides no recourse mechanism when
a connection error occurs and it is non-trival to distiguish from application level and socket level
errors. Setting this to `true` is usually the safest behaviour, as such it defaults to `true`.

* `:tls` - A list of tls options as supported by [`:ssl`](`:ssl`). When using TLS it is highly recommended you
set `:verify` to `:verify_peer`, select a CA trust store using the `:cacertfile` or `:cacerts` options.
Additionally, further hardening can be achieved through other ssl options such as enabling
Expand Down Expand Up @@ -482,7 +490,25 @@ defmodule MLLP.Client do
)

error_reply = {:error, new_error(:sending, reason)}
{:keep_state_and_data, [{:reply, from, error_reply}]}

case data.reconnect_on_send_error do
true ->
Logger.error(
"Failure to send payload using existing socket with reason #{inspect(reason)}, reconnecting..."
)

data = stop_connection(data, reason)

actions = [
{:reply, from, error_reply},
{:next_event, :internal, :connect}
]

next_state(:disconnected, handle_closed(data), actions)

false ->
{:keep_state_and_data, [{:reply, from, error_reply}]}
end
end
end

Expand Down
75 changes: 75 additions & 0 deletions test/client_and_receiver_integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,60 @@ defmodule ClientAndReceiverIntegrationTest do
HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new()
)
end

@tag :tls
@tag port: 8158
test "reconnect when ssl socket is closed", ctx do
{:ok, client_pid} =
MLLP.Client.start_link("localhost", ctx.port, tls: ctx.client_tls_options)

fake_pid1 = spawn(fn -> :ok end)
fake_pid2 = spawn(fn -> :ok end)

dead_socket =
{:sslsocket, {:gen_tcp, hd(:erlang.ports()), :tls_connectioned, :undefined},
[fake_pid1, fake_pid2]}

:sys.replace_state(client_pid, fn {:connected, state} ->
{:connected, Map.put(state, :socket, dead_socket)}
end)

assert MLLP.Client.is_connected?(client_pid)

assert {:error,
%MLLP.Client.Error{
context: :sending,
reason: :closed,
message: "connection closed"
}} ==
MLLP.Client.send(
client_pid,
HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new()
)

wait_until(
fn ->
case :sys.get_state(client_pid) do
{:connected, %{socket: socket}} ->
socket != dead_socket

_ ->
false
end
end,
1_000
)

assert MLLP.Client.is_connected?(client_pid)
{:connected, %{socket: socket}} = :sys.get_state(client_pid)
assert {:ok, _} = :ssl.getstat(socket)

assert ctx.ack ==
MLLP.Client.send(
client_pid,
HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new()
)
end
end

describe "tls handshake failure logging" do
Expand Down Expand Up @@ -681,4 +735,25 @@ defmodule ClientAndReceiverIntegrationTest do

%{receiver: receiver, port: port}
end

defp wait_until(fun, timeout) when is_integer(timeout) do
timer = Process.send_after(self(), :timeout, timeout)
wait_until(fun, timer)
end

defp wait_until(fun, timer) when is_reference(timer) do
receive do
:timeout ->
flunk("Timed out")
after
50 ->
case fun.() do
true ->
Process.cancel_timer(timer)

false ->
wait_until(fun, timer)
end
end
end
end
9 changes: 9 additions & 0 deletions test/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,15 @@ defmodule ClientTest do
MLLP.TCPMock
|> expect(
:connect,
2,
fn ^address, ^port, ^socket_opts, 2000 ->
{:ok, socket}
end
)
|> expect(:send, fn ^socket, ^packet -> {:error, :closed} end)
|> expect(:close, fn _socket ->
:ok
end)

{:ok, client} = Client.start_link(address, port, tcp: MLLP.TCPMock)

Expand Down Expand Up @@ -503,11 +507,16 @@ defmodule ClientTest do
MLLP.TCPMock
|> expect(
:connect,
2,
fn ^address, ^port, ^socket_opts, 2000 ->
{:ok, socket}
end
)
|> expect(:send, fn ^socket, ^packet -> {:error, :closed} end)
|> expect(:close, fn _socket ->
# Assert we received the default options
:ok
end)

{:ok, client} = Client.start_link(address, port, tcp: MLLP.TCPMock)

Expand Down
Loading