%%% @doc %%% Agents are a simple abstraction around state. %%% %%% Often in Erlang there is a need to share or store state that %%% must be accessed from different processes or by the same process %%% at different points in time. %%% %%% The agent module provides a basic server implementation that %%% allows state to be retrieved and updated via a simple API. %%% %%% == Examples == %%% %%% For example, the following agent implements a counter: %%% %%% ``` %%% -module(counter). %%% -export([start_link/1, value/0, increment/0]). %%% %%% start_link(InitialValue) -> %%% agent:start_link(fun() -> InitialValue end, [{name, ?MODULE}]). %%% %%% value() -> %%% agent:get(?MODULE, fun(State) -> State end). %%% %%% increment() -> %%% agent:update(?MODULE, fun(State) -> State + 1 end). %%% ''' %%% %%% Usage would be: %%% %%% ``` %%% {ok, Pid} = counter:start_link(0). %%% 0 = counter:value(). %%% ok = counter:increment(). %%% ok = counter:increment(). %%% 2 = counter:value(). %%% ''' %%% %%% Thanks to the agent server process, the counter can be safely incremented %%% concurrently. %%% %%% Agents provide a segregation between the client and server APIs (similar to %%% gen_servers). In particular, the functions passed as arguments to the calls to %%% agent functions are invoked inside the agent (the server). This distinction %%% is important because you may want to avoid expensive operations inside the %%% agent, as they will effectively block the agent until the request is %%% fulfilled. %%% %%% Consider these two examples: %%% %%% ``` %%% %% Compute in the agent/server %%% get_something(Agent) -> %%% agent:get(Agent, fun(State) -> do_something_expensive(State) end). %%% %%% %% Compute in the agent/client %%% get_something(Agent) -> %%% State = agent:get(Agent, fun(State) -> State end), %%% do_something_expensive(State). %%% ''' %%% %%% The first function blocks the agent. The second function copies all the state %%% to the client and then executes the operation in the client. One aspect to %%% consider is whether the data is large enough to require processing in the server, %%% at least initially, or small enough to be sent to the client cheaply. Another %%% factor is whether the data needs to be processed atomically: getting the %%% state and calling do_something_expensive(State) outside of the agent means %%% that the agent's state can be updated in the meantime. This is specially %%% important in case of updates as computing the new state in the client rather %%% than in the server can lead to race conditions if multiple clients are trying %%% to update the same state to different values. %%% %%% @end -module(agent). %% API -export([start_link/1, start_link/2, start_link/4, start_link/5]). -export([start/1, start/2, start/4, start/5]). -export([get/2, get/3, get/5, get/6]). -export([get_and_update/2, get_and_update/3, get_and_update/5, get_and_update/6]). -export([update/2, update/3, update/5, update/6]). -export([cast/2, cast/4]). -export([stop/1, stop/2, stop/3]). %% gen_server callbacks -behaviour(gen_server). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Types -type on_start() :: {ok, pid()} | {error, {already_started, pid()} | term()}. -type name() :: atom() | {global, term()} | {via, module(), term()}. -type agent() :: pid() | {atom(), node()} | name(). -type state() :: term(). -type init_fun() :: fun(() -> state()). -type get_fun() :: fun((state()) -> term()). -type update_fun() :: fun((state()) -> state()). -type get_and_update_fun() :: fun((state()) -> {term(), state()}). -type timeout_ms() :: non_neg_integer() | infinity. -export_type([on_start/0, name/0, agent/0, state/0]). %%% @doc %%% Starts an agent linked to the current process with the given function. %%% %%% This is often used to start the agent as part of a supervision tree. %%% %%% Once the agent is spawned, the given function Fun is invoked in the server %%% process, and should return the initial agent state. Note that start_link/2 %%% does not return until the given function has returned. %%% %%% === Options === %%% %%% The name option is used for registration. Valid values are: %%% - {name, LocalName} where LocalName is an atom %%% - {name, {global, GlobalName}} where GlobalName is any term %%% - {name, {via, Module, ViaName}} for custom registration %%% %%% If the timeout option is present, the agent is allowed to spend at most %%% the given number of milliseconds on initialization or it will be terminated %%% and the start function will return {error, timeout}. %%% %%% If the debug option is present, the corresponding function in the %%% sys module will be invoked. %%% %%% If the spawn_opt option is present, its value will be passed as options %%% to the underlying process. %%% %%% === Return values === %%% %%% If the server is successfully created and initialized, the function returns %%% {ok, Pid}, where Pid is the PID of the server. If an agent with the %%% specified name already exists, the function returns %%% {error, {already_started, Pid}} with the PID of that process. %%% %%% If the given function callback fails, the function returns {error, Reason}. %%% @end -spec start_link(init_fun()) -> on_start(). start_link(Fun) when is_function(Fun, 0) -> gen_server:start_link(?MODULE, Fun, []). -spec start_link(init_fun(), [gen_server:start_opt()]) -> on_start(). start_link(Fun, Options) when is_function(Fun, 0) -> case proplists:get_value(name, Options) of undefined -> gen_server:start_link(?MODULE, Fun, proplists:delete(name, Options)); Name -> gen_server:start_link(Name, ?MODULE, Fun, proplists:delete(name, Options)) end. %%% @doc %%% Starts an agent linked to the current process. %%% %%% Same as start_link/2 but a module, function, and arguments are expected %%% instead of an anonymous function; Fun in Module will be called with the %%% given arguments Args to initialize the state. %%% @end -spec start_link(module(), atom(), [term()], [gen_server:start_opt()]) -> on_start(). start_link(Module, Fun, Args, Options) -> gen_server:start_link(?MODULE, {Module, Fun, Args}, Options). -spec start_link(name(), module(), atom(), [term()], [gen_server:start_opt()]) -> on_start(). start_link(Name, Module, Fun, Args, Options) -> gen_server:start_link(Name, ?MODULE, {Module, Fun, Args}, Options). %%% @doc %%% Starts an agent process without links (outside of a supervision tree). %%% %%% See start_link/2 for more information. %%% @end -spec start(init_fun()) -> on_start(). start(Fun) when is_function(Fun, 0) -> gen_server:start(?MODULE, Fun, []). -spec start(init_fun(), [gen_server:start_opt()]) -> on_start(). start(Fun, Options) when is_function(Fun, 0) -> case proplists:get_value(name, Options) of undefined -> gen_server:start(?MODULE, Fun, proplists:delete(name, Options)); Name -> gen_server:start(Name, ?MODULE, Fun, proplists:delete(name, Options)) end. %%% @doc %%% Starts an agent without links with the given module, function, and arguments. %%% %%% See start_link/4 for more information. %%% @end -spec start(module(), atom(), [term()], [gen_server:start_opt()]) -> on_start(). start(Module, Fun, Args, Options) -> gen_server:start(?MODULE, {Module, Fun, Args}, Options). -spec start(name(), module(), atom(), [term()], [gen_server:start_opt()]) -> on_start(). start(Name, Module, Fun, Args, Options) -> gen_server:start(Name, ?MODULE, {Module, Fun, Args}, Options). %%% @doc %%% Gets an agent value via the given anonymous function. %%% %%% The function Fun is sent to the Agent which invokes the function %%% passing the agent state. The result of the function invocation is %%% returned from this function. %%% %%% Timeout is an integer greater than zero which specifies how many %%% milliseconds are allowed before the agent executes the function and returns %%% the result value, or the atom infinity to wait indefinitely. If no result %%% is received within the specified time, the function call fails and the caller %%% exits. %%% @end -spec get(agent(), get_fun()) -> term(). get(Agent, Fun) when is_function(Fun, 1) -> get(Agent, Fun, 5000). -spec get(agent(), get_fun(), timeout_ms()) -> term(). get(Agent, Fun, Timeout) when is_function(Fun, 1) -> gen_server:call(Agent, {get, Fun}, Timeout). %%% @doc %%% Gets an agent value via the given function. %%% %%% Same as get/3 but a module, function, and arguments are expected %%% instead of an anonymous function. The state is added as first %%% argument to the given list of arguments. %%% @end -spec get(agent(), module(), atom(), [term()], timeout_ms()) -> term(). get(Agent, Module, Fun, Args, Timeout) -> gen_server:call(Agent, {get, {Module, Fun, Args}}, Timeout). -spec get(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> term(). get(Agent, Module, Fun, Args, Timeout, _GenTimeout) -> gen_server:call(Agent, {get, {Module, Fun, Args}}, Timeout). %%% @doc %%% Gets and updates the agent state in one operation via the given anonymous %%% function. %%% %%% The function Fun is sent to the Agent which invokes the function %%% passing the agent state. The function must return a tuple with two %%% elements, the first being the value to return (that is, the "get" value) %%% and the second one being the new state of the agent. %%% %%% Timeout is an integer greater than zero which specifies how many %%% milliseconds are allowed before the agent executes the function and returns %%% the result value, or the atom infinity to wait indefinitely. If no result %%% is received within the specified time, the function call fails and the caller %%% exits. %%% @end -spec get_and_update(agent(), get_and_update_fun()) -> term(). get_and_update(Agent, Fun) when is_function(Fun, 1) -> get_and_update(Agent, Fun, 5000). -spec get_and_update(agent(), get_and_update_fun(), timeout_ms()) -> term(). get_and_update(Agent, Fun, Timeout) when is_function(Fun, 1) -> case gen_server:call(Agent, {get_and_update, Fun}, Timeout) of {error, {bad_return_value, Other}} -> exit({{bad_return_value, Other}, {gen_server, call, [Agent, {get_and_update, Fun}]}}); Reply -> Reply end. %%% @doc %%% Gets and updates the agent state in one operation via the given function. %%% %%% Same as get_and_update/3 but a module, function, and arguments are expected %%% instead of an anonymous function. The state is added as first %%% argument to the given list of arguments. %%% @end -spec get_and_update(agent(), module(), atom(), [term()], timeout_ms()) -> term(). get_and_update(Agent, Module, Fun, Args, Timeout) -> gen_server:call(Agent, {get_and_update, {Module, Fun, Args}}, Timeout). -spec get_and_update(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> term(). get_and_update(Agent, Module, Fun, Args, Timeout, _GenTimeout) -> gen_server:call(Agent, {get_and_update, {Module, Fun, Args}}, Timeout). %%% @doc %%% Updates the agent state via the given anonymous function. %%% %%% The function Fun is sent to the Agent which invokes the function %%% passing the agent state. The return value of Fun becomes the new %%% state of the agent. %%% %%% This function always returns ok. %%% %%% Timeout is an integer greater than zero which specifies how many %%% milliseconds are allowed before the agent executes the function and returns %%% the result value, or the atom infinity to wait indefinitely. If no result %%% is received within the specified time, the function call fails and the caller %%% exits. %%% @end -spec update(agent(), update_fun()) -> ok. update(Agent, Fun) when is_function(Fun, 1) -> update(Agent, Fun, 5000). -spec update(agent(), update_fun(), timeout_ms()) -> ok. update(Agent, Fun, Timeout) when is_function(Fun, 1) -> gen_server:call(Agent, {update, Fun}, Timeout). %%% @doc %%% Updates the agent state via the given function. %%% %%% Same as update/3 but a module, function, and arguments are expected %%% instead of an anonymous function. The state is added as first %%% argument to the given list of arguments. %%% @end -spec update(agent(), module(), atom(), [term()], timeout_ms()) -> ok. update(Agent, Module, Fun, Args, Timeout) -> gen_server:call(Agent, {update, {Module, Fun, Args}}, Timeout). -spec update(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> ok. update(Agent, Module, Fun, Args, Timeout, _GenTimeout) -> gen_server:call(Agent, {update, {Module, Fun, Args}}, Timeout). %%% @doc %%% Performs a cast (fire and forget) operation on the agent state. %%% %%% The function Fun is sent to the Agent which invokes the function %%% passing the agent state. The return value of Fun becomes the new %%% state of the agent. %%% %%% Note that cast returns ok immediately, regardless of whether Agent (or %%% the node it should live on) exists. %%% @end -spec cast(agent(), update_fun()) -> ok. cast(Agent, Fun) when is_function(Fun, 1) -> gen_server:cast(Agent, {cast, Fun}). %%% @doc %%% Performs a cast (fire and forget) operation on the agent state. %%% %%% Same as cast/2 but a module, function, and arguments are expected %%% instead of an anonymous function. The state is added as first %%% argument to the given list of arguments. %%% @end -spec cast(agent(), module(), atom(), [term()]) -> ok. cast(Agent, Module, Fun, Args) -> gen_server:cast(Agent, {cast, {Module, Fun, Args}}). %%% @doc %%% Synchronously stops the agent with the given Reason. %%% %%% It returns ok if the agent terminates with the given %%% reason. If the agent terminates with another reason, the call will %%% exit. %%% %%% This function keeps OTP semantics regarding error reporting. %%% If the reason is any other than normal, shutdown or %%% {shutdown, _}, an error report will be logged. %%% @end -spec stop(agent()) -> ok. stop(Agent) -> stop(Agent, normal, infinity). -spec stop(agent(), term()) -> ok. stop(Agent, Reason) -> stop(Agent, Reason, infinity). -spec stop(agent(), term(), timeout_ms()) -> ok. stop(Agent, Reason, Timeout) -> gen_server:stop(Agent, Reason, Timeout). %%%============================================================================= %%% gen_server callbacks %%%============================================================================= %%% @private init(Fun) when is_function(Fun, 0) -> _ = initial_call(Fun), {ok, run(Fun, [])}; init({Module, Fun, Args}) -> _ = initial_call({Module, Fun, Args}), {ok, run({Module, Fun, Args}, [])}. %%% @private handle_call({get, Fun}, _From, State) -> {reply, run(Fun, [State]), State}; handle_call({get_and_update, Fun}, _From, State) -> case run(Fun, [State]) of {Reply, NewState} -> {reply, Reply, NewState}; Other -> {reply, {error, {bad_return_value, Other}}, State} end; handle_call({update, Fun}, _From, State) -> {reply, ok, run(Fun, [State])}. %%% @private handle_cast({cast, Fun}, State) -> {noreply, run(Fun, [State])}. %%% @private handle_info(_Info, State) -> {noreply, State}. %%% @private terminate(_Reason, _State) -> ok. %%% @private code_change(_OldVsn, State, Fun) -> {ok, run(Fun, [State])}. %%%============================================================================= %%% Internal functions %%%============================================================================= %% @private initial_call(MFA) -> _ = put('$initial_call', get_initial_call(MFA)), ok. %% @private get_initial_call(Fun) when is_function(Fun, 0) -> {module, Module} = erlang:fun_info(Fun, module), {name, Name} = erlang:fun_info(Fun, name), {Module, Name, 0}; get_initial_call({Module, Fun, Args}) -> {Module, Fun, length(Args)}. %% @private run({Module, Fun, Args}, Extra) -> apply(Module, Fun, Extra ++ Args); run(Fun, Extra) -> apply(Fun, Extra).