feat(command): extend blocking with Excache.Command.Blocking

This commit is contained in:
dualfroz committed 2026-09-04 17:08:12 +02:00
1 parent 903a5b695e
commit 6de76acbff
2 files changed
+131 -2

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
defmodule Excache.Command.Blocking do
@moduledoc false
alias Excache.Command.Reply
@names ~w(BLPOP BRPOP BLMOVE BRPOPLPUSH BLMPOP)
def commandNames, do: @names
def dispatch("BLPOP", args, state), do: popCommand(args, :left, state)
def dispatch("BRPOP", args, state), do: popCommand(args, :right, state)
def dispatch("BLMOVE", [source, destination, fromRaw, toRaw, timeoutRaw], state) do
with {:ok, fromSide} <- parseSide(fromRaw),
{:ok, toSide} <- parseSide(toRaw) do
withTimeout(timeoutRaw, state, fn timeout ->
submit([source], {:move, fromSide, toSide, destination}, timeout, state)
end)
else
:error -> {[Reply.syntax()], state}
end
end
def dispatch("BRPOPLPUSH", [source, destination, timeoutRaw], state) do
withTimeout(timeoutRaw, state, fn timeout ->
submit([source], {:move, :right, :left, destination}, timeout, state)
end)
end
def dispatch("BLMPOP", [timeoutRaw, numkeysRaw | rest], state) when rest != [] do
withTimeout(timeoutRaw, state, fn timeout ->
case parseMpop(numkeysRaw, rest) do
{:ok, keys, side, count} -> submit(keys, {:mpop, side, count}, timeout, state)
{:reply, term} -> {[term], state}
end
end)
end
def dispatch(_name, _args, _state), do: :unhandled
defp popCommand(args, side, state) when length(args) >= 2 do
{keys, [timeoutRaw]} = Enum.split(args, length(args) - 1)
withTimeout(timeoutRaw, state, fn timeout ->
submit(keys, {:pop, side}, timeout, state)
end)
end
defp popCommand(_args, _side, _state), do: :unhandled
defp submit(keys, op, timeout, state) do
request = %{keys: keys, op: op, timeout: timeout}
if state.in_multi do
{[Excache.Blocking.attempt(request)], state}
else
case Excache.Blocking.acquire(request) do
{:served, term} -> {[term], state}
{:blocked, ref} -> {[{:blocked, ref}], state}
end
end
end
defp withTimeout(raw, state, fun) do
case parseTimeout(raw) do
{:ok, timeout} -> fun.(timeout)
{:reply, term} -> {[term], state}
end
end
defp parseTimeout(raw) do
case Reply.parse_score(raw) do
{:ok, seconds} when seconds < 0 -> {:reply, {:error, "ERR timeout is negative"}}
{:ok, seconds} -> {:ok, trunc(seconds * 1000)}
:error -> {:reply, {:error, "ERR timeout is not a float or out of range"}}
end
end
defp parseSide(raw) do
case String.upcase(raw) do
"LEFT" -> {:ok, :left}
"RIGHT" -> {:ok, :right}
_other -> :error
end
end
defp parseMpop(numkeysRaw, rest) do
case Integer.parse(numkeysRaw) do
{numkeys, ""} when numkeys > 0 -> splitMpop(numkeys, rest)
_invalid -> {:reply, {:error, "ERR numkeys should be greater than 0"}}
end
end
defp splitMpop(numkeys, rest) when length(rest) > numkeys do
{keys, [directionRaw | tail]} = Enum.split(rest, numkeys)
case parseSide(directionRaw) do
{:ok, side} -> mpopCount(keys, side, tail)
:error -> {:reply, Reply.syntax()}
end
end
defp splitMpop(_numkeys, _rest), do: {:reply, Reply.syntax()}
defp mpopCount(keys, side, []), do: {:ok, keys, side, 1}
defp mpopCount(keys, side, [countToken, countRaw]) do
if String.upcase(countToken) == "COUNT" do
case Integer.parse(countRaw) do
{count, ""} when count > 0 -> {:ok, keys, side, count}
_invalid -> {:reply, {:error, "ERR count should be greater than 0"}}
end
else
{:reply, Reply.syntax()}
end
end
defp mpopCount(_keys, _side, _tail), do: {:reply, Reply.syntax()}
end
+12 -2
View File
@@ -1,7 +1,7 @@
defmodule Excache.Command.Extended do defmodule Excache.Command.Extended do
@moduledoc false @moduledoc false
alias Excache.Command.{Streams, Bitmaps, HyperLogLog, Keyspace} alias Excache.Command.{Streams, Bitmaps, HyperLogLog, Keyspace, Server, Blocking, Introspection}
@stream ~w(XADD XLEN XRANGE XREVRANGE XDEL XTRIM XSETID XREAD XINFO XGROUP @stream ~w(XADD XLEN XRANGE XREVRANGE XDEL XTRIM XSETID XREAD XINFO XGROUP
XREADGROUP XACK XPENDING XCLAIM XAUTOCLAIM) XREADGROUP XACK XPENDING XCLAIM XAUTOCLAIM)
@@ -17,17 +17,27 @@ defmodule Excache.Command.Extended do
ZCOUNT ZREVRANGE ZREVRANK ZMSCORE ZCOUNT ZREVRANGE ZREVRANK ZMSCORE
RENAME RENAMENX COPY TOUCH UNLINK EXPIREAT EXPIRETIME PEXPIRETIME) RENAME RENAMENX COPY TOUCH UNLINK EXPIREAT EXPIRETIME PEXPIRETIME)
@server ~w(MEMORY CONFIG)
@blocking Excache.Command.Blocking.commandNames()
@introspection Excache.Command.Introspection.commandNames()
def dispatch(name, args, state) do def dispatch(name, args, state) do
cond do cond do
name in @stream -> Streams.dispatch(name, args, state) name in @stream -> Streams.dispatch(name, args, state)
name in @bitmap -> Bitmaps.dispatch(name, args, state) name in @bitmap -> Bitmaps.dispatch(name, args, state)
name in @hll -> HyperLogLog.dispatch(name, args, state) name in @hll -> HyperLogLog.dispatch(name, args, state)
name in @keyspace -> Keyspace.dispatch(name, args, state) name in @keyspace -> Keyspace.dispatch(name, args, state)
name in @server -> Server.dispatch(name, args, state)
name in @blocking -> Blocking.dispatch(name, args, state)
name in @introspection -> Introspection.dispatch(name, args, state)
true -> :unhandled true -> :unhandled
end end
end end
def command_names, do: @stream ++ @bitmap ++ @hll ++ @keyspace def command_names,
do: @stream ++ @bitmap ++ @hll ++ @keyspace ++ @server ++ @blocking ++ @introspection
def write_command_names do def write_command_names do
~w(XADD XDEL XTRIM XSETID XGROUP XREADGROUP XACK XCLAIM XAUTOCLAIM ~w(XADD XDEL XTRIM XSETID XGROUP XREADGROUP XACK XCLAIM XAUTOCLAIM