Skip to content

Commit 139ff88

Browse files
committed
Finish actor api
1 parent 3d2f03d commit 139ff88

File tree

5 files changed

+466
-88
lines changed

5 files changed

+466
-88
lines changed

lib/ally_db/actor_api.ex

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
defmodule AllyDB.ActorAPI do
2+
@moduledoc """
3+
Provides the internal API for managing and interacting with custom actors.
4+
5+
This module handles starting, stopping, and communicating (call/cast) with custom
6+
actor processes (typically `GenServer`s), using the underlying `AllyDB.Core.ProcessManager`
7+
"""
8+
9+
alias AllyDB.Core.ProcessManager
10+
11+
require Logger
12+
13+
@typedoc "The unique identifier for an actor instance."
14+
@type actor_id :: ProcessManager.id()
15+
16+
@typedoc "The module implementing the `AllyDB.Computation` behaviour."
17+
@type actor_module :: module()
18+
19+
@typedoc "Initialization arguments passed to the actor's `init/1` callback."
20+
@type init_arg :: any()
21+
22+
@typedoc "The request message sent via `call/3`."
23+
@type request :: any()
24+
25+
@typedoc "The message sent via `cast/2`."
26+
@type message :: any()
27+
28+
@typedoc "The reply received from a `call/3`."
29+
@type reply :: any()
30+
31+
@typedoc "Possible error reasons returned by API functions."
32+
@type error_reason ::
33+
:actor_not_found
34+
| {:start_failed, any()}
35+
| {:actor_crash, any()}
36+
| {:call_timeout}
37+
38+
@default_call_timeout 5000
39+
40+
@doc """
41+
Starts a new actor process instance.
42+
43+
The actor identified by `actor_module` (which should be an OTP process, typically a `GenServer`)
44+
will be started under the `AllyDB.DynamicSupervisor`. The `actor_id` must be
45+
unique for registration. The `init_arg` is passed to the actor's `start_link/1`.
46+
47+
The actor itself is responsible for registering its `actor_id` via
48+
`ProcessManager.register_process/2` during its initialization.
49+
"""
50+
@spec start_actor(
51+
actor_id :: actor_id(),
52+
actor_module :: actor_module(),
53+
init_arg :: init_arg()
54+
) ::
55+
{:ok, pid()}
56+
| {:error, {:already_started, pid()}}
57+
| {:error, {:start_failed, any()}}
58+
def start_actor(actor_id, actor_module, init_arg) do
59+
if not Code.ensure_loaded?(actor_module) or
60+
not function_exported?(actor_module, :behaviour_info, 1) or
61+
:computation not in actor_module.behaviour_info(:callbacks) do
62+
{:error, {:start_failed, :invalid_module}}
63+
end
64+
65+
case ProcessManager.start_process(actor_id, actor_module, init_arg) do
66+
{:ok, pid} ->
67+
Logger.info(
68+
"ActorAPI: Started actor '#{inspect(actor_id)}' (Module: #{inspect(actor_module)}, PID: #{inspect(pid)})"
69+
)
70+
71+
{:ok, pid}
72+
73+
{:error, {:already_started, pid}} ->
74+
Logger.warning(
75+
"ActorAPI: Actor '#{inspect(actor_id)}' already started (PID: #{inspect(pid)})"
76+
)
77+
78+
{:error, {:already_started, pid}}
79+
80+
{:error, reason} ->
81+
Logger.error(
82+
"ActorAPI: Failed to start actor '#{inspect(actor_id)}' (Module: #{inspect(actor_module)}). Reason: #{inspect(reason)}"
83+
)
84+
85+
{:error, {:start_failed, reason}}
86+
end
87+
end
88+
89+
@doc """
90+
Stops a running actor process instance identified by `actor_id`.
91+
"""
92+
@spec stop_actor(actor_id :: actor_id()) :: :ok | {:error, :actor_not_found}
93+
def stop_actor(actor_id) do
94+
case ProcessManager.stop_process(actor_id) do
95+
:ok ->
96+
Logger.info("ActorAPI: Stopped actor '#{inspect(actor_id)}'")
97+
:ok
98+
99+
{:error, :not_found} ->
100+
Logger.error("ActorAPI: Failed to stop actor '#{inspect(actor_id)}'. Reason: Not found.")
101+
102+
{:error, :actor_not_found}
103+
end
104+
end
105+
106+
@doc """
107+
Sends a synchronous request to the actor identified by `actor_id`.
108+
109+
Waits for a reply from the actor's `handle_call/3` callback.
110+
Returns `{:ok, reply}` where `reply` is the actual value returned by the
111+
actor's `handle_call/3` (e.g., `{:ok, value}` or `{:error, reason}`).
112+
Returns `{:error, :actor_not_found}` if the actor ID is not registered.
113+
Returns `{:error, :call_timeout}` if the actor doesn't reply within the timeout.
114+
Returns `{:error, {:actor_crash, reason}}` if the actor process crashes during the call.
115+
"""
116+
@spec call(
117+
actor_id :: actor_id(),
118+
request :: request(),
119+
timeout :: timeout()
120+
) ::
121+
{:ok, reply()} | {:error, error_reason()}
122+
def call(actor_id, request, timeout \\ @default_call_timeout) do
123+
case find_actor_pid(actor_id) do
124+
{:ok, pid} ->
125+
try do
126+
GenServer.call(pid, request, timeout)
127+
catch
128+
:exit, {:timeout, {GenServer, :call, [_, _, ^timeout]}} ->
129+
Logger.error(
130+
"ActorAPI: Call to actor '#{inspect(actor_id)}' timed out after #{timeout}ms."
131+
)
132+
133+
{:error, :call_timeout}
134+
135+
:exit, reason ->
136+
Logger.error(
137+
"ActorAPI: Call to actor '#{inspect(actor_id)}' (PID: #{inspect(pid)}) crashed. Reason: #{inspect(reason)}"
138+
)
139+
140+
{:error, {:actor_crash, reason}}
141+
else
142+
reply ->
143+
{:ok, reply}
144+
end
145+
146+
{:error, :actor_not_found} ->
147+
{:error, :actor_not_found}
148+
end
149+
end
150+
151+
@doc """
152+
Sends an asynchronous message to the actor identified by `actor_id`.
153+
154+
Does not wait for a reply. The message is handled by the actor's `handle_cast/2`.
155+
Returns `:ok` if the actor ID was found (message sent).
156+
Returns `{:error, :actor_not_found}` if the actor ID is not registered.
157+
"""
158+
@spec cast(actor_id :: actor_id(), message :: message()) ::
159+
:ok | {:error, :actor_not_found}
160+
def cast(actor_id, message) do
161+
case find_actor_pid(actor_id) do
162+
{:ok, pid} ->
163+
GenServer.cast(pid, message)
164+
:ok
165+
166+
{:error, :actor_not_found} ->
167+
{:error, :actor_not_found}
168+
end
169+
end
170+
171+
@spec find_actor_pid(actor_id :: actor_id()) ::
172+
{:ok, pid()} | {:error, :actor_not_found}
173+
defp find_actor_pid(actor_id) do
174+
case ProcessManager.lookup_process(actor_id) do
175+
[{pid, _value}] ->
176+
{:ok, pid}
177+
178+
[] ->
179+
Logger.error("ActorAPI: Cannot find PID for actor '#{inspect(actor_id)}'.")
180+
181+
{:error, :actor_not_found}
182+
end
183+
end
184+
end

lib/ally_db/computation.ex

Lines changed: 0 additions & 85 deletions
This file was deleted.

lib/ally_db/core/process_manager.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ defmodule AllyDB.Core.ProcessManager do
1414
@doc """
1515
Starts a child process under the DynamicSupervisor.
1616
17-
The `init_arg` provided here will be wrapped with the `id` into a tuple
18-
`{id, init_arg}` before being passed to the child's `start_link/1`.
19-
The child process itself is responsible for registration via `register_process/2`.
17+
The `init_arg` provided here is passed directly to the child's `start_link/1` function.
18+
The child process itself is responsible for registration via `register_process/2`
19+
if needed, typically during its `init/1` callback.
2020
"""
2121
@spec start_process(id :: id(), module :: module(), init_arg :: any()) ::
2222
DynamicSupervisor.on_start_child()

0 commit comments

Comments
 (0)