Packages

Cafex is a pure Elixir implementation of Kafka client.

Current section

Files

Jump to
misha_cafex lib cafex connection.ex
Raw

lib/cafex/connection.ex

defmodule Cafex.Connection do
use GenServer
require Logger
alias Cafex.Protocol.Decoder
defmodule State do
@moduledoc false
defstruct client_id: nil,
correlation_id: 0,
host: nil,
port: nil,
send_buffer: nil,
timeout: nil,
socket: nil
end
@default_client_id "cafex"
@default_send_buffer 10_000_000
@default_timeout 5000
@type conn :: pid
@type request :: Cafex.Protocol.Request.t
@type receiver :: {:fsm, pid} | {:server, pid} | pid
@type response :: Decoder.response
# ===================================================================
# API
# ===================================================================
def start_link(host, port, opts \\ []) do
GenServer.start_link __MODULE__, [host, port, opts]
end
def start(host, port, opts \\ []) do
GenServer.start __MODULE__, [host, port, opts]
end
@spec request(conn, request) :: :ok | {:ok, response} | {:error, term}
def request(pid, request) do
GenServer.call pid, {:request, request}
end
@spec async_request(conn, request, receiver) :: :ok
def async_request(pid, request, receiver) do
GenServer.cast pid, {:async_request, request, receiver}
end
def close(pid) do
GenServer.call pid, :close
end
# ===================================================================
# GenServer callbacks
# ===================================================================
def init([host, port, opts]) do
client_id = Keyword.get(opts, :client_id, @default_client_id)
buffer = Keyword.get(opts, :send_buffer, @default_send_buffer)
timeout = Keyword.get(opts, :timeout, @default_timeout)
state = %State{ client_id: client_id,
host: host,
port: port,
send_buffer: buffer,
timeout: timeout } |> maybe_open_socket
{:ok, state}
end
def handle_call({:request, request}, _from, state) do
case do_request(request, state) do
{:ok, state} ->
{:reply, :ok, state}
{{:ok, reply}, state} ->
{:reply, {:ok, reply}, state}
{{:error, reason}, state} ->
{:stop, reason, state}
end
end
def handle_call(:close, _from, state) do
{:stop, :normal, :ok, state}
end
def handle_cast({:async_request, request, receiver}, state) do
case do_request(request, state) do
{:ok, state} ->
{:noreply, state}
{{:ok, reply}, state} ->
send_reply(receiver, {:ok, reply})
{:noreply, state}
{{:error, reason}, state} ->
{:stop, reason, state}
end
end
def handle_info({:tcp_closed, socket}, %{socket: socket} = state) do
Logger.info fn -> "Connection closed by peer" end
{:noreply, %{state | socket: nil}}
end
def terminate(reason, %{host: host, port: port, socket: socket}) do
Logger.debug "Connection closed: #{host}:#{port}, reason: #{inspect reason}"
if socket, do: :gen_tcp.close(socket)
:ok
end
# ===================================================================
# Internal functions
# ===================================================================
defp maybe_open_socket(%{socket: nil, host: host, port: port, send_buffer: buffer} = state) do
case :gen_tcp.connect(:erlang.bitstring_to_list(host), port,
[:binary, {:packet, 4}, {:sndbuf, buffer}]) do
{:ok, socket} ->
%{state | socket: socket}
{:error, reason} ->
throw reason
end
end
defp maybe_open_socket(state), do: state
defp send_sync_request(socket, data, timeout, has_response) do
case :gen_tcp.send(socket, data) do
:ok ->
case has_response do
true -> recv_response(socket, timeout)
false -> :ok
end
{:error, _reason} = error ->
error
end
end
defp recv_response(socket, timeout) do
receive do
{:tcp, ^socket, data} ->
{:ok, data}
{:tcp_closed, ^socket} ->
{:error, :closed}
after
timeout ->
{:error, :timeout}
end
end
defp do_request(request, %{client_id: client_id,
correlation_id: correlation_id,
timeout: timeout} = state) do
data = Cafex.Protocol.encode_request(client_id, correlation_id, request)
has_response = Cafex.Protocol.has_response?(request)
decoder = Cafex.Protocol.Request.decoder(request)
state = %{state | correlation_id: correlation_id + 1} |> maybe_open_socket
case send_sync_request(state.socket, data, timeout, has_response) do
:ok ->
{:ok, state}
{:ok, data} ->
{_, reply} = Cafex.Protocol.Codec.decode_response(decoder, data)
{{:ok, reply}, state}
{:error, reason} ->
Logger.error "Error sending request to broker: #{state.host}:#{state.port}: #{inspect reason}"
{{:error, reason}, state}
end
end
defp send_reply({:fsm, pid}, reply) when is_pid(pid) do
cast_send pid, {:"$gen_event", {:kafka_response, reply}}
end
defp send_reply({:server, pid}, reply) when is_pid(pid) do
cast_send pid, {:"$gen_cast", {:kafka_response, reply}}
end
defp send_reply(pid, reply) when is_pid(pid) do
cast_send pid, {:kafka_response, reply}
end
defp cast_send(dest, msg) do
try do
:erlang.send dest, msg, [:noconnect, :nosuspend]
catch
_, reason -> {:error, reason}
end
end
end