Current section
Files
Jump to
Current section
Files
src/framework/kflow_gen_mfd.erl
%%%===================================================================
%%% @copyright 2019 Klarna Bank AB (publ)
%%%
%%% @doc This module defines a stateless stream processing node that
%%% combines {@link kflow_gen_map}, {@link kflow_gen_filter} and
%%% {@link kflow_gen_demux}.
%%%
%%% This behavior can be used in two modes: full and simplified. In
%%% simplified mode stream processing node is defined like following:
%%%
%%% ```{mfd, fun(Offset, Message) -> {true, Message} | {true, Route, Message} | false end}'''
%%%
%%% In full mode one has to create a callback module with
%%% `kflow_gen_mfd' behavior.
%%%
%%% `mfd' callback takes 3 arguments: first is offset of a message,
%%% second is the message itself and the third one is state of the
%%% callback module. This state is created in `init' callback and
%%% remains the same through the lifetime of the pipe. Return value of
%%% `mfd' callback should be of type {@link return_type/0}.
%%%
%%% `init' and `terminate' callbacks can be used e.g. when some
%%% resource should be obtained to process messages. Both callbacks
%%% are optional; configuration will be passed as is to `filter'
%%% callback when `init' is omitted.
%%%
%%% == Example ==
%%% ```
%%% -module(my_mfd).
%%%
%%% -behavior(kflow_gen_mfd).
%%%
%%% -export([init/1, filtermap/3, terminate/1]).
%%%
%%% init(Config) ->
%%% State = do_init(Config),
%%% State.
%%%
%%% mfd(Offset, Message, State) ->
%%% %% Apply `transform' to the message and pass it downstream:
%%% {true, transform(Message)};
%%% mfd(Offset, Message, State) ->
%%% %% Apply `transform' to the message and pass it to a substream `Route':
%%% {true, Route, transform(Message)};
%%% mfd(Offset, Message, State) ->
%%% %% Drop the message:
%%% false.
%%%
%%% terminate(State) ->
%%% do_cleanup(State).
%%% '''
%%%
%%% NOTE: Since state is immutable, it's actually shared between the
%%% routes.
%%%
%%% @end
%%%===================================================================
-module(kflow_gen_mfd).
-behavior(kflow_gen).
-include("kflow.hrl").
-include_lib("hut/include/hut.hrl").
-export([init/2, handle_message/3, handle_flush/2, terminate/2]).
-export_type([callback_fun/0, return_type/0]).
-type return_type() :: {true, Ret :: term()}
| {true, Route :: term(), Ret :: term()}
| false.
-callback init(_Config) -> _State.
-callback mfd(kflow:offset(), _DataIn, _State) ->
{true, _DataOut} | false.
-callback terminate(_State) -> _.
-optional_callbacks([init/1, terminate/1]).
-type callback_fun() :: fun((kflow:offset(), _Message) -> return_type()).
-record(s1,
{ cb_module :: module()
, cb_state :: term()
}).
-record(s2,
{ function :: callback_fun()
}).
-type state() :: #s1{} | #s2{}.
%% @private
init(_NodeId, {?MODULE, Fun}) when is_function(Fun) ->
is_function(Fun, 2) orelse error({badarity, Fun}),
{ok, #s2{ function = Fun
}};
init(_NodeId, {CbModule, CbConfig}) ->
CbState = kflow_lib:optional_callback(CbModule, init, [CbConfig], CbConfig),
{ok, #s1{ cb_module = CbModule
, cb_state = CbState
}}.
%% @private
handle_message(Msg = #kflow_msg{hidden = true}, State, _) ->
%% Don't execute callback for a hidden message, simply pass it downstream:
{ok, [Msg], State};
handle_message(Msg0, State, _) ->
#kflow_msg{payload = Payload, offset = Offset, route = Route0} = Msg0,
Ret = case State of
#s1{cb_module = CbModule, cb_state = CbState} ->
CbModule:mfd(Offset, Payload, CbState);
#s2{function = Fun} ->
Fun(Offset, Payload)
end,
Msg = case Ret of
{true, NewPayload} ->
Msg0#kflow_msg{payload = NewPayload};
{true, Route, NewPayload} ->
Msg0#kflow_msg{payload = NewPayload, route = [Route|Route0]};
false ->
Msg0#kflow_msg{hidden = true}
end,
{ok, [Msg], State}.
%% @private
handle_flush(State, _) ->
{ok, [], State}.
%% @private
terminate(#s1{cb_state = CbState, cb_module = CbModule}, _) ->
kflow_lib:optional_callback(CbModule, terminate, [CbState]);
terminate(#s2{}, _) ->
ok.