feat: extend clients with refresh and accountClosed

This commit is contained in:
dualfroz committed 2026-09-01 16:21:39 +02:00
1 parent 8f1cb45a0b
commit bf66ea177c
2 files changed
+154 -12

No files matched your search

+75
View File
@@ -162,3 +162,78 @@ defmodule Excache.Clients do
end end
end end
defp refresh(client) do
case netStats(client.socket) do
{netIn, netOut} when netIn > client.netIn ->
%{
client
| netIn: netIn,
netOut: netOut,
lastActiveAt: System.system_time(:millisecond)
}
{netIn, netOut} ->
%{client | netIn: netIn, netOut: netOut}
:closed ->
client
end
end
defp accountClosed(state, nil), do: state
defp accountClosed(state, client) do
%{
state
| closedNetIn: state.closedNetIn + client.netIn,
closedNetOut: state.closedNetOut + client.netOut
}
end
defp buildSnapshot(state) do
clients = state.clients |> Map.values() |> Enum.sort_by(& &1.id)
%{
clients: clients,
connectionsReceived: state.connectionsReceived,
netIn: Enum.reduce(clients, state.closedNetIn, fn client, sum -> sum + client.netIn end),
netOut: Enum.reduce(clients, state.closedNetOut, fn client, sum -> sum + client.netOut end),
runId: state.runId,
startedAt: state.startedAt
}
end
defp terminate(pid), do: DynamicSupervisor.terminate_child(@supervisor, pid)
defp socketOf(pid) do
case Process.info(pid, :links) do
{:links, links} -> Enum.find(links, &tcpPort?/1)
nil -> nil
end
end
defp tcpPort?(link) when is_port(link) do
match?({:name, ~c"tcp_inet"}, Port.info(link, :name))
end
defp tcpPort?(_link), do: false
defp netStats(socket) do
case :inet.getstat(socket, [:recv_oct, :send_oct]) do
{:ok, [recv_oct: netIn, send_oct: netOut]} -> {netIn, netOut}
{:error, _reason} -> :closed
end
end
defp addressOf(socket, fetch) do
case fetch.(socket) do
{:ok, {ip, port}} -> formatAddress(ip, port)
{:error, _reason} -> ""
end
end
defp formatAddress(ip, port) when tuple_size(ip) == 8, do: "[#{:inet.ntoa(ip)}]:#{port}"
defp formatAddress(ip, port), do: "#{:inet.ntoa(ip)}:#{port}"
defp newRunId, do: 20 |> :rand.bytes() |> Base.encode16(case: :lower)
end
+79 -12
View File
@@ -1,8 +1,9 @@
defmodule Excache.Command do defmodule Excache.Command do
@moduledoc false @moduledoc false
alias Excache.{PubSub, Store, Persistence, Aof, Notify} alias Excache.{Blocking, PubSub, Store, Persistence, Aof, Notify}
alias Excache.Command.{Reply, Transaction} alias Excache.Command.{Reply, Transaction}
alias Excache.Eviction.Runner
import Reply, import Reply,
only: [ only: [
@@ -45,10 +46,25 @@ defmodule Excache.Command do
ZADD ZREM ZINCRBY ZPOPMIN ZPOPMAX ZADD ZREM ZINCRBY ZPOPMIN ZPOPMAX
) )
@oomDenied ~w(
SET SETNX SETRANGE APPEND GETSET MSET INCR INCRBY DECR DECRBY INCRBYFLOAT
LPUSH RPUSH LINSERT LSET RPOPLPUSH BLMOVE BRPOPLPUSH
HSET HSETNX HINCRBY
SADD SMOVE SINTERSTORE SUNIONSTORE SDIFFSTORE
ZADD ZINCRBY
XADD XSETID XGROUP XREADGROUP XCLAIM XAUTOCLAIM
SETBIT BITOP BITFIELD PFADD PFMERGE
COPY RENAME RENAMENX
)
@oom {:error, "OOM command not allowed when used memory > 'maxmemory'"}
def known_commands, do: @known ++ Excache.Command.Extended.command_names() def known_commands, do: @known ++ Excache.Command.Extended.command_names()
@extended_write Excache.Command.Extended.write_command_names() @extended_write Excache.Command.Extended.write_command_names()
@blockingNames Excache.Command.Blocking.commandNames()
def execute([], state), do: {[], state} def execute([], state), do: {[], state}
def execute([name | args], state) do def execute([name | args], state) do
@@ -61,27 +77,76 @@ defmodule Excache.Command do
end end
def dispatch_and_log(command, args, state) do def dispatch_and_log(command, args, state) do
{replies, updated} = dispatch(command, args, state) if oomDenied?(command) and Runner.admit() == :oom do
{[@oom], state}
if writeCommand?(command) and not Enum.any?(replies, &match?({:error, _}, &1)) do else
Aof.append(command, aofArgs(command, args, replies)) logged(command, args, state)
Notify.for_command(command, args)
end end
end
defp logged(command, args, state) do
{replies, updated} = dispatch(command, args, state)
succeeded = not Enum.any?(replies, &match?({:error, _}, &1))
if succeeded and oomDenied?(command), do: Runner.account(args)
entries = if succeeded, do: aofEntries(command, args, replies), else: []
logWrites(entries)
logWrites(servedWrites(command, wakeKeys(entries)))
{replies, updated} {replies, updated}
end end
defp logWrites(entries) do
Enum.each(entries, fn {name, args} ->
if writeCommand?(name) do
Aof.append(name, args)
Notify.for_command(name, args)
end
end)
end
defp servedWrites(command, keys) do
if command in @blockingNames, do: Blocking.takeWrites(), else: Blocking.signal(keys)
end
defp wakeKeys(entries), do: Enum.flat_map(entries, &listTargets/1)
defp listTargets({"LPUSH", [key | _values]}), do: [key]
defp listTargets({"RPUSH", [key | _values]}), do: [key]
defp listTargets({"LINSERT", [key | _rest]}), do: [key]
defp listTargets({"RPOPLPUSH", [_source, destination]}), do: [destination]
defp listTargets({"RENAME", [_source, destination]}), do: [destination]
defp listTargets({"RENAMENX", [_source, destination]}), do: [destination]
defp listTargets({"COPY", [_source, destination | _options]}), do: [destination]
defp listTargets(_entry), do: []
defp writeCommand?(command), do: command in @write_commands or command in @extended_write defp writeCommand?(command), do: command in @write_commands or command in @extended_write
defp aofArgs("XADD", args, [{:bulk, id}]) when is_binary(id), defp oomDenied?(command), do: command in @oomDenied
do: Excache.Command.Streams.rewriteAddId(args, id)
defp aofArgs(_command, args, _replies), do: args defp aofEntries("XADD", args, [{:bulk, id}]) when is_binary(id),
do: [{"XADD", Excache.Command.Streams.rewriteAddId(args, id)}]
defp aofEntries("SPOP", [key | _rest] = args, replies) do
case poppedMembers(replies) do
[] -> [{"SPOP", args}]
members -> [{"SREM", [key | members]}]
end
end
defp aofEntries(command, args, _replies), do: [{command, args}]
defp poppedMembers([{:bulk, member}]) when is_binary(member), do: [member]
defp poppedMembers([{:array, entries}]),
do: for({:bulk, member} <- entries, is_binary(member), do: member)
defp poppedMembers(_replies), do: []
defp dispatch("PING", [], state), do: {[{:simple, "PONG"}], state} defp dispatch("PING", [], state), do: {[{:simple, "PONG"}], state}
defp dispatch("PING", [message], state), do: {[{:bulk, message}], state} defp dispatch("PING", [message], state), do: {[{:bulk, message}], state}
defp dispatch("ECHO", [message], state), do: {[{:bulk, message}], state} defp dispatch("ECHO", [message], state), do: {[{:bulk, message}], state}
defp dispatch("COMMAND", _args, state), do: {[{:array, []}], state}
defp dispatch("SELECT", [_db], state), do: {[{:simple, "OK"}], state} defp dispatch("SELECT", [_db], state), do: {[{:simple, "OK"}], state}
defp dispatch("QUIT", [], state), do: {[{:simple, "OK"}], state} defp dispatch("QUIT", [], state), do: {[{:simple, "OK"}], state}
defp dispatch("DBSIZE", [], state), do: {[{:integer, Store.dbsize()}], state} defp dispatch("DBSIZE", [], state), do: {[{:integer, Store.dbsize()}], state}
@@ -287,13 +352,15 @@ defmodule Excache.Command do
end end
end end
defp dispatch("ZPOPMIN", [key], state), do: reply(zrange_reply(Store.zpopmin(key, 1), true), state) defp dispatch("ZPOPMIN", [key], state),
do: reply(zrange_reply(Store.zpopmin(key, 1), true), state)
defp dispatch("ZPOPMIN", [key, count], state) do defp dispatch("ZPOPMIN", [key, count], state) do
with_int(count, state, fn n -> zrange_reply(Store.zpopmin(key, n), true) end) with_int(count, state, fn n -> zrange_reply(Store.zpopmin(key, n), true) end)
end end
defp dispatch("ZPOPMAX", [key], state), do: reply(zrange_reply(Store.zpopmax(key, 1), true), state) defp dispatch("ZPOPMAX", [key], state),
do: reply(zrange_reply(Store.zpopmax(key, 1), true), state)
defp dispatch("ZPOPMAX", [key, count], state) do defp dispatch("ZPOPMAX", [key, count], state) do
with_int(count, state, fn n -> zrange_reply(Store.zpopmax(key, n), true) end) with_int(count, state, fn n -> zrange_reply(Store.zpopmax(key, n), true) end)