port over some more
This commit is contained in:
257
lib/odinsea/database/redis.ex
Normal file
257
lib/odinsea/database/redis.ex
Normal file
@@ -0,0 +1,257 @@
|
||||
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
|
||||
Reference in New Issue
Block a user