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

Allow MLLP connections to be proactively monitored with is_closed?/1 #65

Closed
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
24 changes: 24 additions & 0 deletions lib/mllp/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ defmodule MLLP.Client do
@spec is_connected?(pid :: pid()) :: boolean()
def is_connected?(pid), do: GenServer.call(pid, :is_connected)

@doc """
Returns true if the connection is closed, otherwise false.
"""
@spec is_closed?(pid :: pid()) :: boolean()
def is_closed?(pid), do: GenServer.call(pid, :is_closed)

@doc """
Returns true if the client is or will be attempting to reconnect, otherwise false.
"""
@spec is_pending_reconnect?(pid :: pid()) :: boolean()
def is_pending_reconnect?(pid), do: GenServer.call(pid, :is_pending_reconnect)

@doc """
Instructs the client to disconnect (if connected) and attempt a reconnect.
"""
Expand Down Expand Up @@ -352,6 +364,18 @@ defmodule MLLP.Client do
{:reply, (state.socket && !state.pending_reconnect) == true, state}
end

def handle_call(:is_pending_reconnect, _reply, state) do
{:reply, state.pending_reconnect != nil, state}
end

def handle_call(:is_closed, _reply, %State{socket: nil} = state) do
{:reply, true, state}
end

def handle_call(:is_closed, _reply, state) do
{:reply, state.tcp.is_closed?(state.socket), state}
end

def handle_call(:reconnect, _from, state) do
new_state =
state
Expand Down
29 changes: 29 additions & 0 deletions lib/mllp/tcp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule MLLP.TCPContract do
) :: {:ok, :gen_tcp.socket()} | {:error, any}

@callback close(socket :: :gen_tcp.socket()) :: :ok

@callback is_closed?(socket :: :gen_tcp.socket()) :: boolean()
end

defmodule MLLP.TCP do
Expand All @@ -22,4 +24,31 @@ defmodule MLLP.TCP do
defdelegate recv(socket, length, timeout), to: :gen_tcp
defdelegate connect(address, port, options, timeout), to: :gen_tcp
defdelegate close(socket), to: :gen_tcp

require Logger

@doc """
Checks if the socket is closed.
It does so by attempting to read any data from the socket.

From the [`:gen_tcp` docs](https://www.erlang.org/doc/man/gen_tcp.html#recv-3):

> Argument Length is only meaningful when the socket is
in raw mode and denotes the number of bytes to read.
If Length is 0, all available bytes are returned.
If Length > 0, exactly Length bytes are returned,
or an error; possibly discarding less than Length
bytes of data when the socket is closed from the other
side.
"""
@spec is_closed?(socket :: :gen_tcp.socket()) :: boolean()
def is_closed?(socket) do
case recv(socket, _length = 0, _timeout = 1) do
{:ok, _} -> false
{:error, :timeout} -> false
{:error, reason} ->
Logger.warn("Socket appears to be closed. Reason: #{reason}")
true
end
end
end
38 changes: 38 additions & 0 deletions test/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,44 @@ defmodule ClientTest do
end
end

describe "is_closed?/1" do
test "returns true if the connection is closed" do
address = {127, 0, 0, 1}
port = 4090
socket = make_ref()

MLLP.TCPMock
|> expect(
:connect,
fn ^address, ^port, [:binary, {:packet, 0}, {:active, false}], 2000 ->
{:ok, socket}
end
)
|> expect(:is_closed?, fn _socket -> true end)

{:ok, client} = Client.start_link(address, port, tcp: MLLP.TCPMock, use_backoff: true)
assert MLLP.Client.is_closed?(client)
end

test "returns false if the connection is not closed" do
address = {127, 0, 0, 1}
port = 4090
socket = make_ref()

MLLP.TCPMock
|> expect(
:connect,
fn ^address, ^port, [:binary, {:packet, 0}, {:active, false}], 2000 ->
{:ok, socket}
end
)
|> expect(:is_closed?, fn _socket -> false end)

{:ok, client} = Client.start_link(address, port, tcp: MLLP.TCPMock, use_backoff: true)
refute MLLP.Client.is_closed?(client)
end
end

describe "send/2" do
test "with valid HL7 returns an AA" do
address = {127, 0, 0, 1}
Expand Down