258 lines
7.3 KiB
Elixir
258 lines
7.3 KiB
Elixir
defmodule Odinsea.Database.Redis do
|
|
@moduledoc """
|
|
Redis client for Odinsea.
|
|
Provides key-value storage, pub/sub, and caching functionality.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
# ==================================================================================================
|
|
# Connection
|
|
# ==================================================================================================
|
|
|
|
defp conn do
|
|
# Get Redis connection from application environment
|
|
# In production, this would be a persistent connection pool
|
|
host = Application.get_env(:odinsea, :redis_host, "localhost")
|
|
port = Application.get_env(:odinsea, :redis_port, 6379)
|
|
database = Application.get_env(:odinsea, :redis_database, 0)
|
|
password = Application.get_env(:odinsea, :redis_password, nil)
|
|
|
|
opts = [host: host, port: port, database: database]
|
|
opts = if password, do: Keyword.put(opts, :password, password), else: opts
|
|
|
|
case Redix.start_link(opts) do
|
|
{:ok, conn} -> conn
|
|
{:error, _} -> nil
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Key-Value Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Gets a value by key.
|
|
"""
|
|
@spec get(String.t()) :: {:ok, String.t() | nil} | {:error, term()}
|
|
def get(key) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, value} <- Redix.command(conn, ["GET", key]) do
|
|
{:ok, value}
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Sets a key to a value.
|
|
"""
|
|
@spec set(String.t(), String.t()) :: :ok | {:error, term()}
|
|
def set(key, value) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, _} <- Redix.command(conn, ["SET", key, value]) do
|
|
:ok
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Sets a key to a value with expiration (in seconds).
|
|
"""
|
|
@spec setex(String.t(), integer(), String.t()) :: :ok | {:error, term()}
|
|
def setex(key, seconds, value) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, _} <- Redix.command(conn, ["SETEX", key, seconds, value]) do
|
|
:ok
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a key.
|
|
"""
|
|
@spec del(String.t()) :: :ok | {:error, term()}
|
|
def del(key) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, _} <- Redix.command(conn, ["DEL", key]) do
|
|
:ok
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Checks if a key exists.
|
|
"""
|
|
@spec exists?(String.t()) :: boolean()
|
|
def exists?(key) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, count} <- Redix.command(conn, ["EXISTS", key]) do
|
|
count > 0
|
|
else
|
|
_ -> false
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Sets expiration on a key (in seconds).
|
|
"""
|
|
@spec expire(String.t(), integer()) :: :ok | {:error, term()}
|
|
def expire(key, seconds) do
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, _} <- Redix.command(conn, ["EXPIRE", key, seconds]) do
|
|
:ok
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Pub/Sub Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Publishes a message to a channel.
|
|
The message is automatically JSON-encoded.
|
|
"""
|
|
@spec publish(String.t(), map()) :: :ok | {:error, term()}
|
|
def publish(channel, message) do
|
|
json_message = Jason.encode!(message)
|
|
|
|
with conn when not is_nil(conn) <- conn(),
|
|
{:ok, _} <- Redix.command(conn, ["PUBLISH", channel, json_message]) do
|
|
:ok
|
|
else
|
|
nil -> {:error, :no_connection}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
after
|
|
close_conn()
|
|
end
|
|
|
|
@doc """
|
|
Subscribes to channels and handles messages with a callback function.
|
|
This is a blocking operation that should be run in a separate process.
|
|
"""
|
|
@spec subscribe([String.t()], (String.t(), map() -> any())) :: :ok | {:error, term()}
|
|
def subscribe(channels, callback) when is_list(channels) and is_function(callback, 2) do
|
|
# Pub/Sub in Redix requires a separate connection
|
|
host = Application.get_env(:odinsea, :redis_host, "localhost")
|
|
port = Application.get_env(:odinsea, :redis_port, 6379)
|
|
|
|
opts = [host: host, port: port]
|
|
|
|
case Redix.PubSub.start_link(opts) do
|
|
{:ok, pubsub} ->
|
|
# Subscribe to channels
|
|
Enum.each(channels, fn channel ->
|
|
Redix.PubSub.subscribe(pubsub, channel, self())
|
|
end)
|
|
|
|
# Message loop
|
|
message_loop(pubsub, callback)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp message_loop(pubsub, callback) do
|
|
receive do
|
|
{:redix_pubsub, ^pubsub, :message, %{channel: channel, payload: payload}} ->
|
|
# Decode JSON payload
|
|
case Jason.decode(payload) do
|
|
{:ok, decoded} -> callback.(channel, decoded)
|
|
{:error, _} -> callback.(channel, %{"raw" => payload})
|
|
end
|
|
message_loop(pubsub, callback)
|
|
|
|
{:redix_pubsub, ^pubsub, :subscribed, %{channel: _channel}} ->
|
|
message_loop(pubsub, callback)
|
|
|
|
_ ->
|
|
message_loop(pubsub, callback)
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Helper Functions
|
|
# ==================================================================================================
|
|
|
|
defp close_conn do
|
|
# Note: In a production setup with connection pooling,
|
|
# this would return the connection to the pool instead of closing
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the online count for a world.
|
|
"""
|
|
@spec get_world_online_count(integer()) :: integer()
|
|
def get_world_online_count(world_id) do
|
|
case get("world:#{world_id}:online_count") do
|
|
{:ok, nil} -> 0
|
|
{:ok, count} -> String.to_integer(count)
|
|
{:error, _} -> 0
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates the online count for a world.
|
|
"""
|
|
@spec update_world_online_count(integer(), integer()) :: :ok
|
|
def update_world_online_count(world_id, count) do
|
|
set("world:#{world_id}:online_count", to_string(count))
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Registers a player as online.
|
|
"""
|
|
@spec register_player_online(integer(), integer(), String.t()) :: :ok
|
|
def register_player_online(character_id, world_id, channel) do
|
|
setex("player:#{character_id}:online", 60, Jason.encode!(%{
|
|
world_id: world_id,
|
|
channel: channel,
|
|
timestamp: System.system_time(:second)
|
|
}))
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Unregisters a player as online.
|
|
"""
|
|
@spec unregister_player_online(integer()) :: :ok
|
|
def unregister_player_online(character_id) do
|
|
del("player:#{character_id}:online")
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Checks if a player is online.
|
|
"""
|
|
@spec player_online?(integer()) :: boolean()
|
|
def player_online?(character_id) do
|
|
exists?("player:#{character_id}:online")
|
|
end
|
|
end
|