Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

..

  • Loading branch information...
commit 6cb3b2d07f5e12db60c7b6b831cb7867918541b8 1 parent bf696e5
sheyll authored
Showing with 288 additions and 41 deletions.
  1. +106 −23 src/main/erlang/autumn.erl
  2. +182 −18 src/test/erlang/autumn_test.erl
View
129 src/main/erlang/autumn.erl
@@ -22,7 +22,8 @@
-export([add_factory/3,
remove_factory/1,
push/2,
- pull/3]).
+ push/1,
+ pull/2]).
%% gen_server callbacks
-export([init/1,
@@ -46,12 +47,13 @@
-registered([?SERVER]).
-type factory_id() :: term().
--type start_args() :: [au_item:ref()].
-record(state,
{factories = dict:new() :: dict(),%% id -> #factory{}
- active = dict:new() :: dict(),%% {factory_id(), [au_item:ref()]} -> pid()
- items = dict:new() :: dict() %% au_item:key() -> au_item:ref()
+ %% {factory_id(), [au_item:ref()]} -> pid()
+ active = dict:new() :: dict(),
+ items = dict:new() :: dict(), %% au_item:key() -> au_item:ref()
+ down_handler = dict:new() :: dict() %% reference() -> fun/2
}).
%%%=============================================================================
@@ -125,26 +127,33 @@ remove_factory(Id) ->
%%------------------------------------------------------------------------------
%% @doc
%%
-%% Push a value into the dependency injection mechanism. This might
-%% lead to new processes being spawned.
+%% Provide an item, that factories may use to start new
+%% processes. NOTE: A processes MUST NOT push an item that was
+%% injected as start argument. There is no reason why this should be
+%% necessary. When this is done some autumn functions might get into
+%% infinite loops.
%%
-%% The `Key' is used to identify the item. Other processes can
-%% articulate a dependency by specifying such a key as requirement.
-%%
-%% Autumn will add the key value pair to a tree containing all
-%% processes and configurations and will call `start' on all modules
-%% whose start arguments are completed by this push.
+%% @end
+%% ------------------------------------------------------------------------------
+-spec push(au_item:ref()) ->
+ ok.
+push(Item) ->
+ gen_server:cast(?SERVER, {push, Item}).
+
+%%------------------------------------------------------------------------------
+%% @doc
%%
-%% Autumn will automatically pull the values away when the process
-%% calling push dies.
+%% Provide an item, that factories may use to start new
+%% processes. This is a conveniece function that will create a new
+%% item process and link it with the calling process.
%%
%% @end
%% ------------------------------------------------------------------------------
-spec push(au_item:key(), au_item:value()) ->
ok.
push(Key, Value) ->
- Item = au_item:start_link(Key, Value),
- gen_server:call(?SERVER, {push, Item}).
+ Item = au_item:new_link(Key, Value),
+ gen_server:cast(?SERVER, {push, Item}).
%%------------------------------------------------------------------------------
%% @doc
@@ -153,10 +162,11 @@ push(Key, Value) ->
%%
%% @end
%% ------------------------------------------------------------------------------
--spec pull(au_item:key(), au_item:value(), term()) ->
+-spec pull(au_item:ref(), term()) ->
ok.
-pull(_Key, _Value, _Reason) ->
- todo.
+pull(Item, Reason) ->
+ %% TODO memory leak at monitoring!!!
+ gen_server:cast(?SERVER, {pull, Item, Reason}).
%%%=============================================================================
%%% gen_server Callbacks
@@ -191,14 +201,18 @@ handle_call({remove_factory, Id}, _, S) ->
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-handle_cast(Request, State) ->
- {stop, unexpected_cast, State}.
+handle_cast({push, Item}, S) ->
+ S2 = add_item(Item, S),
+ Factories = find_factory_by_dependency(au_item:key(Item), S2),
+ S3 = lists:foldl(fun apply_factory/2, S2, Factories),
+ {noreply, S3}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-handle_info(Info, State) ->
- {noreply, State}.
+handle_info({'DOWN',Ref,_,_,Reason}, #state{down_handler=DH} = S) ->
+ S2 = (dict:fetch(Ref, DH))(S, Reason),
+ {noreply, S2#state{down_handler = dict:erase(Ref, DH)}}.
%%------------------------------------------------------------------------------
%% @private
@@ -299,3 +313,72 @@ get_values_by_key(ItemId, S) ->
{ok, Items} ->
Items
end.
+
+%%------------------------------------------------------------------------------
+%% @doc
+%% Return a list of factories that depend on a specific item key.
+%% @end
+%%------------------------------------------------------------------------------
+-spec find_factory_by_dependency(au_item:key(), #state{}) ->
+ [#factory{}].
+find_factory_by_dependency(K, #state{factories = Fs}) ->
+ [F || {_,F} <- dict:to_list(Fs),
+ lists:member(K, F#factory.req)].
+
+
+
+%%------------------------------------------------------------------------------
+%% @doc
+%% Add an item to the set of available items.
+%% @end
+%%------------------------------------------------------------------------------
+-spec add_item(au_item:ref(), #state{}) ->
+ #state{}.
+add_item(Item, S) ->
+ K = au_item:key(Item),
+ Ref = au_item:monitor(Item),
+ ItemDown = fun(State, Reason) ->
+ remove_item(Item, State, Reason)
+ end,
+ ItemsWithSameKey = get_values_by_key(K, S),
+ S#state{
+ items = dict:store(K, [Item|ItemsWithSameKey], S#state.items),
+ down_handler = dict:store(Ref, ItemDown, S#state.down_handler)
+ }.
+
+%%------------------------------------------------------------------------------
+%% @doc
+%% Remove an item from the set of available items. No effect if the item
+%% is not available. All depending processes will be terminated.
+%% @end
+%%------------------------------------------------------------------------------
+-spec remove_item(au_item:ref(), #state{}, term()) ->
+ #state{}.
+remove_item(Item, S, Reason) ->
+ NewVs = [V || V <- get_values_by_key(au_item:key(Item), S),
+ V =/= Item],
+ S2 = S#state{items = dict:store(au_item:key(Item), NewVs, S#state.items)},
+ stop_dependent(Item, S2, Reason).
+
+%%------------------------------------------------------------------------------
+%% @doc
+%% Exits all factory instances that depend on a specific item.
+%% @end
+%%------------------------------------------------------------------------------
+-spec stop_dependent(au_item:ref(), #state{}, term()) ->
+ #state{}.
+stop_dependent(I, S = #state{active = As}, Reason) ->
+ S#state{active =
+ dict:filter(fun({_Id, Reqs}, Pid) ->
+ case lists:member(I, Reqs) of
+ true ->
+ exit(Pid, Reason),
+ false;
+ _ ->
+ true
+ end
+ end,
+ As)}.
+
+
+
View
200 src/test/erlang/autumn_test.erl
@@ -10,6 +10,7 @@
%%%................................................................Factory Tests
add_factory_already_added_test() ->
+ stop_autumn(),
{ok, _Pid} = autumn:start_link(),
MFA = {test_m, test_f, [test_arg]},
Res1 = autumn:add_factory(test_id, [xx], MFA),
@@ -18,11 +19,13 @@ add_factory_already_added_test() ->
?assertEqual({error, {already_added, test_id}}, Res2).
remove_not_existant_factory_test() ->
+ stop_autumn(),
{ok, _Pid} = autumn:start_link(),
Res = autumn:remove_factory(test_id),
?assertEqual({error, {not_found, test_id}}, Res).
remove_existant_factory_test() ->
+ stop_autumn(),
{ok, _Pid} = autumn:start_link(),
Res1 = autumn:add_factory(test_id, [xx], {test_m, test_f, [test_arg]}),
Res2 = autumn:remove_factory(test_id),
@@ -34,6 +37,7 @@ remove_existant_factory_test() ->
%%%.............................................................Dependency Tests
independent_factory_test() ->
+ stop_autumn(),
M = em:new(),
Pid = start(),
MFA = {test_m, test_f, [test_arg]},
@@ -50,26 +54,186 @@ independent_factory_test() ->
%%%...................................................................Push Tests
-push_test() ->
+simple_push_test() ->
stop_autumn(),
M = em:new(),
- Pid = start(),
- MFA = {test_m, test_f, [test_arg]},
- Factory = #factory{id = test_id,
- req = [xxx],
- start = MFA},
+ %% two modules with the same requirement, both must be started
+ Pid1 = start(),
+ MFA1 = {test_m_1, test_f, [test_arg]},
+ Factory1 = #factory{id = test_id_1,
+ req = [xxx],
+ start = MFA1},
em:strict(M, au_factory, start_child,
- [Factory, fun([I]) ->
+ [Factory1, fun([I]) ->
au_item:key(I) == xxx andalso
au_item:value(I) == some_val
end],
- {return, {ok, Pid}}),
+ {return, {ok, Pid1}}),
+
+ Pid2 = start(),
+ MFA2 = {test_m_2, test_f, [test_arg]},
+ Factory2 = #factory{id = test_id_2,
+ req = [xxx],
+ start = MFA2},
+ em:strict(M, au_factory, start_child,
+ [Factory2, fun([I]) ->
+ au_item:key(I) == xxx andalso
+ au_item:value(I) == some_val
+ end],
+ {return, {ok, Pid2}}),
+ %% third module with unsatisfied dependency to yyy
+ MFA3 = {test_m_3, test_f, [test_arg]},
+
+ em:replay(M),
+ {ok, _Pid} = autumn:start_link(),
+ Res1 = autumn:add_factory(test_id_1, [xxx], MFA1),
+ Res2 = autumn:add_factory(test_id_2, [xxx], MFA2),
+ Res3 = autumn:add_factory(test_id_3, [xxx, yyy], MFA3),
+
+ autumn:push(xxx, some_val),
+ receive after 100 -> ok end,
+ em:verify(M),
+ ?assertEqual(ok, Res1),
+ ?assertEqual(ok, Res2),
+ ?assertEqual(ok, Res3).
+
+complex_push_test() ->
+ stop_autumn(),
+ M = em:new(),
+ %% two modules with the same requirement, both must be started
+ Pid1 = start(),
+ MFA1 = {test_m_1, test_f, [test_arg]},
+ Factory1 = #factory{id = test_id_1,
+ req = [xxx],
+ start = MFA1},
+ em:strict(M, au_factory, start_child,
+ [Factory1, fun([I]) ->
+ au_item:key(I) == xxx andalso
+ au_item:value(I) == some_val
+ end],
+ {return, {ok, Pid1}}),
+
+ Pid2 = start(),
+ MFA2 = {test_m_2, test_f, [test_arg]},
+ Factory2 = #factory{id = test_id_2,
+ req = [xxx],
+ start = MFA2},
+ em:strict(M, au_factory, start_child,
+ [Factory2, fun([I]) ->
+ au_item:key(I) == xxx andalso
+ au_item:value(I) == some_val
+ end],
+ {function, fun(_) ->
+ %% the second factory pushes zzz
+ autumn:push(zzz, cool_value),
+ autumn:push(zzz, cooler_value),
+ {ok, Pid2}
+ end}),
+ %% third module with unsatisfied dependency to yyy
+ MFA3 = {test_m_3, test_f, [test_arg]},
+
+ %% the fourth modules is invoked foreach zzz that factory2 pushed
+ Pid4 = start(),
+ MFA4 = {test_m_4, test_f, [test_arg]},
+ Factory4 = #factory{id = test_id_4,
+ req = [xxx, zzz],
+ start = MFA4},
+ em:strict(M, au_factory, start_child,
+ [Factory4, fun(_) -> true end],
+ {return, {ok, Pid4}}),
+
+ Pid5 = start(),
+ TestProc = self(),
+ em:strict(M, au_factory, start_child,
+ [Factory4, fun(_) -> true end],
+ {function, fun(_) ->
+ TestProc ! finished,
+ {ok, Pid5}
+ end}),
+
em:replay(M),
{ok, _Pid} = autumn:start_link(),
- Res1 = autumn:add_factory(test_id, [xxx], {test_m, test_f, [test_arg]}),
+ Res1 = autumn:add_factory(test_id_1, [xxx], MFA1),
+ Res2 = autumn:add_factory(test_id_2, [xxx], MFA2),
+ Res3 = autumn:add_factory(test_id_3, [xxx, yyy], MFA3),
+ autumn:add_factory(test_id_4, [xxx, zzz], MFA4),
+
autumn:push(xxx, some_val),
+ receive
+ finished -> ok
+ end,
em:verify(M),
- ?assertEqual(ok, Res1).
+ ?assertEqual(ok, Res1),
+ ?assertEqual(ok, Res2),
+ ?assertEqual(ok, Res3).
+
+item_exit_test() ->
+ stop_autumn(),
+ M = em:new(),
+ %% single process will be created - and destroyed as soon as
+ %% the item is pulled.
+ Pid1 = start(),
+ MFA1 = {test_m_1, test_f, [test_arg]},
+ Factory1 = #factory{id = test_id_1,
+ req = [xxx],
+ start = MFA1},
+ em:strict(M, au_factory, start_child,
+ [Factory1, em:any()],
+ {function, fun([_, [Item]]) ->
+ au_item:invalidate(Item),
+ {ok, Pid1}
+ end}),
+ em:replay(M),
+ {ok, _Pid} = autumn:start_link(),
+ autumn:add_factory(test_id_1, [xxx], MFA1),
+ monitor(process, Pid1),
+ Item = au_item:new(xxx, v),
+ IRef = au_item:monitor(Item),
+ autumn:push(Item),
+ receive
+ {'DOWN', IRef, _, _, Reason} ->
+ receive
+ %% expect the item to be killed
+ {'DOWN', _, process, Pid1, Reason} -> ok
+ end
+ end,
+ em:verify(M),
+ ok.
+
+pull_test() ->
+ stop_autumn(),
+ M = em:new(),
+ %% single process will be created - and destroyed as soon as
+ %% the item is pulled.
+ Pid1 = start(),
+ MFA1 = {test_m_1, test_f, [test_arg]},
+ Factory1 = #factory{id = test_id_1,
+ req = [xxx],
+ start = MFA1},
+ TestP = self(),
+ em:strict(M, au_factory, start_child,
+ [Factory1, em:any()],
+ {function,
+ fun(_) ->
+ TestP ! running,
+ {ok, Pid1}
+ end}),
+ em:replay(M),
+ {ok, _Pid} = autumn:start_link(),
+ autumn:add_factory(test_id_1, [xxx], MFA1),
+ monitor(process, Pid1),
+ Item = au_item:new(xxx, v),
+ autumn:push(Item),
+ Reason = reason,
+ receive running -> ok end,
+ autumn:pull(Item, Reason),
+ receive
+ %% expect the item to be killed
+ {'DOWN', _, process, Pid1, Reason} -> ok
+ end,
+ em:verify(M),
+ ok.
+
%%%............................................................Boilerplate Tests
@@ -99,11 +263,11 @@ stop_autumn() ->
end.
start() ->
- spawn_link(fun() ->
- receive
- A -> A
-
- after 300000 ->
- ok
- end
- end).
+ spawn(fun() ->
+ receive
+ A -> A
+
+ after 300000 ->
+ ok
+ end
+ end).
Please sign in to comment.
Something went wrong with that request. Please try again.