Packages
extreme
0.5.4
1.1.4
1.1.3
1.1.2
1.1.1
1.1.1-rc01
1.1.0
1.1.0-rc9
1.1.0-rc8
1.1.0-rc7
1.1.0-rc6
1.1.0-rc5
1.1.0-rc4
1.1.0-rc3
1.1.0-rc2
1.1.0-rc1
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
Elixir TCP client for EventStore.
Current section
Files
Jump to
Current section
Files
lib/extreme.ex
defmodule Extreme do
use GenServer
alias Extreme.Request
require Logger
alias Extreme.Response
## Client API
@doc """
Starts connection to EventStore using `connection_settings` and optional `opts`.
"""
def start_link(connection_settings, opts \\[]) do
GenServer.start_link(__MODULE__, connection_settings, opts)
end
@doc """
Executes protobuf `message` against `server`. Returns:
- {:ok, protobuf_message} on success .
- {:error, :not_authenticated} on wrong credentials.
- {:error, error_reason, protobuf_message} on failure.
"""
def execute(server, message) do
GenServer.call(server, {:execute, message})
end
@doc """
Reads events specified in `read_events`, sends them to `subscriber`
and leaves `subscriber` subscribed per `subscribe` message.
`subscriber` is process that will keep receiving {:on_event, event} messages.
`read_events` :: Extreme.Messages.ReadStreamEvents
`subscribe` :: Extreme.Messages.SubscribeToStream
Returns {:ok, subscription} when subscription is success.
If `stream` is hard deleted `subscriber` will receive message {:extreme, :error, :stream_hard_deleted, stream}
If `stream` is soft deleted `subscriber` will receive message {:extreme, :warn, :stream_soft_deleted, stream}.
In case of soft deleted stream, new event will recreate stream and it will be sent to `subscriber` as described above
Hard deleted streams can't be recreated so suggestion is not to handle this message but rather crash when it happens
"""
def read_and_stay_subscribed(server, subscriber, stream, from_event_number \\ 0, per_page \\ 4096, resolve_link_tos \\ true, require_master \\ false) do
GenServer.call(server, {:read_and_stay_subscribed, subscriber, {stream, from_event_number, per_page, resolve_link_tos, require_master}})
end
@doc """
Subscribe `subscriber` to `stream` using `server`.
`subscriber` is process that will keep receiving {:on_event, event} messages.
Returns {:ok, subscription} when subscription is success.
NOTE: If `stream` is hard deleted, `subscriber` will NOT receive any message!
"""
def subscribe_to(server, subscriber, stream, resolve_link_tos \\ true) do
GenServer.call(server, {:subscribe_to, subscriber, stream, resolve_link_tos})
end
## Server Callbacks
@subscriptions_sup Extreme.SubscriptionsSupervisor
def init(connection_settings) do
user = Keyword.fetch!(connection_settings, :username)
pass = Keyword.fetch!(connection_settings, :password)
GenServer.cast(self, {:connect, connection_settings, 1})
{:ok, sup} = Extreme.SubscriptionsSupervisor.start_link(self)
{:ok, %{socket: nil, pending_responses: %{}, subscriptions: %{}, subscriptions_sup: sup, credentials: %{user: user, pass: pass}, received_data: <<>>, should_receive: nil}}
end
def handle_cast({:connect, connection_settings, attempt}, state) do
db_type = Keyword.get(connection_settings, :db_type, :node)
|> cast_to_atom
case connect(db_type, connection_settings, attempt) do
{:ok, socket} -> {:noreply, %{state|socket: socket}}
error -> {:stop, error, state}
end
end
defp connect(:cluster, connection_settings, attempt) do
{:ok, host, port} = Extreme.ClusterConnection.get_node(connection_settings)
connect(host, port, connection_settings, attempt)
end
defp connect(:node, connection_settings, attempt) do
host = Keyword.fetch!(connection_settings, :host)
port = Keyword.fetch!(connection_settings, :port)
connect(host, port, connection_settings, attempt)
end
defp connect(:cluster_dns, connection_settings, attempt) do
{:ok, host, port} = Extreme.ClusterConnection.get_node(:cluster_dns, connection_settings)
connect(host, port, connection_settings, attempt)
end
defp connect(host, port, connection_settings, attempt) do
Logger.info "Connecting Extreme to #{host}:#{port}"
opts = [:binary, active: :once]
case :gen_tcp.connect(String.to_char_list(host), port, opts) do
{:ok, socket} ->
Logger.info "Successfuly connected to EventStore @ #{host}:#{port}"
:timer.send_after(1_000, :send_ping)
{:ok, socket}
_ ->
max_attempts = Keyword.get(connection_settings, :max_attempts, :infinity)
reconnect = case max_attempts do
:infinity -> true
max when attempt <= max -> true
_ -> false
end
if reconnect do
reconnect_delay = Keyword.get connection_settings, :reconnect_delay, 1_000
Logger.warn "Error connecting to EventStore @ #{host}:#{port}. Will retry in #{reconnect_delay} ms."
:timer.sleep(reconnect_delay)
db_type = Keyword.get(connection_settings, :db_type, :node)
|> cast_to_atom
connect(db_type, connection_settings, attempt + 1)
else
{:error, :max_attempt_exceeded}
end
end
end
def handle_call({:execute, protobuf_msg}, from, state) do
{message, correlation_id} = Request.prepare(protobuf_msg, state.credentials)
#Logger.debug "Will execute #{inspect protobuf_msg}"
:ok = :gen_tcp.send(state.socket, message)
state = put_in(state.pending_responses, Map.put(state.pending_responses, correlation_id, from))
{:noreply, state}
end
def handle_call({:read_and_stay_subscribed, subscriber, params}, _from, state) do
{:ok, subscription} = Extreme.SubscriptionsSupervisor.start_subscription(state.subscriptions_sup, subscriber, params)
#Logger.debug "Subscription is: #{inspect subscription}"
{:reply, {:ok, subscription}, state}
end
def handle_call({:subscribe_to, subscriber, stream, resolve_link_tos}, _from, state) do
{:ok, subscription} = Extreme.SubscriptionsSupervisor.start_subscription(state.subscriptions_sup, subscriber, stream, resolve_link_tos)
#Logger.debug "Subscription is: #{inspect subscription}"
{:reply, {:ok, subscription}, state}
end
def handle_call({:subscribe, subscriber, msg}, from, state) do
#Logger.debug "Subscribing #{inspect subscriber} with: #{inspect msg}"
{message, correlation_id} = Request.prepare(msg, state.credentials)
:ok = :gen_tcp.send(state.socket, message)
state = put_in(state.pending_responses, Map.put(state.pending_responses, correlation_id, from))
state = put_in(state.subscriptions, Map.put(state.subscriptions, correlation_id, subscriber))
{:noreply, state}
end
def handle_info(:send_ping, state) do
message = Request.prepare(:ping)
:ok = :gen_tcp.send(state.socket, message)
{:noreply, state}
end
def handle_info({:tcp, socket, pkg}, state) do
:inet.setopts(socket, active: :once) # Allow the socket to send us the next message
state = process_package(pkg, state)
{:noreply, state}
end
def handle_info({:tcp_closed, _port}, state), do: {:stop, :tcp_closed, state}
# This package carries message from it's start. Process it and return new `state`
defp process_package(<<message_length :: 32-unsigned-little-integer, content :: binary>>, %{socket: _socket, received_data: <<>>} = state) do
#Logger.debug "Processing package with message_length of: #{message_length}"
slice_content(message_length, content)
|> process_content(state)
end
# Process package for unfinished message. Process it and return new `state`
defp process_package(pkg, %{socket: _socket} = state) do
#Logger.debug "Processing next package. We need #{state.should_receive} bytes and we have collected #{byte_size(state.received_data)} so far and we have #{byte_size(pkg)} more"
slice_content(state.should_receive, state.received_data <> pkg)
|> process_content(state)
end
defp slice_content(message_length, content) do
if byte_size(content) < message_length do
#Logger.debug "We have unfinished message of length #{message_length}(#{byte_size(content)}): #{inspect content}"
{:unfinished_message, message_length, content}
else
case content do
<<message :: binary - size(message_length), next_message :: binary>> -> {message, next_message}
<<message :: binary - size(message_length)>> -> {message, <<>>}
end
end
end
defp process_content({:unfinished_message, expected_message_length, data}, state) do
%{state|should_receive: expected_message_length, received_data: data}
end
defp process_content({message, <<>>}, state) do
#Logger.debug "Processing single message: #{inspect message} and we have already received: #{inspect state.received_data}"
state = process_message(message, state)
#Logger.debug "After processing content state is #{inspect state}"
%{state|should_receive: nil, received_data: <<>>}
end
defp process_content({message, rest}, state) do
#Logger.debug "Processing message: #{inspect message}"
#Logger.debug "But we have something else in package: #{inspect rest}"
state = process_message(message, %{state|should_receive: nil, received_data: <<>>})
process_package(rest, state)
end
defp process_message(message, state) do
#"Let's finally process whole message: #{inspect message}"
Response.parse(message)
|> respond(state)
end
defp respond({:pong, _correlation_id}, state) do
#Logger.debug "#{inspect self} got :pong"
:timer.send_after 1_000, :send_ping
state
end
defp respond({:heartbeat_request, correlation_id}, state) do
#Logger.debug "#{inspect self} Tick-Tack"
message = Request.prepare(:heartbeat_response, correlation_id)
:ok = :gen_tcp.send(state.socket, message)
%{state|pending_responses: state.pending_responses}
end
defp respond({:error, :not_authenticated, correlation_id}, state) do
{:error, :not_authenticated}
|> respond_with(correlation_id, state)
end
defp respond({_auth, correlation_id, response}, state) do
response
|> respond_with(correlation_id, state)
end
defp respond_with(response, correlation_id, state) do
#Logger.debug "Responding with response: #{inspect response}"
case Map.get(state.pending_responses, correlation_id) do
nil ->
respond_to_subscription(response, correlation_id, state.subscriptions)
state
from ->
:ok = GenServer.reply(from, Response.reply(response))
pending_responses = Map.delete(state.pending_responses, correlation_id)
%{state|pending_responses: pending_responses}
end
end
defp respond_to_subscription(response, correlation_id, subscriptions) do
case Map.get(subscriptions, correlation_id) do
nil -> :ok #Logger.error "Can't find correlation_id #{inspect correlation_id} for response #{inspect response}"
subscription -> GenServer.cast(subscription, Response.reply(response))
end
end
@doc """
Cast the provided value to an atom if appropriate.
If the provided value is a string, convert it to an atom.
"""
def cast_to_atom(value) when is_binary(value),
do: String.to_atom(value)
@doc """
Cast the provided value to an atom if appropriate.
If the provided value is not a string, return it as-is.
"""
def cast_to_atom(value),
do: value
end