mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
400 lines
16 KiB
Erlang
400 lines
16 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(call_manager_ops).
|
|
-typing([eqwalizer]).
|
|
|
|
-include_lib("fluxer_gateway/include/timeout_config.hrl").
|
|
|
|
-export([
|
|
do_create_call/3,
|
|
do_handoff_to_topology/2,
|
|
do_lookup_call/2,
|
|
do_get_or_create_call/3,
|
|
do_start_transferred_call/3,
|
|
do_stop_call/3,
|
|
do_terminate_call/2,
|
|
collect_active_call_ids/1
|
|
]).
|
|
|
|
-export_type([channel_id/0, call_data/0, state/0, handoff_result/0]).
|
|
|
|
-type channel_id() :: integer().
|
|
-type call_data() :: map().
|
|
-type state() :: #{calls := map()}.
|
|
-type handoff_result() :: #{attempted := non_neg_integer(), handed_off := non_neg_integer()}.
|
|
|
|
-spec do_create_call(channel_id(), call_data(), state()) ->
|
|
{reply, {ok, pid()} | {error, already_exists | term()}, state()}.
|
|
do_create_call(ChannelId, CallData, #{calls := Calls} = State) ->
|
|
case maps:get(ChannelId, Calls, undefined) of
|
|
{Pid, _Ref} when is_pid(Pid) ->
|
|
handle_existing_create(Pid, ChannelId, CallData, Calls, State);
|
|
undefined ->
|
|
attempt_create(ChannelId, CallData, Calls, State)
|
|
end.
|
|
|
|
-spec handle_existing_create(pid(), channel_id(), call_data(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, already_exists | term()}, state()}.
|
|
handle_existing_create(Pid, ChannelId, CallData, Calls, State) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true ->
|
|
{reply, {error, already_exists}, State};
|
|
false ->
|
|
cleanup_stale(ChannelId),
|
|
do_create_call(ChannelId, CallData, State#{calls := maps:remove(ChannelId, Calls)})
|
|
end.
|
|
|
|
-spec attempt_create(channel_id(), call_data(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, already_exists | term()}, state()}.
|
|
attempt_create(ChannelId, CallData, Calls, State) ->
|
|
CallName = process_registry:build_process_key(call, ChannelId),
|
|
case process_registry:registry_whereis(CallName) of
|
|
undefined ->
|
|
start_new_call(ChannelId, CallData, CallName, Calls, State);
|
|
_ExistingPid ->
|
|
{reply, {error, already_exists}, State}
|
|
end.
|
|
|
|
-spec start_new_call(channel_id(), call_data(), process_registry:process_key(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
start_new_call(ChannelId, CallData, CallName, Calls, State) ->
|
|
start_call_from_result(ChannelId, CallName, call:start_link(CallData), Calls, State).
|
|
|
|
-spec start_call_from_result(
|
|
channel_id(), process_registry:process_key(), {ok, pid()} | {error, term()}, map(), state()
|
|
) -> {reply, {ok, pid()} | {error, term()}, state()}.
|
|
start_call_from_result(ChannelId, CallName, StartResult, Calls, State) ->
|
|
case StartResult of
|
|
{ok, Pid} ->
|
|
start_registered_call(ChannelId, CallName, Pid, Calls, State);
|
|
{error, Reason} ->
|
|
{reply, {error, Reason}, State}
|
|
end.
|
|
|
|
-spec do_start_transferred_call(channel_id(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
do_start_transferred_call(ChannelId, TransferState, #{calls := Calls} = State) when
|
|
is_map(TransferState)
|
|
->
|
|
case ensure_local_owner(ChannelId) of
|
|
ok ->
|
|
ensure_start_transferred_call(ChannelId, TransferState, Calls, State);
|
|
{error, Reason} ->
|
|
{reply, {error, Reason}, State}
|
|
end;
|
|
do_start_transferred_call(_ChannelId, _TransferState, State) ->
|
|
{reply, {error, invalid_transfer_state}, State}.
|
|
|
|
-spec ensure_start_transferred_call(channel_id(), map(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
ensure_start_transferred_call(ChannelId, TransferState, Calls, State) ->
|
|
CallName = process_registry:build_process_key(call, ChannelId),
|
|
case process_registry:registry_whereis(CallName) of
|
|
undefined ->
|
|
start_transferred_call(ChannelId, TransferState, CallName, Calls, State);
|
|
ExistingPid when is_pid(ExistingPid) ->
|
|
use_existing_transferred_call(ChannelId, ExistingPid, Calls, State)
|
|
end.
|
|
|
|
-spec use_existing_transferred_call(channel_id(), pid(), map(), state()) ->
|
|
{reply, {ok, pid()}, state()}.
|
|
use_existing_transferred_call(ChannelId, ExistingPid, Calls, State) ->
|
|
Ref = monitor(process, ExistingPid),
|
|
ets:insert(call_pid_cache, {ChannelId, ExistingPid}),
|
|
{reply, {ok, ExistingPid}, State#{calls := Calls#{ChannelId => {ExistingPid, Ref}}}}.
|
|
|
|
-spec start_transferred_call(
|
|
channel_id(), map(), process_registry:process_key(), map(), state()
|
|
) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
start_transferred_call(ChannelId, TransferState, CallName, Calls, State) ->
|
|
StartResult = call:start_link_from_state(TransferState#{channel_id => ChannelId}),
|
|
start_call_from_result(ChannelId, CallName, StartResult, Calls, State).
|
|
|
|
-spec start_registered_call(
|
|
channel_id(), process_registry:process_key(), pid(), map(), state()
|
|
) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
start_registered_call(ChannelId, CallName, Pid, Calls, State) ->
|
|
case process_registry:register_and_monitor(CallName, Pid, Calls) of
|
|
{ok, RegPid, Ref, NewCalls0} ->
|
|
register_call(ChannelId, RegPid, Ref, NewCalls0, State);
|
|
{error, Reason} ->
|
|
{reply, {error, Reason}, State}
|
|
end.
|
|
|
|
-spec register_call(channel_id(), pid(), reference(), map(), state()) ->
|
|
{reply, {ok, pid()}, state()}.
|
|
register_call(ChannelId, RegPid, Ref, NewCalls0, State) ->
|
|
CleanCalls = maps:remove(process_registry:build_process_key(call, ChannelId), NewCalls0),
|
|
ets:insert(call_pid_cache, {ChannelId, RegPid}),
|
|
{reply, {ok, RegPid}, State#{calls := CleanCalls#{ChannelId => {RegPid, Ref}}}}.
|
|
|
|
-spec do_lookup_call(channel_id(), state()) ->
|
|
{reply, {ok, pid()} | {error, not_found}, state()}.
|
|
do_lookup_call(ChannelId, #{calls := Calls} = State) ->
|
|
case ets:lookup(call_pid_cache, ChannelId) of
|
|
[{ChannelId, Pid}] when is_pid(Pid) ->
|
|
lookup_cached(ChannelId, Pid, Calls, State);
|
|
_ ->
|
|
lookup_fallback(ChannelId, Calls, State)
|
|
end.
|
|
|
|
-spec lookup_cached(channel_id(), pid(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, not_found}, state()}.
|
|
lookup_cached(ChannelId, Pid, Calls, State) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true ->
|
|
{reply, {ok, Pid}, State};
|
|
false ->
|
|
ets:delete(call_pid_cache, ChannelId),
|
|
lookup_fallback(ChannelId, Calls, State)
|
|
end.
|
|
|
|
-spec lookup_fallback(channel_id(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, not_found}, state()}.
|
|
lookup_fallback(ChannelId, Calls, State) ->
|
|
case maps:get(ChannelId, Calls, undefined) of
|
|
{Pid, _Ref} when is_pid(Pid) ->
|
|
lookup_fallback_alive(ChannelId, Pid, Calls, State);
|
|
undefined ->
|
|
lookup_via_registry(ChannelId, Calls, State)
|
|
end.
|
|
|
|
-spec lookup_fallback_alive(channel_id(), pid(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, not_found}, state()}.
|
|
lookup_fallback_alive(ChannelId, Pid, Calls, State) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true ->
|
|
ets:insert(call_pid_cache, {ChannelId, Pid}),
|
|
{reply, {ok, Pid}, State};
|
|
false ->
|
|
cleanup_stale(ChannelId),
|
|
UpdatedCalls = maps:remove(ChannelId, Calls),
|
|
lookup_fallback(ChannelId, UpdatedCalls, State#{calls := UpdatedCalls})
|
|
end.
|
|
|
|
-spec lookup_via_registry(channel_id(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, not_found}, state()}.
|
|
lookup_via_registry(ChannelId, Calls, State) ->
|
|
CallName = process_registry:build_process_key(call, ChannelId),
|
|
case process_registry:lookup_or_monitor(CallName, ChannelId, Calls) of
|
|
{ok, Pid, _Ref, NewCalls} ->
|
|
ets:insert(call_pid_cache, {ChannelId, Pid}),
|
|
{reply, {ok, Pid}, State#{calls := NewCalls}};
|
|
{error, not_found} ->
|
|
{reply, {error, not_found}, State}
|
|
end.
|
|
|
|
-spec do_get_or_create_call(channel_id(), call_data(), state()) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
do_get_or_create_call(ChannelId, CallData, #{calls := Calls} = State) ->
|
|
case maps:get(ChannelId, Calls, undefined) of
|
|
{Pid, _Ref} when is_pid(Pid) ->
|
|
get_or_create_existing(ChannelId, Pid, CallData, Calls, State);
|
|
undefined ->
|
|
do_create_call(ChannelId, CallData, State)
|
|
end.
|
|
|
|
-spec get_or_create_existing(channel_id(), pid(), call_data(), map(), state()) ->
|
|
{reply, {ok, pid()} | {error, term()}, state()}.
|
|
get_or_create_existing(ChannelId, Pid, CallData, Calls, State) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true ->
|
|
{reply, {ok, Pid}, State};
|
|
false ->
|
|
cleanup_stale(ChannelId),
|
|
do_create_call(ChannelId, CallData, State#{calls := maps:remove(ChannelId, Calls)})
|
|
end.
|
|
|
|
-spec do_terminate_call(channel_id(), state()) ->
|
|
{reply, ok | {error, not_found}, state()}.
|
|
do_terminate_call(ChannelId, #{calls := Calls} = State) ->
|
|
do_stop_call(ChannelId, normal, State#{calls := Calls}).
|
|
|
|
-spec do_stop_call(channel_id(), term(), state()) ->
|
|
{reply, ok | {error, not_found}, state()}.
|
|
do_stop_call(ChannelId, Reason, #{calls := Calls} = State) ->
|
|
case maps:get(ChannelId, Calls, undefined) of
|
|
{Pid, Ref} ->
|
|
demonitor(Ref, [flush]),
|
|
stop_call_if_alive(Pid, Reason),
|
|
cleanup_stale(ChannelId),
|
|
{reply, ok, State#{calls := maps:remove(ChannelId, Calls)}};
|
|
undefined ->
|
|
{reply, {error, not_found}, State}
|
|
end.
|
|
|
|
-spec stop_call_if_alive(pid(), term()) -> ok.
|
|
stop_call_if_alive(Pid, Reason) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true -> gen_server:stop(Pid, Reason, ?SHUTDOWN_TIMEOUT);
|
|
false -> ok
|
|
end.
|
|
|
|
-spec do_handoff_to_topology([node()], state()) -> {handoff_result(), state()}.
|
|
do_handoff_to_topology(TargetNodes, State) when is_list(TargetNodes) ->
|
|
HandoffTargets = [Node || Node <- lists:usort(TargetNodes), is_atom(Node)],
|
|
case HandoffTargets of
|
|
[] ->
|
|
{empty_handoff_result(), State};
|
|
_ ->
|
|
handoff_call_ids(
|
|
collect_active_call_ids(maps:get(calls, State)), HandoffTargets, State
|
|
)
|
|
end;
|
|
do_handoff_to_topology(_TargetNodes, State) ->
|
|
{empty_handoff_result(), State}.
|
|
|
|
-spec collect_active_call_ids(map()) -> [channel_id()].
|
|
collect_active_call_ids(Calls) ->
|
|
lists:sort(
|
|
maps:fold(
|
|
fun collect_active_call_id/3,
|
|
[],
|
|
Calls
|
|
)
|
|
).
|
|
|
|
-spec collect_active_call_id(term(), term(), [channel_id()]) -> [channel_id()].
|
|
collect_active_call_id(ChannelId, {Pid, _Ref}, Acc) when
|
|
is_integer(ChannelId),
|
|
ChannelId > 0,
|
|
is_pid(Pid)
|
|
->
|
|
case process_liveness:is_alive(Pid) of
|
|
true -> [ChannelId | Acc];
|
|
false -> Acc
|
|
end;
|
|
collect_active_call_id(_ChannelId, _Value, Acc) ->
|
|
Acc.
|
|
|
|
-spec handoff_call_ids([channel_id()], [node()], state()) -> {handoff_result(), state()}.
|
|
handoff_call_ids(CallIds, TargetNodes, State) ->
|
|
lists:foldl(
|
|
fun(ChannelId, Acc) -> handoff_call_id(ChannelId, TargetNodes, Acc) end,
|
|
{empty_handoff_result(), State},
|
|
CallIds
|
|
).
|
|
|
|
-spec handoff_call_id(channel_id(), [node()], {handoff_result(), state()}) ->
|
|
{handoff_result(), state()}.
|
|
handoff_call_id(ChannelId, TargetNodes, {Result, State}) ->
|
|
case resolve_handoff_target(ChannelId, TargetNodes) of
|
|
skip -> {Result, State};
|
|
{handoff, TargetNode} -> handoff_call_id_to_owner(ChannelId, TargetNode, Result, State)
|
|
end.
|
|
|
|
-spec handoff_call_id_to_owner(channel_id(), node(), handoff_result(), state()) ->
|
|
{handoff_result(), state()}.
|
|
handoff_call_id_to_owner(ChannelId, TargetNode, Result, State) ->
|
|
case handoff_call_to_owner(ChannelId, TargetNode, State) of
|
|
{true, NewState} -> {mark_handoff_success(Result), NewState};
|
|
{false, NewState} -> {mark_handoff_attempt(Result), NewState}
|
|
end.
|
|
|
|
-spec resolve_handoff_target(channel_id(), [node()]) -> skip | {handoff, node()}.
|
|
resolve_handoff_target(ChannelId, TargetNodes) ->
|
|
OwnerNode = gateway_node_router:select_owner_node(ChannelId, TargetNodes),
|
|
case OwnerNode =:= node() of
|
|
true -> skip;
|
|
false -> {handoff, OwnerNode}
|
|
end.
|
|
|
|
-spec handoff_call_to_owner(channel_id(), node(), state()) -> {boolean(), state()}.
|
|
handoff_call_to_owner(ChannelId, TargetNode, State) ->
|
|
case export_local_call_state(ChannelId, State) of
|
|
{ok, TransferState} ->
|
|
start_transferred_call_on_owner(ChannelId, TargetNode, TransferState, State);
|
|
{error, _Reason} ->
|
|
{false, State}
|
|
end.
|
|
|
|
-spec export_local_call_state(channel_id(), state()) -> {ok, map()} | {error, term()}.
|
|
export_local_call_state(ChannelId, #{calls := Calls}) ->
|
|
case maps:get(ChannelId, Calls, undefined) of
|
|
{Pid, _Ref} when is_pid(Pid) ->
|
|
export_call_state(Pid);
|
|
_ ->
|
|
{error, not_found}
|
|
end.
|
|
|
|
-spec export_call_state(pid()) -> {ok, map()} | {error, term()}.
|
|
export_call_state(Pid) ->
|
|
case safe_gen_call(Pid, export_handoff_state, ?DEFAULT_GEN_SERVER_TIMEOUT) of
|
|
{ok, TransferState} when is_map(TransferState) -> {ok, TransferState};
|
|
{error, Reason} -> {error, Reason};
|
|
Other -> {error, {unexpected_export_reply, Other}}
|
|
end.
|
|
|
|
-spec start_transferred_call_on_owner(channel_id(), node(), map(), state()) ->
|
|
{boolean(), state()}.
|
|
start_transferred_call_on_owner(ChannelId, TargetNode, TransferState, State) ->
|
|
Request = {start_transferred, ChannelId, TransferState},
|
|
case call_manager_node(TargetNode, Request, ?DEFAULT_GEN_SERVER_TIMEOUT) of
|
|
{ok, Pid} when is_pid(Pid) -> stop_local_call_after_handoff(ChannelId, State);
|
|
_Other -> {false, State}
|
|
end.
|
|
|
|
-spec stop_local_call_after_handoff(channel_id(), state()) -> {boolean(), state()}.
|
|
stop_local_call_after_handoff(ChannelId, State) ->
|
|
case do_stop_call(ChannelId, {shutdown, handoff}, State) of
|
|
{reply, ok, NewState} -> {true, NewState};
|
|
{reply, _Other, NewState} -> {false, NewState}
|
|
end.
|
|
|
|
-spec ensure_local_owner(channel_id()) -> ok | {error, term()}.
|
|
ensure_local_owner(ChannelId) ->
|
|
case gateway_node_router:owner_node_result(ChannelId, calls) of
|
|
{ok, OwnerNode} when OwnerNode =:= node() -> ok;
|
|
{ok, OwnerNode} -> {error, {not_owner, OwnerNode}};
|
|
{error, Reason} -> {error, Reason}
|
|
end.
|
|
|
|
-spec cleanup_stale(channel_id()) -> ok.
|
|
cleanup_stale(ChannelId) ->
|
|
CallName = process_registry:build_process_key(call, ChannelId),
|
|
process_registry:safe_unregister(CallName),
|
|
ets:delete(call_pid_cache, ChannelId),
|
|
ok.
|
|
|
|
-spec call_manager_node(node(), term(), timeout()) -> term().
|
|
call_manager_node(TargetNode, Request, Timeout) ->
|
|
try
|
|
gen_server:call(call_manager_server_ref(TargetNode), Request, Timeout)
|
|
catch
|
|
exit:{timeout, _} -> {error, timeout};
|
|
exit:{nodedown, _} -> {error, unavailable};
|
|
exit:{noproc, _} -> {error, unavailable};
|
|
exit:_ -> {error, unavailable}
|
|
end.
|
|
|
|
-spec call_manager_server_ref(node()) -> atom() | {atom(), node()}.
|
|
call_manager_server_ref(TargetNode) when TargetNode =:= node() ->
|
|
call_manager;
|
|
call_manager_server_ref(TargetNode) ->
|
|
{call_manager, TargetNode}.
|
|
|
|
-spec safe_gen_call(pid(), term(), timeout()) -> term().
|
|
safe_gen_call(Pid, Request, Timeout) ->
|
|
try
|
|
gen_server:call(Pid, Request, Timeout)
|
|
catch
|
|
exit:{timeout, _} -> {error, timeout};
|
|
exit:{nodedown, _} -> {error, unavailable};
|
|
exit:{noproc, _} -> {error, not_found};
|
|
exit:_ -> {error, unavailable}
|
|
end.
|
|
|
|
-spec empty_handoff_result() -> handoff_result().
|
|
empty_handoff_result() ->
|
|
#{attempted => 0, handed_off => 0}.
|
|
|
|
-spec mark_handoff_attempt(handoff_result()) -> handoff_result().
|
|
mark_handoff_attempt(#{attempted := Attempted, handed_off := HandedOff}) ->
|
|
#{attempted => Attempted + 1, handed_off => HandedOff}.
|
|
|
|
-spec mark_handoff_success(handoff_result()) -> handoff_result().
|
|
mark_handoff_success(#{attempted := Attempted, handed_off := HandedOff}) ->
|
|
#{attempted => Attempted + 1, handed_off => HandedOff + 1}.
|