Current section

Files

Jump to
agent_session_manager lib asm session owner_guard.ex
Raw

lib/asm/session/owner_guard.ex

defmodule ASM.Session.OwnerGuard do
@moduledoc """
Binds a session subtree's lifetime to an owner process.
The provider form of `ASM.query/3` starts a session, runs one prompt, and
stops the session from the caller's `after` block. An untrappable
`Process.exit(caller, :kill)` skips that block, and the supervised subtree
keeps running with its provider process group still attached. This guard
closes that window: it monitors the owner and terminates the subtree child
through `ASM.Session.Supervisor.stop_session/2` when the owner goes down.
Two placement facts matter:
- stopping a session subtree is `ASM.Session.Server` stopping itself is not
enough, because `ASM.Session.Subtree` would restart it; the subtree child
itself has to be terminated
- the guard therefore lives OUTSIDE the subtree it guards, under
`ASM.Session.GuardSupervisor`. A guard supervised by the subtree it is
terminating would be shut down while blocked in that very call
The guard also monitors the subtree, so an ordinary `ASM.stop_session/1`
retires the guard instead of leaving it waiting on an owner forever.
"""
use GenServer, restart: :temporary
alias ASM.Session
@type state :: %{
session_id: String.t(),
supervisor: GenServer.server(),
owner_ref: reference(),
subtree_ref: reference()
}
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) when is_list(opts), do: GenServer.start_link(__MODULE__, opts)
@impl true
def init(opts) do
owner_ref = opts |> Keyword.fetch!(:owner) |> Process.monitor()
subtree_ref = opts |> Keyword.fetch!(:subtree) |> Process.monitor()
{:ok,
%{
session_id: Keyword.fetch!(opts, :session_id),
supervisor: Keyword.fetch!(opts, :supervisor),
owner_ref: owner_ref,
subtree_ref: subtree_ref
}}
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{owner_ref: ref} = state) do
_ = Session.Supervisor.stop_session(state.supervisor, state.session_id)
{:stop, :normal, state}
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{subtree_ref: ref} = state) do
{:stop, :normal, state}
end
def handle_info(_message, state), do: {:noreply, state}
end