Packages
ferricstore
0.4.1
0.11.12
0.11.11
0.11.10
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.0
0.1.0
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/commands/server.ex
# Suppress function clause grouping warnings (clauses added by different agents)
defmodule Ferricstore.Commands.Server do
alias Ferricstore.HLC
alias Ferricstore.Store.Ops
@moduledoc """
Handles Redis server commands: PING, ECHO, DBSIZE, KEYS, FLUSHDB, FLUSHALL,
INFO, COMMAND, SELECT, LOLWUT, and DEBUG.
Each handler takes the uppercased command name, a list of string arguments,
and an injected store map. Returns plain Elixir terms — the connection layer
handles RESP encoding.
## Supported commands
* `PING [message]` — returns `{:simple, "PONG"}` or echoes the message
* `ECHO message` — returns the message as a bulk string
* `DBSIZE` — returns the number of keys in the store
* `KEYS pattern` — returns keys matching a glob pattern (`*`, `?`)
* `FLUSHDB [ASYNC|SYNC]` — deletes all keys
* `FLUSHALL [ASYNC|SYNC]` — alias for FLUSHDB (single-db server)
* `INFO [section]` — returns server information as a bulk string
* `COMMAND` — returns array of command info tuples
* `COMMAND COUNT` — returns number of supported commands
* `COMMAND DOCS name` — returns simplified docs for a command
* `COMMAND INFO name [name ...]` — returns info for specific commands
* `COMMAND LIST` — returns all command names
* `COMMAND GETKEYS command [args...]` — returns which args are keys
* `SELECT db` — always returns error (not supported)
* `LOLWUT [VERSION version]` — returns ASCII art with FerricStore branding
* `DEBUG SLEEP seconds` — sleeps for N seconds (testing only)
"""
alias Ferricstore.AuditLog
alias Ferricstore.Commands.Catalog
alias Ferricstore.Raft.Cluster, as: RaftCluster
alias Ferricstore.Raft.WARaftBackend
alias Ferricstore.Store.Router
alias Ferricstore.Stats
@waraft_table :ferricstore_waraft_backend
@doc """
Handles a server command.
## Parameters
- `cmd` - Uppercased command name (e.g. `"PING"`, `"KEYS"`)
- `args` - List of string arguments
- `store` - Injected store map with `keys`, `dbsize`, `flush` callbacks
## Returns
Plain Elixir term: `{:simple, "PONG"}`, string, integer, list, `:ok`, or `{:error, message}`.
"""
@spec handle(binary(), [binary()], map()) :: term()
def handle(cmd, args, store)
# ---------------------------------------------------------------------------
# PING
# ---------------------------------------------------------------------------
def handle("PING", [], _store), do: {:simple, "PONG"}
def handle("PING", [msg], _store), do: msg
def handle("PING", _args, _store) do
{:error, "ERR wrong number of arguments for 'ping' command"}
end
# ---------------------------------------------------------------------------
# ECHO
# ---------------------------------------------------------------------------
def handle("ECHO", [msg], _store), do: msg
def handle("ECHO", _args, _store) do
{:error, "ERR wrong number of arguments for 'echo' command"}
end
# ---------------------------------------------------------------------------
# DBSIZE
# ---------------------------------------------------------------------------
def handle("DBSIZE", [], store) do
alias Ferricstore.Store.CompoundKey
Ops.keys(store)
|> CompoundKey.user_visible_keys()
|> length()
end
def handle("DBSIZE", _args, _store) do
{:error, "ERR wrong number of arguments for 'dbsize' command"}
end
# ---------------------------------------------------------------------------
# KEYS
# ---------------------------------------------------------------------------
def handle("KEYS", [pattern], store) do
alias Ferricstore.Store.CompoundKey
Ops.keys(store)
|> CompoundKey.user_visible_keys()
|> Enum.filter(&Ferricstore.GlobMatcher.match?(&1, pattern))
end
def handle("KEYS", [], _store) do
{:error, "ERR wrong number of arguments for 'keys' command"}
end
def handle("KEYS", _args, _store) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# FLUSHDB
# ---------------------------------------------------------------------------
def handle("FLUSHDB", args, store) when args in [[], ["ASYNC"], ["SYNC"]] do
AuditLog.log(:dangerous_command, %{command: "FLUSHDB", args: args})
with :ok <- Ops.flush(store) do
Ferricstore.Commands.Stream.clear_local_state()
# Wipe prob files (bloom, CMS, cuckoo, TopK) across all shards.
# store.flush deletes keys via Raft which should clean up files via
# maybe_delete_prob_file, but as a safety net we also wipe the prob
# directories directly.
flush_store_prob_dirs(store)
end
end
def handle("FLUSHDB", _args, _store) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# FLUSHALL — alias for FLUSHDB in our single-database server
# ---------------------------------------------------------------------------
def handle("FLUSHALL", args, store) when args in [[], ["ASYNC"], ["SYNC"]] do
AuditLog.log(:dangerous_command, %{command: "FLUSHALL", args: args})
with :ok <- Ops.flush(store) do
Ferricstore.Commands.Stream.clear_local_state()
flush_store_prob_dirs(store)
end
end
def handle("FLUSHALL", _args, _store) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# SELECT — not supported
# ---------------------------------------------------------------------------
def handle("SELECT", [_db], _store) do
{:error, "ERR SELECT not supported. Use named caches."}
end
def handle("SELECT", _args, _store) do
{:error, "ERR wrong number of arguments for 'select' command"}
end
# ---------------------------------------------------------------------------
# INFO [section]
# ---------------------------------------------------------------------------
def handle("INFO", [], store), do: handle("INFO", ["all"], store)
def handle("INFO", [section], store) do
section_lower = String.downcase(section)
info_string(section_lower, store)
end
def handle("INFO", _args, _store) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# COMMAND (no subcommand) — return all command info tuples
# ---------------------------------------------------------------------------
def handle("COMMAND", [], _store) do
Catalog.all() |> Enum.map(&Catalog.info_tuple/1)
end
# ---------------------------------------------------------------------------
# COMMAND subcommands
# ---------------------------------------------------------------------------
def handle("COMMAND", [subcmd | rest], _store) do
case String.upcase(subcmd) do
"COUNT" when rest == [] ->
Catalog.count()
"LIST" when rest == [] ->
Catalog.names()
"INFO" ->
case rest do
[] ->
{:error, "ERR wrong number of arguments for 'command|info' command"}
names ->
Enum.map(names, fn name ->
case Catalog.lookup(name) do
{:ok, cmd} -> Catalog.info_tuple(cmd)
:error -> nil
end
end)
end
"DOCS" ->
case rest do
[] ->
{:error, "ERR wrong number of arguments for 'command|docs' command"}
names ->
Enum.flat_map(names, fn name ->
case Catalog.lookup(name) do
{:ok, cmd} -> [cmd.name, [cmd.summary]]
:error -> []
end
end)
end
"GETKEYS" ->
case rest do
[] ->
{:error, "ERR wrong number of arguments for 'command|getkeys' command"}
[cmd_name | cmd_args] ->
case Catalog.get_keys(cmd_name, cmd_args) do
{:ok, keys} -> keys
{:error, msg} -> {:error, msg}
end
end
_ ->
{:error, "ERR unknown subcommand '#{subcmd}'. Try COMMAND HELP."}
end
end
# ---------------------------------------------------------------------------
# LOLWUT [VERSION version]
# ---------------------------------------------------------------------------
def handle("LOLWUT", [], _store), do: lolwut_art()
def handle("LOLWUT", [version_opt, _version], _store) do
case String.upcase(version_opt) do
"VERSION" -> lolwut_art()
_ -> {:error, "ERR syntax error"}
end
end
def handle("LOLWUT", _args, _store) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# DEBUG SLEEP seconds
# ---------------------------------------------------------------------------
def handle("DEBUG", [subcmd | rest], store) do
case String.upcase(subcmd) do
"SLEEP" ->
case rest do
[seconds_str] ->
AuditLog.log(:dangerous_command, %{command: "DEBUG", args: ["SLEEP", seconds_str]})
case Integer.parse(seconds_str) do
{secs, ""} when secs >= 0 ->
Process.sleep(secs * 1000)
:ok
_ ->
{:error, "ERR invalid argument for DEBUG SLEEP"}
end
_ ->
{:error, "ERR wrong number of arguments for 'debug' command"}
end
"RELOAD" when rest == [] ->
:ok
"FLUSHALL" ->
AuditLog.log(:dangerous_command, %{command: "DEBUG", args: ["FLUSHALL"]})
handle("FLUSHALL", [], store)
"BATCHER-STATS" when rest == [] ->
ctx = FerricStore.Instance.get(:default)
shard_count = ctx.shard_count
{:simple, debug_batcher_stats(shard_count)}
"SET-ACTIVE-EXPIRE" when length(rest) == 1 ->
:ok
"CHANGE-REPL-ID" when rest == [] ->
:ok
"QUICKLIST-PACKED-THRESHOLD" ->
:ok
"AOFSTAT" when rest == [] ->
%{}
"SFLAGS" when rest == [] ->
%{}
_ ->
{:error, "ERR unknown subcommand '#{subcmd}'. Try DEBUG HELP."}
end
end
def handle("DEBUG", [], _store) do
{:error, "ERR wrong number of arguments for 'debug' command"}
end
# ---------------------------------------------------------------------------
# CONFIG
# ---------------------------------------------------------------------------
def handle("CONFIG", [subcmd | rest], _store) do
handle_config(String.upcase(subcmd), upcase_local_modifier(rest))
end
def handle("CONFIG", [], _store) do
{:error, "ERR wrong number of arguments for 'config' command"}
end
# ---------------------------------------------------------------------------
# MODULE stubs
# ---------------------------------------------------------------------------
def handle("MODULE", [subcmd | _rest], _store) do
case String.upcase(subcmd) do
"LIST" -> []
"LOAD" -> {:error, "ERR FerricStore does not support modules"}
"UNLOAD" -> {:error, "ERR FerricStore does not support modules"}
_ -> {:error, "ERR unknown subcommand for 'module' command"}
end
end
def handle("MODULE", _, _store), do: {:error, "ERR unknown subcommand for 'module' command"}
# ---------------------------------------------------------------------------
# WAITAOF stub
# ---------------------------------------------------------------------------
def handle("WAITAOF", [_, _, _], _store), do: [0, 0]
def handle("WAITAOF", _args, _store),
do: {:error, "ERR wrong number of arguments for 'waitaof' command"}
# ---------------------------------------------------------------------------
# SLOWLOG
# ---------------------------------------------------------------------------
def handle("SLOWLOG", [subcmd | rest], _store) do
case String.upcase(subcmd) do
"GET" ->
case rest do
[] ->
format_slowlog_entries(Ferricstore.SlowLog.get())
[count_str] ->
case Integer.parse(count_str) do
{count, ""} when count >= 0 ->
format_slowlog_entries(Ferricstore.SlowLog.get(count))
_ ->
{:error, "ERR value is not an integer or out of range"}
end
_ ->
{:error, "ERR unknown subcommand or wrong number of arguments for 'slowlog' command"}
end
"LEN" when rest == [] ->
Ferricstore.SlowLog.len()
"RESET" when rest == [] ->
Ferricstore.SlowLog.reset()
:ok
"HELP" when rest == [] ->
[
"SLOWLOG GET [<count>] -- Return top entries from the slowlog.",
"SLOWLOG LEN -- Return the number of entries in the slowlog.",
"SLOWLOG RESET -- Reset the slowlog."
]
_ ->
{:error, "ERR unknown subcommand or wrong number of arguments for 'slowlog' command"}
end
end
def handle("SLOWLOG", [], _store) do
{:error, "ERR unknown subcommand or wrong number of arguments for 'slowlog' command"}
end
# ---------------------------------------------------------------------------
# SAVE / BGSAVE / LASTSAVE
# ---------------------------------------------------------------------------
@last_save_key {__MODULE__, :last_save_unix_seconds}
def handle("SAVE", [], store), do: save_now(store)
def handle("SAVE", _args, _store) do
{:error, "ERR wrong number of arguments for 'save' command"}
end
def handle("BGSAVE", [], store) do
_ = Task.start(fn -> save_now(store) end)
{:simple, "Background saving started"}
end
def handle("BGSAVE", _args, _store) do
{:error, "ERR wrong number of arguments for 'bgsave' command"}
end
def handle("LASTSAVE", [], _store), do: last_save_time()
def handle("LASTSAVE", _args, _store) do
{:error, "ERR wrong number of arguments for 'lastsave' command"}
end
def handle("FERRICSTORE.BLOBGC", [], store) do
with {:ok, ctx} <- server_instance_ctx(store),
{:ok, stats} <- Router.sweep_blob_garbage(ctx) do
blob_gc_result(stats)
else
{:error, :no_default_instance} ->
{:error, "ERR no default instance available for 'ferricstore.blobgc' command"}
{:error, reason} ->
{:error, "ERR blob gc failed: #{inspect(reason)}"}
end
end
def handle("FERRICSTORE.BLOBGC", _args, _store) do
{:error, "ERR wrong number of arguments for 'ferricstore.blobgc' command"}
end
def handle("FERRICSTORE.DOCTOR", args, store) when is_list(args) do
with {:ok, ctx} <- server_instance_ctx(store) do
Ferricstore.Doctor.handle_command(normalize_doctor_args(args), ctx)
else
{:error, :no_default_instance} ->
{:error, "ERR no default instance available for 'ferricstore.doctor' command"}
end
end
# ===========================================================================
# Private helpers
# ===========================================================================
# ---------------------------------------------------------------------------
# DEBUG helpers
# ---------------------------------------------------------------------------
defp debug_batcher_stats(shard_count), do: debug_waraft_stats(shard_count)
defp debug_waraft_stats(shard_count) do
if shard_count <= 0 do
""
else
0..(shard_count - 1)
|> Enum.map(&debug_waraft_shard_stats/1)
|> Enum.join(" | ")
end
end
defp debug_waraft_shard_stats(shard_index) do
partition = shard_index + 1
components = [
{"server", :wa_raft_server.registered_name(@waraft_table, partition)},
{"acceptor", :wa_raft_acceptor.registered_name(@waraft_table, partition)},
{"queue", :wa_raft_queue.registered_name(@waraft_table, partition)},
{"storage", :wa_raft_storage.registered_name(@waraft_table, partition)}
]
body =
components
|> Enum.map(fn {label, name} -> "#{label}=#{component_process_stat(name)}" end)
|> Kernel.++(["inflight_bytes=#{WARaftBackend.inflight_commit_bytes(shard_index)}"])
|> Enum.join(",")
"WA#{shard_index}:#{body}"
end
defp component_process_stat(name) do
case process_stat(name) do
:down -> "down"
{mq, reductions} -> "mq=#{mq},r=#{reductions}"
end
end
defp process_stat(name) do
with pid when is_pid(pid) <- Process.whereis(name),
info when is_list(info) <- Process.info(pid, [:message_queue_len, :reductions]) do
{Keyword.get(info, :message_queue_len, 0), Keyword.get(info, :reductions, 0)}
else
_ -> :down
end
end
# ---------------------------------------------------------------------------
# SAVE helpers
# ---------------------------------------------------------------------------
defp save_now(store) do
case persistence_barrier(store) do
:ok ->
record_last_save()
:ok
{:error, msg} when is_binary(msg) ->
{:error, msg}
{:error, reason} ->
{:error, "ERR save failed: #{inspect(reason)}"}
other ->
{:error, "ERR save failed: #{inspect(other)}"}
end
end
defp persistence_barrier(%{persistence_barrier: barrier}) when is_function(barrier, 0) do
barrier.()
end
defp persistence_barrier(%FerricStore.Instance{} = ctx), do: persistence_barrier_for_ctx(ctx)
defp persistence_barrier(%{__instance_ctx__: %FerricStore.Instance{} = ctx}),
do: persistence_barrier_for_ctx(ctx)
defp persistence_barrier(_store), do: :ok
defp persistence_barrier_for_ctx(ctx) do
with :ok <- flush_raft_batchers(ctx),
:ok <- flush_bitcask_writers(ctx),
:ok <- sync_checkpointers(ctx) do
:ok
end
end
defp flush_raft_batchers(%{name: :default, shard_count: shard_count}) do
Enum.reduce_while(0..(shard_count - 1), :ok, fn i, :ok ->
case Ferricstore.Raft.Batcher.flush(i, 30_000) do
:ok -> {:cont, :ok}
{:error, _} = err -> {:halt, err}
other -> {:halt, {:error, {:batcher_flush_failed, i, other}}}
end
end)
end
defp flush_raft_batchers(_ctx), do: :ok
defp flush_bitcask_writers(%{shard_count: shard_count} = ctx) do
Enum.reduce_while(0..(shard_count - 1), :ok, fn i, :ok ->
case Ferricstore.Store.BitcaskWriter.flush(ctx, i, 30_000) do
:ok -> {:cont, :ok}
{:error, _} = err -> {:halt, err}
other -> {:halt, {:error, {:bitcask_writer_flush_failed, i, other}}}
end
end)
end
defp sync_checkpointers(%{shard_count: shard_count} = ctx) do
Enum.reduce_while(0..(shard_count - 1), :ok, fn i, :ok ->
name = Ferricstore.Store.BitcaskCheckpointer.process_name(i, ctx)
case Process.whereis(name) do
pid when is_pid(pid) ->
case Ferricstore.Store.BitcaskCheckpointer.sync_now(pid) do
:ok -> {:cont, :ok}
{:error, _} = err -> {:halt, err}
other -> {:halt, {:error, {:checkpointer_sync_failed, i, other}}}
end
nil ->
case sync_active_file(ctx, i) do
:ok -> {:cont, :ok}
{:error, _} = err -> {:halt, err}
other -> {:halt, {:error, {:active_file_sync_failed, i, other}}}
end
end
end)
end
defp sync_active_file(ctx, shard_index) do
try do
{_file_id, path, _shard_path} = Ferricstore.Store.ActiveFile.get(ctx, shard_index)
Ferricstore.Bitcask.NIF.v2_fsync(path)
rescue
error -> {:error, {:active_file_sync_exception, shard_index, error}}
catch
kind, reason -> {:error, {:active_file_sync_throw, shard_index, kind, reason}}
end
end
defp record_last_save do
ts = System.os_time(:second)
:persistent_term.put(@last_save_key, ts)
ts
end
defp last_save_time do
:persistent_term.get(@last_save_key, 0)
end
# ---------------------------------------------------------------------------
# FLUSHDB helper
# ---------------------------------------------------------------------------
defp flush_all_prob_dirs do
data_dir = Application.get_env(:ferricstore, :data_dir, "data")
Ferricstore.ProbCleanup.flush_all(data_dir, shard_count())
end
defp flush_store_prob_dirs(%FerricStore.Instance{} = ctx) do
Ferricstore.ProbCleanup.flush_all(ctx.data_dir, ctx.shard_count)
end
defp flush_store_prob_dirs(store) when is_map(store) do
case Map.fetch(store, :flush_prob_dirs) do
{:ok, flush_prob_dirs} when is_function(flush_prob_dirs, 0) -> flush_prob_dirs.()
_ -> flush_all_prob_dirs()
end
end
defp flush_store_prob_dirs(_store), do: flush_all_prob_dirs()
# ---------------------------------------------------------------------------
# INFO section builders
# ---------------------------------------------------------------------------
@all_sections [
"server",
"clients",
"memory",
"keyspace",
"stats",
"persistence",
"replication",
"cpu",
"namespace_config",
"raft",
"bitcask",
"ferricstore",
"keydir_analysis"
]
# Read shard_count from persistent_term (set by application.ex) with
# Application.get_env fallback for early startup / test environments.
defp shard_count do
try do
FerricStore.Instance.get(:default).shard_count
rescue
ArgumentError ->
Application.get_env(:ferricstore, :shard_count, 4)
end
end
defp info_string(section, store) when section in ["all", "everything"] do
Enum.map_join(@all_sections, "\r\n", fn s -> build_section(s, store) end)
end
defp info_string(section, store) when section in @all_sections do
build_section(section, store)
end
defp info_string(_unknown, _store) do
# Redis returns an empty string for unknown sections
""
end
defp build_section("server", _store) do
ctx = default_instance_ctx()
info = if ctx && ctx.server_info_fn, do: ctx.server_info_fn.(), else: %{}
port = Map.get(info, :tcp_port, 0)
redis_mode = Map.get(info, :redis_mode, "embedded")
uptime_seconds = Stats.uptime_seconds()
uptime_days = div(uptime_seconds, 86_400)
{os_family, os_name} = :os.type()
{major, minor, patch} = :os.version()
os_string = "#{os_family}:#{os_name} #{major}.#{minor}.#{patch}"
fields = [
{"redis_version", "7.4.0"},
{"ferricstore_version", "0.4.1"},
{"redis_mode", redis_mode},
{"os", os_string},
{"arch_bits", "64"},
{"tcp_port", Integer.to_string(port)},
{"uptime_in_seconds", Integer.to_string(uptime_seconds)},
{"uptime_in_days", Integer.to_string(uptime_days)},
{"hz", "10"},
{"configured_hz", "10"},
{"process_id", Integer.to_string(System.pid() |> String.to_integer())},
{"run_id", Stats.run_id()},
{"ferricstore_git_sha", "dev"}
]
format_section("Server", fields)
end
defp build_section("clients", _store) do
ctx = default_instance_ctx()
connected = if ctx && ctx.connected_clients_fn, do: ctx.connected_clients_fn.(), else: 0
blocked = safe_ets_size(:ferricstore_waiters)
tracking =
safe_ets_size(:ferricstore_tracking_connections)
fields = [
{"connected_clients", Integer.to_string(connected)},
{"blocked_clients", Integer.to_string(blocked)},
{"tracking_clients", Integer.to_string(tracking)},
{"maxclients", "10000"}
]
format_section("Clients", fields)
end
defp build_section("memory", _store) do
total = :erlang.memory(:total)
process_mem = :erlang.memory(:processes)
shard_count = shard_count()
# Sum ETS memory across keydir tables per shard.
keydir_bytes =
Enum.reduce(0..(shard_count - 1), 0, fn i, acc ->
try do
case :ets.info(:"keydir_#{i}", :memory) do
words when is_integer(words) ->
acc + words * :erlang.system_info(:wordsize)
_ ->
acc
end
rescue
ArgumentError -> acc
end
end)
# RSS approximation: best we can do on BEAM is :erlang.memory(:total)
used_memory_rss = total
# Peak: we do not track a high-water mark yet, so report current.
used_memory_peak = total
# Fragmentation ratio (rss / used). With BEAM they are the same, so ~1.0.
frag_ratio =
if total > 0,
do: Float.round(used_memory_rss / total, 2),
else: 1.0
fields = [
{"used_memory", Integer.to_string(total)},
{"used_memory_human", format_bytes(total)},
{"used_memory_rss", Integer.to_string(used_memory_rss)},
{"used_memory_peak", Integer.to_string(used_memory_peak)},
{"mem_fragmentation_ratio", format_float_field(frag_ratio)},
{"keydir_used_bytes", Integer.to_string(keydir_bytes)},
{"hot_cache_used_bytes", Integer.to_string(keydir_bytes)},
{"beam_process_memory", Integer.to_string(process_mem)}
]
format_section("Memory", fields)
end
defp build_section("keyspace", store) do
key_count = Ops.dbsize(store)
ctx =
try do
FerricStore.Instance.get(:default)
rescue
_ -> nil
end
{expires, avg_ttl} = if ctx, do: compute_expiry_stats(ctx), else: {0, 0}
fields = [
{"db0", "keys=#{key_count},expires=#{expires},avg_ttl=#{avg_ttl}"}
]
format_section("Keyspace", fields)
end
defp build_section("stats", _store) do
rate = read_sample_rate()
hot_sampled = Stats.total_hot_reads()
cold_sampled = Stats.total_cold_reads()
hits_sampled = Stats.keyspace_hits()
misses_sampled = Stats.keyspace_misses()
# Estimated actuals: sampled counters × sample rate
hot_est = hot_sampled * rate
cold_est = cold_sampled * rate
hits_est = hits_sampled * rate
misses_est = misses_sampled * rate
total_reads = hits_est + misses_est
hit_ratio = if total_reads > 0, do: Float.round(hits_est / total_reads * 100, 2), else: 0.0
hot_pct =
if hot_est + cold_est > 0,
do: Float.round(hot_est / (hot_est + cold_est) * 100, 2),
else: 0.0
fields = [
{"total_connections_received", Integer.to_string(Stats.total_connections())},
{"total_commands_processed", Integer.to_string(Stats.total_commands())},
{"keyspace_hits", Integer.to_string(hits_est)},
{"keyspace_misses", Integer.to_string(misses_est)},
{"keyspace_hit_ratio", format_float_field(hit_ratio)},
{"hot_reads", Integer.to_string(hot_est)},
{"cold_reads", Integer.to_string(cold_est)},
{"hot_cache_hit_ratio", format_float_field(hot_pct)},
{"read_sample_rate", "1:#{rate}"},
{"expired_keys", Integer.to_string(Stats.expired_keys())},
{"evicted_keys", Integer.to_string(Stats.evicted_keys())}
]
format_section("Stats", fields)
end
defp build_section("persistence", _store) do
fields = [
{"loading", "0"},
{"rdb_changes_since_last_save", "0"},
{"rdb_last_save_time", Integer.to_string(last_save_time())}
]
format_section("Persistence", fields)
end
defp build_section("replication", _store) do
fields = [
{"role", "master"},
{"connected_slaves", "0"}
]
format_section("Replication", fields)
end
defp build_section("cpu", _store) do
# Stub: BEAM does not expose per-process CPU counters cheaply.
fields = [
{"used_cpu_sys", "0.000000"},
{"used_cpu_user", "0.000000"}
]
format_section("CPU", fields)
end
defp build_section("namespace_config", _store) do
alias Ferricstore.NamespaceConfig
entries = NamespaceConfig.get_all()
count = length(entries)
all_default = if count == 0, do: "1", else: "0"
default_fields = [
{"namespace_config_count", Integer.to_string(count)},
{"namespace_config_all_default", all_default},
{"default_window_ms", Integer.to_string(NamespaceConfig.default_window_ms())}
]
entry_fields =
Enum.flat_map(entries, fn entry ->
%{prefix: prefix, window_ms: w} = entry
changed_at = Map.get(entry, :changed_at, 0)
changed_by = Map.get(entry, :changed_by, "")
[
{"ns_#{prefix}_window_ms", Integer.to_string(w)},
{"ns_#{prefix}_changed_at", Integer.to_string(changed_at)},
{"ns_#{prefix}_changed_by", changed_by}
]
end)
format_section("Namespace_Config", default_fields ++ entry_fields)
end
# ---------------------------------------------------------------------------
# INFO raft -- per-shard Raft state
# ---------------------------------------------------------------------------
defp build_section("raft", _store) do
shard_count = shard_count()
fields =
Enum.flat_map(0..(shard_count - 1), fn i ->
try do
case RaftCluster.members(i, 1_000) do
{:ok, _members, leader} ->
local_id = local_raft_member_id(i)
{commit_index, last_applied, current_term} = raft_section_counters(i, local_id)
role =
if leader == local_id do
"leader"
else
"follower"
end
leader_node_str =
case leader do
{_name, node_name} -> Atom.to_string(node_name)
_ -> "unknown"
end
[
{"shard_#{i}_role", role},
{"shard_#{i}_current_term", Integer.to_string(current_term)},
{"shard_#{i}_commit_index", Integer.to_string(commit_index)},
{"shard_#{i}_last_applied", Integer.to_string(last_applied)},
{"shard_#{i}_leader_node", leader_node_str}
] ++ waraft_info_fields(i)
_ ->
[
{"shard_#{i}_role", "unknown"},
{"shard_#{i}_current_term", "0"},
{"shard_#{i}_commit_index", "0"},
{"shard_#{i}_last_applied", "0"},
{"shard_#{i}_leader_node", "unknown"}
] ++ waraft_info_fields(i)
end
rescue
_ ->
[
{"shard_#{i}_role", "unknown"},
{"shard_#{i}_current_term", "0"},
{"shard_#{i}_commit_index", "0"},
{"shard_#{i}_last_applied", "0"},
{"shard_#{i}_leader_node", "unknown"}
] ++ waraft_info_fields(i)
catch
_, _ ->
[
{"shard_#{i}_role", "unknown"},
{"shard_#{i}_current_term", "0"},
{"shard_#{i}_commit_index", "0"},
{"shard_#{i}_last_applied", "0"},
{"shard_#{i}_leader_node", "unknown"}
] ++ waraft_info_fields(i)
end
end)
format_section("Raft", fields)
end
# ---------------------------------------------------------------------------
# INFO bitcask -- per-shard storage stats
# ---------------------------------------------------------------------------
defp build_section("bitcask", _store) do
shard_count = shard_count()
data_dir = Application.get_env(:ferricstore, :data_dir, "data")
instance_ctx = default_instance_ctx()
fields =
Enum.flat_map(0..(shard_count - 1), fn i ->
shard_dir = Ferricstore.DataDir.shard_data_path(data_dir, i)
{data_files, hint_files, total_bytes} =
try do
case Ferricstore.FS.ls(shard_dir) do
{:ok, files} ->
data = Enum.filter(files, &String.ends_with?(&1, ".log"))
hints = Enum.filter(files, &String.ends_with?(&1, ".hint"))
total =
Enum.reduce(files, 0, fn f, acc ->
path = Path.join(shard_dir, f)
case File.stat(path) do
{:ok, %{size: size}} ->
acc + size
{:error, reason} ->
emit_info_bitcask_scan_failed(:stat_shard_file, i, path, reason)
acc
end
end)
{length(data), length(hints), total}
{:error, reason} ->
emit_info_bitcask_scan_failed(:list_shard_dir, i, shard_dir, reason)
{0, 0, 0}
end
rescue
kind ->
emit_info_bitcask_scan_failed(:scan_shard_dir, i, shard_dir, kind)
{0, 0, 0}
catch
kind, reason ->
emit_info_bitcask_scan_failed(:scan_shard_dir, i, shard_dir, {kind, reason})
{0, 0, 0}
end
merge_candidates = max(0, data_files - 1)
last_applied = atomic_metric(instance_ctx, :last_applied_index, i)
last_released = atomic_metric(instance_ctx, :last_released_cursor_index, i)
replay_safe = atomic_metric(instance_ctx, :replay_safe_index, i)
replay_safe_requested = atomic_metric(instance_ctx, :replay_safe_requested_index, i)
replay_safe_lag = max(replay_safe_requested - replay_safe, 0)
replay_safe_persist_failures =
atomic_metric(instance_ctx, :replay_safe_persist_failures, i)
flow_lmdb_replay_safe = atomic_metric(instance_ctx, :flow_lmdb_replay_safe_index, i)
flow_lmdb_replay_safe_requested =
atomic_metric(instance_ctx, :flow_lmdb_replay_safe_requested_index, i)
flow_lmdb_replay_safe_lag =
max(flow_lmdb_replay_safe_requested - flow_lmdb_replay_safe, 0)
flow_lmdb_replay_safe_persist_failures =
atomic_metric(instance_ctx, :flow_lmdb_replay_safe_persist_failures, i)
flow_lmdb_mirror_enqueue_failures =
atomic_metric(instance_ctx, :flow_lmdb_mirror_enqueue_failures, i)
flow_lmdb_mirror_degraded =
atomic_metric(instance_ctx, :flow_lmdb_mirror_degraded, i)
flow_lmdb_writer_pending_ops =
atomic_metric(instance_ctx, :flow_lmdb_writer_pending_ops, i)
flow_lmdb_writer_oldest_pending_age_us =
atomic_metric(instance_ctx, :flow_lmdb_writer_oldest_pending_age_us, i)
flow_lmdb_writer_flush_failures =
atomic_metric(instance_ctx, :flow_lmdb_writer_flush_failures, i)
flow_history_projected = atomic_metric(instance_ctx, :flow_history_projected_index, i)
flow_history_requested = atomic_metric(instance_ctx, :flow_history_requested_index, i)
flow_history_lag = max(flow_history_requested - flow_history_projected, 0)
flow_history_projector_pending_entries =
atomic_metric(instance_ctx, :flow_history_projector_pending_entries, i)
flow_history_projector_oldest_pending_age_us =
atomic_metric(instance_ctx, :flow_history_projector_oldest_pending_age_us, i)
flow_history_projector_flush_failures =
atomic_metric(instance_ctx, :flow_history_projector_flush_failures, i)
flow_history_projector_queue_full =
atomic_metric(instance_ctx, :flow_history_projector_queue_full, i)
release_gap = max(last_applied - last_released, 0)
release_cursor_blocked_apply_count =
atomic_metric(instance_ctx, :release_cursor_blocked_apply_count, i)
checkpoint_dirty = atomic_metric(instance_ctx, :checkpoint_flags, i)
checkpoint_in_flight = atomic_metric(instance_ctx, :checkpoint_in_flight, i)
[
{"shard_#{i}_data_file_count", Integer.to_string(data_files)},
{"shard_#{i}_hint_file_count", Integer.to_string(hint_files)},
{"shard_#{i}_total_size_bytes", Integer.to_string(total_bytes)},
{"shard_#{i}_merge_candidates", Integer.to_string(merge_candidates)},
{"shard_#{i}_last_applied_index", Integer.to_string(last_applied)},
{"shard_#{i}_last_released_cursor_index", Integer.to_string(last_released)},
{"shard_#{i}_replay_safe_index", Integer.to_string(replay_safe)},
{"shard_#{i}_replay_safe_requested_index", Integer.to_string(replay_safe_requested)},
{"shard_#{i}_replay_safe_lag", Integer.to_string(replay_safe_lag)},
{"shard_#{i}_replay_safe_persist_failures",
Integer.to_string(replay_safe_persist_failures)},
{"shard_#{i}_flow_lmdb_replay_safe_index", Integer.to_string(flow_lmdb_replay_safe)},
{"shard_#{i}_flow_lmdb_replay_safe_requested_index",
Integer.to_string(flow_lmdb_replay_safe_requested)},
{"shard_#{i}_flow_lmdb_replay_safe_lag", Integer.to_string(flow_lmdb_replay_safe_lag)},
{"shard_#{i}_flow_lmdb_replay_safe_persist_failures",
Integer.to_string(flow_lmdb_replay_safe_persist_failures)},
{"shard_#{i}_flow_lmdb_mirror_enqueue_failures",
Integer.to_string(flow_lmdb_mirror_enqueue_failures)},
{"shard_#{i}_flow_lmdb_mirror_degraded", Integer.to_string(flow_lmdb_mirror_degraded)},
{"shard_#{i}_flow_lmdb_writer_pending_ops",
Integer.to_string(flow_lmdb_writer_pending_ops)},
{"shard_#{i}_flow_lmdb_writer_oldest_pending_age_us",
Integer.to_string(flow_lmdb_writer_oldest_pending_age_us)},
{"shard_#{i}_flow_lmdb_writer_flush_failures",
Integer.to_string(flow_lmdb_writer_flush_failures)},
{"shard_#{i}_flow_history_projected_index", Integer.to_string(flow_history_projected)},
{"shard_#{i}_flow_history_requested_index", Integer.to_string(flow_history_requested)},
{"shard_#{i}_flow_history_projection_lag", Integer.to_string(flow_history_lag)},
{"shard_#{i}_flow_history_projector_pending_entries",
Integer.to_string(flow_history_projector_pending_entries)},
{"shard_#{i}_flow_history_projector_oldest_pending_age_us",
Integer.to_string(flow_history_projector_oldest_pending_age_us)},
{"shard_#{i}_flow_history_projector_flush_failures",
Integer.to_string(flow_history_projector_flush_failures)},
{"shard_#{i}_flow_history_projector_queue_full",
Integer.to_string(flow_history_projector_queue_full)},
{"shard_#{i}_release_cursor_gap", Integer.to_string(release_gap)},
{"shard_#{i}_release_cursor_blocked_apply_count",
Integer.to_string(release_cursor_blocked_apply_count)},
{"shard_#{i}_checkpoint_dirty", Integer.to_string(checkpoint_dirty)},
{"shard_#{i}_checkpoint_in_flight", Integer.to_string(checkpoint_in_flight)}
]
end)
format_section("Bitcask", fields)
end
# ---------------------------------------------------------------------------
# INFO ferricstore -- aggregate native metrics
# ---------------------------------------------------------------------------
defp build_section("ferricstore", _store) do
shard_count = shard_count()
raft_committed =
Enum.reduce(0..(shard_count - 1), 0, fn i, acc ->
{_commit_index, last_applied, _term} = raft_section_counters(i, local_raft_member_id(i))
acc + last_applied
end)
hot_cache_evictions =
try do
:persistent_term.get({Ferricstore.Stats, :hot_cache_evictions}, 0)
rescue
_ -> 0
catch
_, _ -> 0
end
keydir_full_rejections =
try do
:persistent_term.get({Ferricstore.Stats, :keydir_full_rejections}, 0)
rescue
_ -> 0
catch
_, _ -> 0
end
fields = [
{"raft_commands_committed", Integer.to_string(raft_committed)},
{"hot_cache_evictions", Integer.to_string(hot_cache_evictions)},
{"keydir_full_rejections", Integer.to_string(keydir_full_rejections)}
]
format_section("Ferricstore", fields)
end
# ---------------------------------------------------------------------------
# INFO keydir_analysis -- per-prefix keydir breakdown
# ---------------------------------------------------------------------------
defp build_section("keydir_analysis", _store) do
shard_count = shard_count()
# Collect all keys from all keydir ETS tables and group by prefix
prefix_data =
Enum.reduce(0..(shard_count - 1), %{}, fn i, acc ->
table = :"keydir_#{i}"
try do
:ets.foldl(
fn {key, _value, _exp, _lfu, _fid, _off, _vsize}, inner_acc ->
prefix = Ferricstore.Stats.extract_prefix(key)
# Estimate per-key bytes: key binary + value + expire_at + LFU + ETS tuple overhead
key_bytes = byte_size(key) + 8 + 8 + 64
current = Map.get(inner_acc, prefix, {0, 0})
{count, bytes} = current
Map.put(inner_acc, prefix, {count + 1, bytes + key_bytes})
end,
acc,
table
)
rescue
_ -> acc
catch
_, _ -> acc
end
end)
distinct_prefixes = map_size(prefix_data)
prefix_fields =
prefix_data
|> Enum.sort_by(fn {_prefix, {count, _bytes}} -> count end, :desc)
|> Enum.flat_map(fn {prefix, {count, bytes}} ->
[
{"prefix_#{prefix}_key_count", Integer.to_string(count)},
{"prefix_#{prefix}_keydir_bytes", Integer.to_string(bytes)}
]
end)
fields = [{"distinct_prefixes", Integer.to_string(distinct_prefixes)} | prefix_fields]
format_section("Keydir_Analysis", fields)
end
defp waraft_info_fields(shard_index) do
segment_log = WARaftBackend.segment_log_memory_status(shard_index)
[
{"shard_#{shard_index}_waraft_inflight_commit_bytes",
Integer.to_string(WARaftBackend.inflight_commit_bytes(shard_index))},
{"shard_#{shard_index}_waraft_segment_log_ets_entries",
waraft_info_value(segment_log[:ets_entries])},
{"shard_#{shard_index}_waraft_segment_log_ets_bytes",
waraft_info_value(segment_log[:ets_bytes])},
{"shard_#{shard_index}_waraft_segment_log_disk_first_index",
waraft_info_value(segment_log[:disk_first_index])},
{"shard_#{shard_index}_waraft_segment_log_disk_last_index",
waraft_info_value(segment_log[:disk_last_index])},
{"shard_#{shard_index}_waraft_segment_log_max_ets_entries",
waraft_info_value(segment_log[:max_ets_entries])},
{"shard_#{shard_index}_waraft_segment_log_max_ets_bytes",
waraft_info_value(segment_log[:max_ets_bytes])},
{"shard_#{shard_index}_waraft_segment_log_min_ets_entries",
waraft_info_value(segment_log[:min_ets_entries])}
]
end
defp waraft_info_value(:infinity), do: "infinity"
defp waraft_info_value(:undefined), do: "0"
defp waraft_info_value(nil), do: "0"
defp waraft_info_value(value) when is_integer(value), do: Integer.to_string(value)
defp waraft_info_value(value) when is_binary(value), do: value
defp waraft_info_value(value), do: inspect(value)
defp local_raft_member_id(shard_index) do
RaftCluster.shard_server_id(shard_index)
end
defp raft_section_counters(shard_index, local_id) do
_ = local_id
waraft_section_counters(shard_index)
end
defp waraft_section_counters(shard_index) do
{last_applied, term_from_position} =
case WARaftBackend.storage_position(shard_index) do
{:ok, {:raft_log_pos, index, term}} when is_integer(index) and is_integer(term) ->
{index, term}
_other ->
{0, 0}
end
{last_applied, last_applied, term_from_position}
end
defp emit_info_bitcask_scan_failed(phase, shard_index, path, reason) do
:telemetry.execute(
[:ferricstore, :commands, :info, :bitcask_scan_failed],
%{count: 1},
%{
phase: phase,
shard_index: shard_index,
path: path,
reason: reason
}
)
rescue
_ -> :ok
end
defp default_instance_ctx do
FerricStore.Instance.get(:default)
rescue
_ -> nil
catch
_, _ -> nil
end
defp atomic_metric(%FerricStore.Instance{} = ctx, field, shard_index) do
case Map.get(ctx, field) do
ref when is_reference(ref) ->
size = :atomics.info(ref).size
if shard_index < size, do: :atomics.get(ref, shard_index + 1), else: 0
_ ->
0
end
rescue
_ -> 0
end
defp atomic_metric(_ctx, _field, _shard_index), do: 0
# ---------------------------------------------------------------------------
# Keyspace expiry stats helper
# ---------------------------------------------------------------------------
# Compute expires count and avg_ttl from ETS keydirs.
# Uses :ets.select_count for expires (O(n) at C level, no term creation)
# and samples up to 20 keys per shard for avg_ttl.
defp compute_expiry_stats(ctx) do
now = HLC.now_ms()
count_spec = [{{:_, :_, :"$1", :_, :_, :_, :_}, [{:>, :"$1", 0}], [true]}]
sample_spec = [{{:_, :_, :"$1", :_, :_, :_, :_}, [{:>, :"$1", 0}], [:"$1"]}]
{total_expires, ttl_samples} =
for i <- 0..(ctx.shard_count - 1), reduce: {0, []} do
{exp_acc, ttl_acc} ->
keydir = elem(ctx.keydir_refs, i)
try do
count = :ets.select_count(keydir, count_spec)
samples =
case :ets.select(keydir, sample_spec, 20) do
{results, _cont} -> results
:"$end_of_table" -> []
end
{exp_acc + count, samples ++ ttl_acc}
rescue
ArgumentError -> {exp_acc, ttl_acc}
end
end
avg_ttl =
case ttl_samples do
[] ->
0
_ ->
remaining = Enum.map(ttl_samples, fn exp -> max(0, exp - now) end)
div(Enum.sum(remaining), length(remaining))
end
{total_expires, avg_ttl}
end
# ---------------------------------------------------------------------------
# INFO formatting helpers
# ---------------------------------------------------------------------------
defp format_section(header, fields) do
lines = Enum.map(fields, fn {k, v} -> [k, ":", v] end)
["# ", header, "\r\n", Enum.intersperse(lines, "\r\n"), "\r\n"]
|> IO.iodata_to_binary()
end
defp safe_ets_size(table) do
case :ets.info(table, :size) do
:undefined -> 0
n -> n
end
rescue
ArgumentError -> 0
end
defp format_float_field(val) do
:erlang.float_to_binary(val, [{:decimals, 2}])
end
defp read_sample_rate do
FerricStore.Instance.get(:default).read_sample_rate
rescue
ArgumentError -> 100
end
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes}B"
defp format_bytes(bytes) when bytes < 1024 * 1024 do
kb = Float.round(bytes / 1024, 2)
"#{kb}K"
end
defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024 do
mb = Float.round(bytes / (1024 * 1024), 2)
"#{mb}M"
end
defp format_bytes(bytes) do
gb = Float.round(bytes / (1024 * 1024 * 1024), 2)
"#{gb}G"
end
# ---------------------------------------------------------------------------
# CONFIG helpers
# ---------------------------------------------------------------------------
defp handle_config("GET", ["LOCAL" | rest]) do
handle_config_get_local(rest)
end
defp handle_config("GET", [pattern]) do
Ferricstore.Config.get(pattern)
|> Enum.flat_map(fn {k, v} -> [k, v] end)
end
defp handle_config("GET", _args),
do: {:error, "ERR wrong number of arguments for 'config|get' command"}
defp handle_config("SET", ["LOCAL" | rest]) do
handle_config_set_local(rest)
end
defp handle_config("SET", [key, value]) do
old_value = Ferricstore.Config.get_value(key)
case Ferricstore.Config.set(key, value) do
:ok ->
AuditLog.log(:config_change, %{
parameter: key,
old_value: Ferricstore.Config.redact_for_metadata(key, old_value || ""),
new_value: Ferricstore.Config.redact_for_metadata(key, value)
})
:ok
{:error, _reason} = err ->
err
end
end
defp handle_config("SET", _args),
do: {:error, "ERR wrong number of arguments for 'config|set' command"}
defp handle_config("RESETSTAT", []) do
Stats.reset()
Ferricstore.SlowLog.reset()
:ok
end
defp handle_config("RESETSTAT", _),
do: {:error, "ERR wrong number of arguments for 'config|resetstat' command"}
defp handle_config("REWRITE", []) do
Ferricstore.Config.rewrite()
end
defp handle_config("REWRITE", _),
do: {:error, "ERR wrong number of arguments for 'config|rewrite' command"}
defp handle_config(subcmd, _) do
{:error, "ERR unknown subcommand '#{String.downcase(subcmd)}' for 'config' command"}
end
defp lolwut_art do
art = """
_____ _ ____ _
| ___|__ _ __ _ __(_) ___/ ___|| |_ ___ _ __ ___
| |_ / _ \\ '__| '__| |/ __\\___ \\| __/ _ \\| '__/ _ \\
| _| __/ | | | | | (__ ___) | || (_) | | | __/
|_| \\___|_| |_| |_|\\___|____/ \\__\\___/|_| \\___|
v0.1.0
"""
String.trim_trailing(art)
end
# -- CONFIG SET LOCAL key value -------------------------------------------------
defp handle_config_set_local([key, value]) do
Ferricstore.Config.Local.set(String.downcase(key), value)
end
defp handle_config_set_local(_args) do
{:error, "ERR wrong number of arguments for 'config|set|local' command"}
end
# -- CONFIG GET LOCAL key ------------------------------------------------------
defp handle_config_get_local([key]) do
case Ferricstore.Config.Local.get(String.downcase(key)) do
{:ok, value} -> [String.downcase(key), value]
{:error, _} = err -> err
end
end
defp handle_config_get_local(_args) do
{:error, "ERR wrong number of arguments for 'config|get|local' command"}
end
# When the first arg to CONFIG GET/SET is "local" (any case), upcase it so
# the pattern match `["LOCAL" | rest]` works regardless of client casing.
defp upcase_local_modifier(["local" | rest]), do: ["LOCAL" | rest]
defp upcase_local_modifier(["Local" | rest]), do: ["LOCAL" | rest]
defp upcase_local_modifier([first | rest]) when is_binary(first) do
if String.upcase(first) == "LOCAL" do
["LOCAL" | rest]
else
[first | rest]
end
end
defp upcase_local_modifier(args), do: args
defp normalize_doctor_args([]), do: []
defp normalize_doctor_args([command | rest]) when is_binary(command) do
case String.upcase(command) do
"CHECK" -> ["CHECK" | normalize_doctor_scope_args(rest)]
"START" -> ["START" | normalize_doctor_start_args(rest)]
"STATUS" -> ["STATUS" | rest]
"LIST" -> ["LIST" | rest]
"CANCEL" -> ["CANCEL" | rest]
other -> [other | rest]
end
end
defp normalize_doctor_args(args), do: args
defp normalize_doctor_start_args([kind, subject | rest])
when is_binary(kind) and is_binary(subject) do
[String.upcase(kind), String.upcase(subject) | normalize_doctor_scope_args(rest)]
end
defp normalize_doctor_start_args([kind | rest]) when is_binary(kind),
do: [String.upcase(kind) | rest]
defp normalize_doctor_start_args(args), do: args
defp normalize_doctor_scope_args([scope_kw, scope | rest])
when is_binary(scope_kw) and is_binary(scope) do
case String.upcase(scope_kw) do
"SCOPE" -> ["SCOPE", scope | rest]
"SCOPES" -> ["SCOPES", scope | rest]
_other -> [scope_kw, scope | rest]
end
end
defp normalize_doctor_scope_args(args), do: args
defp server_instance_ctx(%{instance_ctx: %FerricStore.Instance{} = ctx}), do: {:ok, ctx}
defp server_instance_ctx(_store) do
{:ok, FerricStore.Instance.get(:default)}
rescue
ArgumentError -> {:error, :no_default_instance}
end
defp blob_gc_result(stats) do
[
"deleted_files",
Map.get(stats, :deleted_files, 0),
"deleted_bytes",
Map.get(stats, :deleted_bytes, 0),
"kept_files",
Map.get(stats, :kept_files, 0),
"deleted_tmp_files",
Map.get(stats, :deleted_tmp_files, 0),
"deleted_tmp_bytes",
Map.get(stats, :deleted_tmp_bytes, 0),
"hardened_protections_seen",
Map.get(stats, :hardened_protections_seen, 0),
"hardened_protections_released",
Map.get(stats, :hardened_protections_released, 0),
"hardened_protections_blocked",
Map.get(stats, :hardened_protections_blocked, 0)
]
end
# ---------------------------------------------------------------------------
# SLOWLOG formatting
# ---------------------------------------------------------------------------
defp format_slowlog_entries(entries) do
Enum.map(entries, fn {id, timestamp_us, duration_us, command} ->
[id, timestamp_us, duration_us, command]
end)
end
end