Current section
Files
Jump to
Current section
Files
src/client/temporal_sdk_client.erl
-module(temporal_sdk_client).
-behaviour(gen_server).
% elp:ignore W0012 W0040 E1599
-moduledoc {file, "../../docs/client/-module.md"}.
-export([
request/5
]).
-export([
start_link/2,
request_with_ctx/6
]).
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2
]).
-include("sdk.hrl").
-include("grpc.hrl").
-include("proto.hrl").
-doc """
SDK gRPC client configuration options as a map.
""".
-type opts() :: #{
adapter => temporal_sdk_grpc:adapter(),
pool_size => pos_integer(),
pool_strategy => round_robin | random,
grpc_opts => grpc_opts(),
grpc_opts_longpoll => grpc_opts(),
helpers => helpers()
}.
-export_type([opts/0]).
-doc """
SDK gRPC client configuration options as a property list.
""".
-type user_opts() :: [
{adapter, temporal_sdk_grpc:adapter()}
| {pool_size, pos_integer()}
| {pool_strategy, round_robin | random}
| {grpc_opts, grpc_opts()}
| {grpc_opts_longpoll, grpc_opts()}
| {helpers, helpers()}
].
-export_type([user_opts/0]).
-doc """
Temporal API gRPC service message type specification.
""".
-type msg() :: ?TEMPORAL_SPEC:'$msg'() | ?TEMPORAL_SPEC_OPERATOR:'$msg'().
-export_type([msg/0]).
-doc """
Temporal API gRPC service message name.
""".
-type msg_name() :: ?TEMPORAL_SPEC:'$msg_name'() | ?TEMPORAL_SPEC_OPERATOR:'$msg_name'().
-export_type([msg_name/0]).
-doc """
gRPC request options.
""".
-type grpc_opts() :: temporal_sdk_grpc:opts().
-export_type([grpc_opts/0]).
-doc """
gRPC request type.
gRPC request types:
- `call` - blocking request where response result is of type `t:call_result/0`,
- `cast` - non-blocking "fire-and-forget" request where response is not returned,
- `msg` - asynchronous request where response result is received as an OTP message.
""".
-type request_type() :: call | cast | msg.
-export_type([request_type/0]).
-doc """
gRPC request common error.
""".
-type request_error() ::
{error, timeout | {invalid_opts, Details :: term()} | invalid_cluster | invalid_request_type}.
-doc """
gRPC `call` request succesfull response result.
""".
-type call_result_success() :: {ok, dynamic()}.
-export_type([call_result_success/0]).
-doc """
gRPC `call` request failed response result.
""".
-type call_result_error() :: temporal_sdk_grpc:result_error() | request_error().
-export_type([call_result_error/0]).
-doc """
gRPC `call` request succesfull or failed response result.
""".
-type call_result() :: call_result_success() | call_result_error().
-export_type([call_result/0]).
-doc """
gRPC `cast` request return result.
""".
-type cast_result() :: ok | request_error().
-export_type([cast_result/0]).
-doc """
gRPC `msg` request return result.
""".
-type msg_result() :: reference() | request_error().
-export_type([msg_result/0]).
-doc """
gRPC request response result or return result.
""".
-type result() :: call_result() | cast_result() | msg_result().
-export_type([result/0]).
-doc """
SDK gRPC client request processing helper functions.
""".
-type helpers() :: #{
id => helpers_id_fun(),
identity => helpers_identity_fun(),
to_payload_mapper => to_payload_mapper(),
from_payload_mapper => from_payload_mapper(),
serializer => helpers_serializer_fun()
}.
-doc """
SDK gRPC client helper function mapping Erlang term to Temporal payload.
""".
-type to_payload_mapper() :: fun(
(
Cluster :: temporal_sdk_cluster:cluster_name(),
MsgName :: msg_name(),
Key :: term(),
Position :: pos_integer(),
Data :: temporal_sdk:term_to_payload()
) -> Payload :: temporal_sdk:temporal_payload()
).
-export_type([to_payload_mapper/0]).
-doc """
SDK gRPC client helper function mapping from Temporal payload to Erlang term.
""".
-type from_payload_mapper() :: fun(
(
Cluster :: temporal_sdk_cluster:cluster_name(),
MsgName :: msg_name(),
Key :: term(),
Position :: pos_integer(),
Data :: temporal_sdk:temporal_payload()
) -> Arg :: temporal_sdk:term_from_payload()
).
-export_type([from_payload_mapper/0]).
-doc """
Function generating gRPC request identifiers.
""".
-type helpers_id_fun() :: fun(
(
Cluster :: temporal_sdk_cluster:cluster_name(),
MsgName :: msg_name(),
IdKey :: term(),
Msg :: map()
) -> unicode:chardata()
).
-export_type([helpers_id_fun/0]).
-doc """
Function generating gRPC request worker/client identities.
""".
-type helpers_identity_fun() :: fun(
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Id :: temporal_sdk:serializable(),
MsgName :: msg_name()
) -> unicode:chardata()
).
-export_type([helpers_identity_fun/0]).
-doc """
Function serializing Erlang terms to Unicode characters.
""".
-type helpers_serializer_fun() :: fun(
(
Cluster :: temporal_sdk_cluster:cluster_name(),
MsgName :: temporal_sdk_client:msg_name() | 'temporal.api.command.v1.*' | atom(),
Key :: term(),
Term :: temporal_sdk:serializable()
) -> unicode:chardata()
).
-export_type([helpers_serializer_fun/0]).
-record(req_info, {
from :: gen_server:from() | pid() | noreply,
req_proc_pid :: pid(),
type :: call | cast | {msg, reference()},
timer_ref :: reference()
}).
-type req_info() :: #req_info{}.
-record(state, {
cluster :: temporal_sdk_cluster:cluster_name(),
req_queue = [] :: [req_info()]
}).
%% gRPC requests timeouts should be fully covered by a http2 adapter.
%% Timeouts implemented here can be considered as a last line of defence against misbehaving
%% http2 adapters.
-define(TIMEOUT_RATIO, 1.5).
-define(CALL_TIMEOUT_RATIO, 2).
-define(INTERNAL_REQUEST_MSG_TAG, temporal_sdk_internal_grpc_request).
%% -------------------------------------------------------------------------------------------------
%% internal
-doc false.
-spec request
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info()
) ->
Result :: call_result()
when
Type :: call;
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info()
) ->
Result :: cast_result()
when
Type :: cast;
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info()
) ->
Result :: msg_result()
when
Type :: msg.
request(Cluster, Msg, Type, GrpcOpts, RequestInfo) ->
case temporal_sdk_cluster:is_started(Cluster) of
true ->
do_request(
Cluster,
Msg,
Type,
GrpcOpts,
RequestInfo,
temporal_sdk_grpc:max_timeout(GrpcOpts),
otel_ctx:get_current()
);
Err ->
Err
end.
%% -------------------------------------------------------------------------------------------------
%% gen_server
-doc false.
-spec start_link(Cluster :: temporal_sdk_cluster:cluster_name(), Worker :: integer()) ->
gen_server:start_ret().
start_link(Cluster, W) ->
gen_server:start_link(
{local, temporal_sdk_utils_path:atom_path([?MODULE, Cluster, W])}, ?MODULE, [Cluster], []
).
-doc false.
init([Cluster]) ->
process_flag(trap_exit, true),
State = #state{cluster = Cluster},
{ok, State}.
-doc false.
handle_call(
{?INTERNAL_REQUEST_MSG_TAG, {Msg, Opts, RequestInfo, OtelCtx, Timeout}}, From, State
) ->
handle_request(call, From, Msg, Opts, RequestInfo, OtelCtx, Timeout, State).
-doc false.
handle_cast(
{?INTERNAL_REQUEST_MSG_TAG, Ref, From, {Msg, Opts, RequestInfo, OtelCtx, Timeout}}, State
) ->
handle_request({msg, Ref}, From, Msg, Opts, RequestInfo, OtelCtx, Timeout, State).
-doc false.
handle_info({timeout, _TimerRef, Pid}, State) ->
erlang:exit(Pid, {shutdown, timeout}),
handle_response(Pid, timeout, State);
handle_info({?TEMPORAL_GRPC_MSG_TAG, Pid, Result}, State) ->
handle_response(Pid, Result, State);
handle_info({'EXIT', _Pid, normal}, State) ->
{noreply, State};
handle_info({'EXIT', Pid, Reason}, State) ->
Result = temporal_sdk_utils_error:normalize_error(Reason),
handle_response(Pid, Result, State).
handle_request(Type, From, Msg, Opts, RequestInfo, OtelCtx, Timeout, State) ->
ReqPid = spawn_link(
?MODULE,
request_with_ctx,
[self(), State#state.cluster, Msg, Opts, RequestInfo, OtelCtx]
),
TimerRef = erlang:start_timer(round(Timeout * ?TIMEOUT_RATIO), self(), ReqPid),
Req = #req_info{from = From, req_proc_pid = ReqPid, type = Type, timer_ref = TimerRef},
State1 = State#state{req_queue = [Req | State#state.req_queue]},
{noreply, State1}.
handle_response(Pid, Result, State) ->
{value, #req_info{from = From, req_proc_pid = Pid, type = Type, timer_ref = TimerRef}, ReqQueue} =
lists:keytake(Pid, 3, State#state.req_queue),
NormResult =
case Result of
timeout ->
{error, timeout};
_ ->
erlang:cancel_timer(TimerRef),
Result
end,
case Type of
% eqwalizer:ignore
call -> gen_server:reply(From, NormResult);
% eqwalizer:ignore
{msg, Ref} -> From ! {?TEMPORAL_SDK_GRPC_TAG, Ref, NormResult}
end,
State1 = State#state{req_queue = ReqQueue},
{noreply, State1}.
%% -------------------------------------------------------------------------------------------------
%% private
-spec do_request
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info(),
Timeout :: pos_integer(),
OtelCtx :: otel_ctx:t()
) ->
Result :: call_result()
when
Type :: call;
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info(),
Timeout :: pos_integer(),
OtelCtx :: otel_ctx:t()
) ->
Result :: cast_result()
when
Type :: cast;
(
Cluster :: temporal_sdk_cluster:cluster_name(),
Msg :: msg(),
Type,
GrpcOpts :: grpc_opts(),
RequestInfo :: temporal_sdk_grpc:request_info(),
Timeout :: pos_integer(),
OtelCtx :: otel_ctx:t()
) ->
Result :: msg_result()
when
Type :: msg.
do_request(Cluster, Msg, call, Opts, RequestInfo, Timeout, OtelCtx) ->
case pick_worker(Cluster) of
{ok, Worker} ->
gen_server:call(
Worker,
{?INTERNAL_REQUEST_MSG_TAG, {Msg, Opts, RequestInfo, OtelCtx, Timeout}},
round(Timeout * ?CALL_TIMEOUT_RATIO)
);
Err ->
Err
end;
do_request(Cluster, Msg, cast, Opts, RequestInfo, Timeout, OtelCtx) ->
Pid = spawn(?MODULE, request_with_ctx, [noreply, Cluster, Msg, Opts, RequestInfo, OtelCtx]),
temporal_sdk_utils_proc:exit_after(round(Timeout * ?TIMEOUT_RATIO), Pid);
do_request(Cluster, Msg, msg, Opts, RequestInfo, Timeout, OtelCtx) ->
case pick_worker(Cluster) of
{ok, Worker} ->
Ref = make_ref(),
gen_server:cast(
Worker,
{?INTERNAL_REQUEST_MSG_TAG, Ref, self(), {Msg, Opts, RequestInfo, OtelCtx, Timeout}}
),
Ref;
Err ->
Err
end;
do_request(_Cluster, _Msg, _Invalid, _Opts, _RequestInfo, _Timeout, _OtelCtx) ->
{error, invalid_request_type}.
-spec pick_worker(Cluster :: temporal_sdk_cluster:cluster_name()) ->
{ok, atom()}
| {error, invalid_cluster}
| {error, {invalid_opts, Reason :: term()}}.
pick_worker(Cluster) ->
case temporal_sdk_client_opts:pick_worker_id(Cluster) of
{ok, W} -> {ok, temporal_sdk_utils_path:atom_path([?MODULE, Cluster, W])};
Err -> Err
end.
-doc false.
request_with_ctx(From, Cluster, Msg, Opts, RequestInfo, OtelCtx) ->
otel_ctx:attach(OtelCtx),
temporal_sdk_grpc:request(From, Cluster, Msg, Opts, RequestInfo).