feat(introspection): add Excache.Command.Introspection.Client
This commit is contained in:
1 parent
eb1528087c
commit
c1d29f2ae7
1 file changed
+157
@@ -0,0 +1,157 @@
|
||||
defmodule Excache.Command.Introspection.Client do
|
||||
@moduledoc false
|
||||
|
||||
alias Excache.Clients
|
||||
alias Excache.Command.Reply
|
||||
|
||||
@help [
|
||||
"CLIENT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
|
||||
"ID",
|
||||
" Return the client id of the current connection.",
|
||||
"INFO",
|
||||
" Return information about the current connection.",
|
||||
"GETNAME",
|
||||
" Return the name of the current connection.",
|
||||
"SETNAME <name>",
|
||||
" Assign the name <name> to the current connection.",
|
||||
"LIST [ID <id> [<id> ...]] [TYPE normal|master|replica|pubsub]",
|
||||
" Return one line of information per connection.",
|
||||
"KILL <ip:port>",
|
||||
" Close the connection reachable at <ip:port>.",
|
||||
"KILL <filter> <value> [<filter> <value> ...]",
|
||||
" Close connections matching ID, ADDR, LADDR, TYPE, MAXAGE or SKIPME.",
|
||||
"HELP",
|
||||
" Print this help."
|
||||
]
|
||||
|
||||
@killFilters ~w(ID ADDR LADDR TYPE MAXAGE SKIPME)
|
||||
@clientTypes ~w(normal master replica pubsub)
|
||||
|
||||
def dispatch("ID", [], state),
|
||||
do: withClient(state, fn client -> {:integer, client.id} end)
|
||||
|
||||
def dispatch("INFO", [], state),
|
||||
do: withClient(state, fn client -> {:bulk, line(client, now())} end)
|
||||
|
||||
def dispatch("GETNAME", [], state),
|
||||
do: withClient(state, fn client -> {:bulk, client.name} end)
|
||||
|
||||
def dispatch("SETNAME", [name], state) do
|
||||
if validName?(name) do
|
||||
withClient(state, fn client ->
|
||||
:ok = Clients.setName(client.pid, name)
|
||||
{:simple, "OK"}
|
||||
end)
|
||||
else
|
||||
{[{:error, "ERR Client names cannot contain spaces, newlines or special characters."}],
|
||||
state}
|
||||
end
|
||||
end
|
||||
|
||||
def dispatch("LIST", args, state) do
|
||||
case listFilter(args) do
|
||||
{:ok, matcher} ->
|
||||
moment = now()
|
||||
|
||||
text =
|
||||
Clients.snapshot().clients
|
||||
|> Enum.filter(matcher)
|
||||
|> Enum.map_join("", fn client -> line(client, moment) <> "\n" end)
|
||||
|
||||
{[{:bulk, text}], state}
|
||||
|
||||
:error ->
|
||||
{[Reply.syntax()], state}
|
||||
end
|
||||
end
|
||||
|
||||
def dispatch("KILL", [address], state) do
|
||||
killed = Clients.kill(fn client -> client.addr == address end, self())
|
||||
|
||||
if killed > 0 do
|
||||
{[{:simple, "OK"}], state}
|
||||
else
|
||||
{[{:error, "ERR No such client address in the client list"}], state}
|
||||
end
|
||||
end
|
||||
|
||||
def dispatch("KILL", [_, _ | _] = args, state) do
|
||||
case killFilter(args) do
|
||||
{:ok, matcher} -> {[{:integer, Clients.kill(matcher, self())}], state}
|
||||
:error -> {[Reply.syntax()], state}
|
||||
end
|
||||
end
|
||||
|
||||
def dispatch("HELP", [], state),
|
||||
do: {[{:array, Enum.map(@help, fn text -> {:simple, text} end)}], state}
|
||||
|
||||
def dispatch(subcommand, _args, state) do
|
||||
{[
|
||||
{:error,
|
||||
"ERR Unknown subcommand or wrong number of arguments for '#{subcommand}'. " <>
|
||||
"Try CLIENT HELP."}
|
||||
], state}
|
||||
end
|
||||
|
||||
defp line(client, moment) do
|
||||
Enum.join(
|
||||
[
|
||||
"id=#{client.id}",
|
||||
"addr=#{client.addr}",
|
||||
"laddr=#{client.laddr}",
|
||||
"name=#{client.name}",
|
||||
"age=#{elapsed(moment, client.connectedAt)}",
|
||||
"idle=#{elapsed(moment, client.lastActiveAt)}",
|
||||
"flags=N",
|
||||
"db=0",
|
||||
"sub=#{Clients.subscriptionCount(client.pid)}",
|
||||
"psub=0",
|
||||
"tot-net-in=#{client.netIn}",
|
||||
"tot-net-out=#{client.netOut}"
|
||||
],
|
||||
" "
|
||||
)
|
||||
end
|
||||
|
||||
defp withClient(state, build) do
|
||||
case Clients.find(self()) do
|
||||
nil -> {[{:error, "ERR CLIENT is only available on a server connection"}], state}
|
||||
client -> {[build.(client)], state}
|
||||
end
|
||||
end
|
||||
|
||||
defp listFilter([]), do: {:ok, fn _client -> true end}
|
||||
|
||||
defp listFilter([keyword | [_ | _] = rest]) do
|
||||
case String.upcase(keyword) do
|
||||
"ID" -> idFilter(rest)
|
||||
"TYPE" -> typeFilter(rest)
|
||||
_other -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp listFilter(_args), do: :error
|
||||
|
||||
defp idFilter(raw) do
|
||||
ids = Enum.map(raw, &parseId/1)
|
||||
|
||||
if :error in ids do
|
||||
:error
|
||||
else
|
||||
wanted = MapSet.new(ids)
|
||||
{:ok, fn client -> MapSet.member?(wanted, client.id) end}
|
||||
end
|
||||
end
|
||||
|
||||
defp typeFilter([type]) do
|
||||
case String.downcase(type) do
|
||||
"normal" -> {:ok, fn client -> Clients.subscriptionCount(client.pid) == 0 end}
|
||||
"pubsub" -> {:ok, fn client -> Clients.subscriptionCount(client.pid) > 0 end}
|
||||
"master" -> {:ok, fn _client -> false end}
|
||||
"replica" -> {:ok, fn _client -> false end}
|
||||
_other -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp typeFilter(_raw), do: :error
|
||||
|
||||
Reference in new issue
Block a user