Skip to content

Instantly share code, notes, and snippets.

@joakimk
Last active October 23, 2015 19:05
Show Gist options
  • Save joakimk/09ffdf6f47ca63412dc5 to your computer and use it in GitHub Desktop.
Save joakimk/09ffdf6f47ca63412dc5 to your computer and use it in GitHub Desktop.
Phoenix channel status using an elixir agent. Will only work on a single server.
defmodule BroadcastChannel do
use Phoenix.Channel
def join(channel, auth_message, socket) do
socket = assign(socket, :client_version, auth_message["client_version"])
ClientStats.join(channel, socket.assigns[:client_version])
broadcast_stats
{:ok, socket}
end
def terminate(_reason, socket) do
channel = socket.topic
ClientStats.leave(channel, socket.assigns[:client_version])
broadcast_stats
:ok
end
defp broadcast_stats do
stats = ClientStats.current
Endpoint.broadcast("statistics", "all", stats)
end
end
defmodule ClientStats do
@most_popular_channels_limit 10
def start_link do
Agent.start_link(fn -> default_stats end, name: __MODULE__)
end
def join(channel_name, version) do
update_count channel_name, 1
update_version_count version, 1
end
def leave(channel_name, version) do
update_count channel_name, -1
update_version_count version, -1
end
def clear do
update fn (stats) -> default_stats end
end
def current do
Agent.get(__MODULE__, fn (stats) ->
%{
connected_clients: stats.connected_clients,
max_connected_clients: stats.max_connected_clients,
client_versions: client_versions_as_array(stats),
most_popular_channels: most_popular_channels(stats)
}
end)
end
defp update_count(channel_name, count_difference) do
update fn (stats) ->
active_channels =
stats.active_channels
|> Map.put(channel_name, current_active_channels_count(stats, channel_name) + count_difference)
|> reject_empty
|> Enum.into(%{})
stats = stats
|> Map.put(:active_channels, active_channels)
|> Map.put(:connected_clients, stats.connected_clients + count_difference)
stats
|> Map.put(:max_connected_clients, max_connected_clients(stats))
end
end
defp update_version_count(version, count_difference) do
update fn (stats) ->
current_count = stats.client_versions[version] || 0
client_versions = stats.client_versions
|> Map.put(version, current_count + count_difference)
|> reject_empty
|> Enum.into(%{})
stats = stats
|> Map.put(:client_versions, client_versions)
stats
end
end
defp max_connected_clients(stats) do
if stats.connected_clients > stats.max_connected_clients do
stats.connected_clients
else
stats.max_connected_clients
end
end
defp client_versions_as_array(stats) do
Enum.map(stats.client_versions, fn ({version, connected_clients}) ->
%{
name: version,
connected_clients: connected_clients,
}
end)
|> Enum.sort_by(&(&1.connected_clients))
|> Enum.reverse
end
defp most_popular_channels(stats) do
Enum.map(stats.active_channels, fn ({name, count}) ->
%{
name: name,
connected_clients: count,
}
end)
|> Enum.sort_by(&(&1.connected_clients))
|> Enum.reverse
|> limit_channels
end
defp reject_empty(channels) do
channels
|> Enum.drop_while(fn ({name, count}) -> count == 0 end)
end
defp current_active_channels_count(stats, channel_name) do
stats.active_channels[channel_name] || 0
end
defp limit_channels(channels) do
{ first_group, _rest } = Enum.split(channels, @most_popular_channels_limit)
first_group
end
defp default_stats do
%{
connected_clients: 0,
max_connected_clients: 0,
client_versions: %{},
active_channels: %{}
}
end
defp update(callback) do
Agent.update(__MODULE__, callback)
end
end
defmodule ClientStatsTest do
use ExUnit.Case
setup do
ClientStats.clear
end
test "stores stats" do
ClientStats.join("test2", "1.0")
ClientStats.join("test1", "1.1")
ClientStats.join("test2", "1.1")
stats = ClientStats.current
assert stats == %{
connected_clients: 3,
max_connected_clients: 3,
client_versions: [
%{ name: "1.1", connected_clients: 2 },
%{ name: "1.0", connected_clients: 1 },
],
most_popular_channels: [
%{ name: "test2", connected_clients: 2 },
%{ name: "test1", connected_clients: 1 },
]
}
end
test "removes stats when a client leaves" do
ClientStats.join("test", "1.0")
ClientStats.join("test", "1.0")
ClientStats.leave("test", "1.0")
stats = ClientStats.current
assert stats == %{
connected_clients: 1,
max_connected_clients: 2,
client_versions: [
%{ name: "1.0", connected_clients: 1 },
],
most_popular_channels: [
%{ name: "test", connected_clients: 1 },
]
}
ClientStats.leave("test", "1.0")
stats = ClientStats.current
assert stats == %{
connected_clients: 0,
max_connected_clients: 2,
client_versions: [],
most_popular_channels: []
}
end
test "limits most_popular_channels to 15 entries" do
Enum.each 1..20, fn (number) ->
ClientStats.join("test#{number}", "1.0")
end
stats = ClientStats.current
assert stats.connected_clients == 20
assert Enum.count(stats.most_popular_channels) == 10
end
end
@henrik
Copy link

henrik commented Oct 23, 2015

Kind of like how this would read:

socket = socket |> assign(:client_version, auth_message["client_version"])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment