Packages
redix
0.3.0
1.6.0
1.5.3
1.5.2
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.0
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
retired
1.1.0
1.0.0
0.11.2
0.11.1
0.11.0
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.0
Fast, pipelined, resilient Redis driver for Elixir.
Current section
Files
Jump to
Current section
Files
lib/redix/pub_sub/connection.ex
defmodule Redix.PubSub.Connection do
@moduledoc false
use Connection
require Logger
alias Redix.Protocol
alias Redix.ConnectionUtils
@initial_state %{
tail: "",
socket: nil,
recipients: HashDict.new,
monitors: HashDict.new,
reconnection_attempts: 0,
queue: :queue.new,
clients_to_notify_of_reconnection: [],
}
## Callbacks
@doc false
def init(opts) do
{:connect, :init, Dict.merge(@initial_state, opts: opts)}
end
@doc false
def connect(info, state)
def connect(info, state) do
case ConnectionUtils.connect(info, state) do
{:ok, state} ->
if info == :backoff do
Enum.each(state.clients_to_notify_of_reconnection, &send(&1, msg(:reconnected, nil)))
state = %{state | clients_to_notify_of_reconnection: []}
end
{:ok, state}
o ->
o
end
end
@doc false
def disconnect(reason, state)
def disconnect(:stop, state) do
{:stop, :normal, state}
end
def disconnect({:error, reason}, state) do
Logger.error "Disconnected from Redis (#{ConnectionUtils.format_host(state)}): #{:inet.format_error(reason)}"
:gen_tcp.close(state.socket)
state = disconnect_and_notify_clients(state, reason)
ConnectionUtils.backoff_or_stop(%{state | tail: "", socket: nil}, 0, reason)
end
@doc false
def handle_call(operation, from, state)
def handle_call({op, channels, recipient}, _from, state) when op in [:subscribe, :psubscribe] do
subscribe(state, op, channels, recipient)
end
def handle_call({op, channels, recipient}, _from, state) when op in [:unsubscribe, :punsubscribe] do
unsubscribe(state, op, channels, recipient)
end
@doc false
def handle_cast(operation, state)
def handle_cast(:stop, state) do
{:disconnect, :stop, state}
end
@doc false
def handle_info(msg, state)
def handle_info({:tcp, socket, data}, %{socket: socket} = state) do
:ok = :inet.setopts(socket, active: :once)
state = new_data(state, state.tail <> data)
{:noreply, state}
end
def handle_info({:tcp_closed, socket}, %{socket: socket} = state) do
{:disconnect, {:error, :tcp_closed}, state}
end
def handle_info({:tcp_error, socket, reason}, %{socket: socket} = state) do
{:disconnect, {:error, reason}, state}
end
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
recipient_terminated(state, pid)
end
## Helper functions
defp new_data(state, <<>>) do
%{state | tail: <<>>}
end
defp new_data(state, data) do
case Protocol.parse(data) do
{:ok, resp, rest} ->
state |> handle_message(resp) |> new_data(rest)
{:error, :incomplete} ->
%{state | tail: data}
end
end
defp subscribe(state, op, channels, recipient) do
state = monitor_recipient(state, recipient)
{channels_to_subscribe_to, recipients} =
Enum.flat_map_reduce(channels, state.recipients, &subscribe_to_channel(&1, &2, recipient, op))
state = %{state | recipients: recipients}
command = op |> Atom.to_string |> String.upcase
if channels_to_subscribe_to != [] do
state
|> enqueue(Enum.map(channels_to_subscribe_to, &{op, &1, recipient}))
|> ConnectionUtils.send_reply(Protocol.pack([command|channels_to_subscribe_to]), :ok)
else
{:reply, :ok, state}
end
end
defp unsubscribe(state, op, channels, recipient) do
state = demonitor_recipient(state, recipient)
{channels_to_unsubscribe_from, recipients} =
Enum.flat_map_reduce(channels, state.recipients, &unsubscribe_from_channel(&1, &2, recipient, op))
state = %{state | recipients: recipients}
command = op |> Atom.to_string |> String.upcase
if channels_to_unsubscribe_from != [] do
state
|> enqueue(Enum.map(channels_to_unsubscribe_from, &{op, &1, recipient}))
|> ConnectionUtils.send_reply(Protocol.pack([command|channels_to_unsubscribe_from]), :ok)
else
{:reply, :ok, state}
end
end
defp recipient_terminated(state, recipient) do
{channels_to_unsubscribe_from, recipients} = Enum.flat_map_reduce state.recipients, state.recipients, fn {channel, for_channel}, recipients ->
if HashSet.member?(for_channel, recipient) do
for_channel = HashSet.delete(for_channel, recipient)
if HashSet.size(for_channel) == 0 do
{[channel], Dict.delete(recipients, channel)}
else
{[], Dict.put(recipients, channel, for_channel)}
end
else
{[], recipients}
end
end
state = %{state | recipients: recipients}
{channels, patterns} = Enum.partition(channels_to_unsubscribe_from, &match?({:channel, _}, &1))
channels = Enum.map(channels, fn({:channel, channel}) -> channel end)
patterns = Enum.map(patterns, fn({:pattern, pattern}) -> pattern end)
commands = []
if channels != [] do
commands = [["UNSUBSCRIBE"|channels]|commands]
state = enqueue(state, Enum.map(channels, &{:unsubscribe, &1, nil}))
end
if patterns != [] do
commands = [["PUNSUBSCRIBE"|patterns]|commands]
state = enqueue(state, Enum.map(patterns, &{:punsubscribe, &1, nil}))
end
if commands != [] do
ConnectionUtils.send_noreply(state, Enum.map(commands, &Protocol.pack/1))
else
{:noreply, state}
end
end
defp subscribe_to_channel(channel, recipients, recipient, op) do
key = {op_to_type(op), channel}
if recipients[key] do
recipients = Dict.update!(recipients, key, &HashSet.put(&1, recipient))
send(recipient, msg(op, channel))
{[], recipients}
else
{[channel], recipients}
end
end
defp unsubscribe_from_channel(channel, recipients, recipient, op) do
key = {op_to_type(op), channel}
if for_channel = recipients[key] do
new_for_channel = HashSet.delete(for_channel, recipient)
if HashSet.size(new_for_channel) == 0 do
{[channel], recipients}
else
send(recipient, msg(op, channel))
{[], Dict.put(recipients, key, new_for_channel)}
end
else
{[], recipients}
end
end
defp handle_message(state, [op, channel, _count]) when op in ~w(subscribe psubscribe) do
op = String.to_atom(op)
{{:value, {^op, ^channel, recipient}}, new_queue} = :queue.out(state.queue)
send(recipient, msg(op, channel))
state = %{state | queue: new_queue}
update_in state, [:recipients, {op_to_type(op), channel}], fn(set) ->
(set || HashSet.new) |> HashSet.put(recipient)
end
end
defp handle_message(state, [op, channel, _count]) when op in ~w(unsubscribe punsubscribe) do
op = String.to_atom(op)
{{:value, {^op, ^channel, recipient}}, new_queue} = :queue.out(state.queue)
state = %{state | queue: new_queue}
# If `recipient` is nil it means that this unsubscription came from a
# monitored recipient going down: we don't need to send anything to anyone
# and we don't need to remove it from the state (it had already been
# removed).
if is_nil(recipient) do
state
else
send(recipient, msg(op, channel))
update_in state, [:recipients, {op_to_type(op), channel}], &HashSet.delete(&1, recipient)
end
end
defp handle_message(state, ["message", channel, payload]) do
recipients = Dict.fetch!(state.recipients, {:channel, channel})
Enum.each(recipients, fn(rec) -> send(rec, msg(:message, payload, channel)) end)
state
end
defp handle_message(state, ["pmessage", pattern, channel, payload]) do
recipients = Dict.fetch!(state.recipients, {:pattern, pattern})
Enum.each(recipients, fn(rec) -> send(rec, msg(:pmessage, payload, {pattern, channel})) end)
state
end
defp msg(type, payload, metadata \\ nil) do
{:redix_pubsub, type, payload, metadata}
end
defp enqueue(%{queue: queue} = state, elems) when is_list(elems) do
%{state | queue: :queue.join(queue, :queue.from_list(elems))}
end
defp op_to_type(op) when op in [:subscribe, :unsubscribe], do: :channel
defp op_to_type(op) when op in [:psubscribe, :punsubscribe], do: :pattern
defp monitor_recipient(state, recipient) do
update_in state.monitors[recipient], fn
nil ->
Process.monitor(recipient)
ref when is_reference(ref) ->
ref
end
end
defp demonitor_recipient(state, recipient) do
unless Enum.any?(state.recipients, fn({_, recipients}) -> HashSet.member?(recipients, recipient) end) do
state.monitors |> Dict.fetch!(recipient) |> Process.demonitor
end
state
end
defp disconnect_and_notify_clients(state, error_reason) do
# First, demonitor all the monitored clients and reset the state.
{clients, state} = get_and_update_in state.monitors, fn(monitors) ->
monitors |> Dict.values |> Enum.each(&Process.demonitor/1)
{Dict.keys(monitors), HashDict.new}
end
# Then, let's send a message to each of those clients.
for client <- clients do
subscribed_to = client_subscriptions(state, client)
send(client, msg(:disconnected, error_reason, subscribed_to))
end
%{state | clients_to_notify_of_reconnection: clients}
end
defp client_subscriptions(state, client) do
state.recipients
|> Enum.filter(fn({_, recipients}) -> HashSet.member?(recipients, client) end)
|> Enum.map(fn({channel, _recipients}) -> channel end)
end
end