diff --git a/.gitignore b/.gitignore index 8e46d5a..7e2ec34 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,6 @@ deps *.plt erl_crash.dump ebin -rel/example_project +_rel/* .concrete/DEV_MODE .rebar diff --git a/Makefile b/Makefile index 86f0e80..9a49fe8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ PROJECT = egol -DEPS = edown gen_leader gproc lager +#DEPS = edown gen_leader gproc lager +DEPS = lager -dep_gproc = git git://github.com/uwiger/gproc 0.3.1 -dep_edown = git https://github.com/esl/edown.git master -dep_gen_leader = git https://github.com/abecciu/gen_leader_revival.git master +#dep_gproc = git git://github.com/uwiger/gproc 0.3.1 +#dep_edown = git https://github.com/esl/edown.git master +#dep_gen_leader = git https://github.com/abecciu/gen_leader_revival.git master include erlang.mk @@ -15,5 +16,7 @@ ERLC_COMPILE_OPTS= +'{parse_transform, lager_transform}' ERLC_OPTS= $(ERLC_COMPILE_OPTS) +debug_info TEST_ERLC_OPTS= $(ERLC_COMPILE_OPTS) +release: app + relx --vm_args "./config/vm.args" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9249f34 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +This repo originates from this article (link to NDC article) and the associated talk +at NDC London 2014 (link to slideshare). + +Conway's Game of Life cell automaton is used to show how to think like an Erlanger. + +The principle of using asynchronous message passing and thinking in terms of +protocols describing the interactions between processes is applicable to a number of +settings, not just Erlang, but the overlay of using Erlang supervision is much easier +and straightforward to do in Erlang. + +The contents of the repo has moved on from the initial version used for the article +and the talk at NDC - even the talk code was improved from the article's - but I have +tried to tag relevant revisions to make it easier for you to cherry pick a particular +stage in the evolution of the code. + +The wiki for this repo contains a more in-depth explanation of the thinking process +and the tools used than what you will find here with the code. + +Notable tags: +* ndc1 - tag for the NDC article's code. + +Notable branches: +* permanent_collector - instead of spawning a new collector for each time step, the + collector process is re-used. It saves so little resources (5%) that it is not + worth the hassle to do it. + diff --git a/config/sys.config b/config/sys.config new file mode 100644 index 0000000..5378b87 --- /dev/null +++ b/config/sys.config @@ -0,0 +1,8 @@ +[{lager, [ + {handlers, [ + {lager_console_backend, info}, + {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, + {lager_file_backend, [{file, "log/console.log"}, {level, info}]} + ]} + ]} +]. diff --git a/config/vm.args b/config/vm.args new file mode 100644 index 0000000..db0ee31 --- /dev/null +++ b/config/vm.args @@ -0,0 +1 @@ ++P 10000000 diff --git a/eqc_test/Makefile b/eqc_test/Makefile new file mode 100644 index 0000000..b565955 --- /dev/null +++ b/eqc_test/Makefile @@ -0,0 +1,8 @@ + + + +console: beams + erl -pa ../ebin ../deps/*/ebin . -config test.config + +beams: + erlc *.erl diff --git a/eqc_test/egol_eqc.erl b/eqc_test/egol_eqc.erl new file mode 100644 index 0000000..2c28bbd --- /dev/null +++ b/eqc_test/egol_eqc.erl @@ -0,0 +1,472 @@ +-module(egol_eqc). + +-compile(export_all). + +-include_lib("eqc/include/eqc.hrl"). +-include_lib("eqc/include/eqc_component.hrl"). + +-record(state, + { id, + dim, + content, + time=0, + waiting_on = undefined, + neighbour_count = 0, + neighbour_history = [], + neighbours = [] :: [pid()], + kill_count=0, + pending_query_content=[] + }). + + +api_spec() -> + #api_spec{ + language = erlang, + modules = [ + #api_module{ + name = egol_protocol, + functions = [ #api_fun{ name = query_content, arity = 3}, + #api_fun{ name = query_response, arity = 2} + ] + } + ]}. + + + +initial_state() -> + #state{}. + +%% @doc Default generated property +-spec prop_cell() -> eqc:property(). +prop_cell() -> + ?SETUP( fun my_setup/0, + ?FORALL(Cmds, commands(?MODULE), + begin + start(), + {H, S, Res} = run_commands(?MODULE,Cmds), + stop(S), + pretty_commands(?MODULE, Cmds, {H, S, Res}, + aggregate(command_names(Cmds), + Res == ok)) + end)). + +my_setup() -> + eqc_mocking:start_mocking(api_spec()), + fun eqc_mocking:stop_mocking/0. + + +start() -> + {ok, _} = application:ensure_all_started(egol), + ok. + +stop(S) -> + application:stop(egol), + [ catch exit(NPid, stop) || NPid <- S#state.neighbours ], + timer:sleep(10), + ok. + + + + +weight(#state{waiting_on=W}, step) when is_list(W) -> 0; +weight(_S, get) -> 1; +weight(_S, cell) -> 1; +weight(_S, query_response) -> 25; +weight(#state{waiting_on=[_]}, last_query_response) -> 100; +weight(_S, kill) -> 1; +weight(_S, _) -> 2. + + +command_precondition_common(S, Cmd) -> + S#state.id /= undefined orelse Cmd == cell. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +cell(XY, Dim, Content) -> + {ok, _Pid} = egol_cell_sup:start_cell(XY, Dim, Content), + Neighbours = start_neighbours(XY, Dim), + Neighbours. + +start_neighbours(XY, Dim) -> + Nids = egol_util:neighbours(XY, Dim), + [ start_neighbour(Nid) || Nid <- Nids ]. + +start_neighbour(Nid) -> + Pid = spawn( fun dummy_neigbour_loop/0 ), + egol_cell_mgr:reg(Nid, Pid), + Pid. + +dummy_neigbour_loop() -> + receive + _ -> dummy_neigbour_loop() + end. + +cell_args(_S) -> + ?LET({CellId, Dim}, cell_id_and_dim(), + [CellId, Dim, content()]). + +cell_pre(S) -> + S#state.id == undefined. + +cell_post(_S, [Id, _, _], _Neighbours) -> + Pid = egol_cell_mgr:lookup(Id), + is_pid(Pid) andalso erlang:is_process_alive(Pid). + +cell_next(S, Res, [CellId, Dim, Content]) -> + S#state{id=CellId, dim=Dim, content=Content, neighbours=Res}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +step(Id, _Dim, _Time) -> + egol_cell:step(Id), + await_collecting_status(Id, 10). + + +await_collecting_status(_Id, 0) -> + step_error; +await_collecting_status(Id, N) -> + case egol_cell:collecting_status(Id) of + undefined -> + timer:sleep(5), + await_collecting_status(Id, N-1); + _ -> + step_success + end. + +step_args(S) -> + [S#state.id, S#state.dim, S#state.time]. + +step_pre(S) -> + S#state.id /= undefined andalso + S#state.waiting_on == undefined. + +step_pre(S, [XY, Dim, Time]) -> + S#state.id == XY andalso + S#state.dim == Dim andalso + S#state.time == Time. + +step_callouts(_S, [XY, Dim, Time ]) -> + step_callouts_for_time(Time, egol_util:neighbours(XY, Dim), XY). + +step_callouts_for_time(Time, Neighbours, XY) -> + Queries = lists:duplicate(8, ?CALLOUT(egol_protocol, query_content, [?WILDCARD, Time, XY], ok)), + %% Queries = [ ?CALLOUT(egol_protocol, query_content, [N, Time, XY], ok) + %% || N <- Neighbours ], + %% io:format("Queries ~p~n", [Queries]), + ?PAR(Queries). + +step_post(_S, _Args, Res) -> + Res == step_success. + +step_next(S, _Res, _Args) -> + S#state{waiting_on = egol_util:neighbours(S#state.id, S#state.dim)}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +get(Id, Time) -> + egol_cell:get(Id, Time). + +get_args(S) -> + [S#state.id, S#state.time]. + +get_pre(S) -> + S#state.id /= undefined. + +get_pre(S, [Id, Time]) -> + S#state.id == Id andalso + S#state.time == Time. + +get_post(S, [_Id, _Time], Res) -> + Res == S#state.content. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +query_response(Id, Resp) -> + send_query_response(Id, Resp). + +query_response_args(S) -> + [S#state.id, neighbour_response(S)]. + +neighbour_response(S) -> + ?LET(N, neighbour(S), + case lists:keyfind({N, S#state.time}, 1, S#state.neighbour_history) of + false -> + {{N, S#state.time}, content()}; + Resp -> + Resp + end). + +query_response_pre(S) -> + is_list(S#state.waiting_on) andalso + length(S#state.waiting_on) > 1. + +query_response_pre(S, [Id, {{Neighbour, Time}, _}] ) -> + Id /= undefined andalso + is_list(S#state.waiting_on) andalso + lists:member(Neighbour, S#state.waiting_on) andalso + Time == S#state.time. + +query_response_next(S, _Res, [_, {{Neighbour, _Time}, Content}=Resp]) -> + NC = S#state.neighbour_count + Content, + Wait = lists:delete(Neighbour, S#state.waiting_on), + S#state{waiting_on = Wait, neighbour_count = NC, + neighbour_history=S#state.neighbour_history ++ [Resp]}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +last_query_response(Resp, {Id, AwaitTime}) -> + send_query_response(Id, Resp), + await_time_change(Id, AwaitTime, 50). + +await_time_change(_, _, 0) -> + error; +await_time_change(Id, AwaitTime, N) -> + case egol_cell:time(Id) of + AwaitTime -> + ok; + _ -> + timer:sleep(5), + await_time_change(Id, AwaitTime, N-1) + end. + + +last_query_response_args(S) -> + [{{hd(S#state.waiting_on), S#state.time}, content()}, {S#state.id, S#state.time+1}]. + +cell_content(#state{id=Id, time=Time, content=Content}) -> + {{Id, Time}, Content}. + +last_query_response_pre(#state{waiting_on=[_]}) -> true; +last_query_response_pre(_S) -> false. + +last_query_response_pre(S, [_, {Id, AwaitTime}]) -> + S#state.id == Id andalso + (S#state.time + 1) == AwaitTime. + +last_query_response_callouts(#state{pending_query_content=[]}, _) -> + ?EMPTY; +last_query_response_callouts(S, [{_, Content}, _]) -> + NC = S#state.neighbour_count + Content, + NextContent = next_content(S#state.content, NC), + ?PAR([?CALLOUT(egol_protocol, query_response, + [?WILDCARD, cell_content_msg(S#state.id, NeighbourTime, NextContent)], ok) + || {_NeighbourId, NeighbourTime} <- S#state.pending_query_content]). + +last_query_response_post(_S, _Args, Res) -> + Res == ok. + +last_query_response_next(S, _Res, [{_, Content}=Resp, _AwaitIdTime]) -> + NC = S#state.neighbour_count + Content, + S#state{pending_query_content=[], + content=next_content(S#state.content, NC), + time=S#state.time+1, + waiting_on=undefined, + neighbour_count=0, + neighbour_history=S#state.neighbour_history ++ [Resp]}. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +query_content(Id, Time) -> + send_query_content(Id, Time), + _ = egol_cell:collecting_status(Id), + ok. + +query_content_args(S) -> + [S#state.id, S#state.time]. + +query_content_pre(S) -> + S#state.id /= undefined. + +query_content_pre(S, [Id, Time]) -> + S#state.id == Id andalso + S#state.time == Time. + + +query_content_callouts(S, [Id, Time]) -> + CellContent = {cell_content, {{Id, Time}, S#state.content}}, + ?CALLOUT(egol_protocol, query_response, [?WILDCARD, CellContent], ok). + +query_content_next(S, _Res, [_Id, _Time]) -> + S. + + +cell_content_msg(Id, Time, Content) -> + {cell_content, {{Id, Time}, Content}}. + +cell_content_msg(S) -> + cell_content_msg(S#state.id, S#state.time, S#state.content). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +query_future(Id, _RequestId, Time) -> + send_query_content(Id, Time), + ok. + +query_future_args(#state{id=undefined}) -> + [undefined]; +query_future_args(S) -> + ?LET(Neighbour, oneof(egol_util:neighbours(S#state.id, S#state.dim)), + [S#state.id, Neighbour, S#state.time+1]). + +query_future_pre(_S, [undefined]) -> false; +query_future_pre(S, [Id, _NeighbourId, Time]) -> + S#state.id /= undefined andalso + S#state.id == Id andalso + (S#state.time+1) == Time. + +query_future_next(S, _Res, [_Id, NeighbourId, Time]) -> + S#state{pending_query_content= + S#state.pending_query_content++[{NeighbourId, Time}]}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +kill(Id, _Dim, 0, _) -> + OldPid = egol_cell_mgr:lookup(Id), + egol_cell:kill(Id), + await_new_cell_pid(OldPid, Id), + ok; +kill(Id, _Dim, EndTime, NeighbourHistory) -> + OldPid = egol_cell_mgr:lookup(Id), + egol_cell:kill(Id), + await_new_cell_pid(OldPid, Id), + timer:sleep(50), + _ = egol_cell:collecting_status(Id), + drive_collector(Id, 0, EndTime, NeighbourHistory), + timer:sleep(50), + ok. + +await_new_cell_pid(OldPid, Id) -> + case egol_cell_mgr:lookup(Id) of + NewPid when is_pid(NewPid) andalso + NewPid /= OldPid -> + NewPid; + _ -> + timer:sleep(5), + await_new_cell_pid(OldPid, Id) + end. + + +%% go forward until the cell has progressed to max time +drive_collector(_Id, T, T, _NeighbourHistory) -> + ok; +drive_collector(Id, T, EndTime, NeighbourHistory) -> + ResponsesForTime = [ R + || {{_,RTime},_}=R <- NeighbourHistory, + RTime == T ], + send_all_query_responses(Id, ResponsesForTime), + case await_time_change(Id, T, 20) of + ok -> + timer:sleep(50), + _ = egol_cell:collecting_status(Id), + drive_collector(Id, T+1, EndTime, NeighbourHistory); + error -> + error + end. + +send_all_query_responses(Id, NeighbourHistory) -> + lists:foreach(fun(CellContent) -> + send_query_response(Id, CellContent) + end, + NeighbourHistory). + +kill_args(#state{id=Id, dim=Dim, time=Time, neighbour_history=NH}) -> + [Id, Dim, Time, NH]. + +kill_pre(S) -> + S#state.id /= undefined andalso + S#state.kill_count<1. + +kill_pre(S, [Id, _Dim, EndTime, _]) -> + S#state.id == Id andalso + S#state.time == EndTime. + +kill_callouts(_S, [_Id, _Dim, 0, _]) -> + ?EMPTY; +kill_callouts(_S, [Id, Dim, EndTime, _NH]) -> + Neighbours = egol_util:neighbours(Id, Dim), + StepCallouts = ?SEQ( [ step_callouts_for_time(T, Neighbours, Id) + || T <- lists:seq(0, EndTime-1) ] ), + StepCallouts. + +kill_post(_S, _Args, Res) -> + Res == ok. + +kill_next(S, _Pid, [_Id, _Dim, _EndTime, _NH]) -> + S#state{waiting_on=undefined, + pending_query_content = [], + neighbour_count=0, + kill_count=S#state.kill_count+1}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +kill_neighbour(Nid, Id) -> + Pid = egol_cell_mgr:lookup(Nid), + ensure_death(Pid), + NewPid = start_neighbour(Nid), + timer:sleep(200), + _Status = egol_cell:collecting_status(Id), + timer:sleep(200), + NewPid. + +ensure_death(Pid) -> + Ref = monitor(process, Pid), + exit(Pid, stop), + receive + {'DOWN', Ref, process, Pid, _Reason} -> ok + end. + + +kill_neighbour_args(S) -> + [neighbour(S), S#state.id]. + +kill_neighbour_pre(#state{waiting_on=undefined}) -> false; +kill_neighbour_pre(#state{waiting_on=W}) -> W /= []. + +kill_neighbour_callouts(S, [Nid, _Id]) -> + case lists:member(Nid, S#state.waiting_on) of + true -> + ?CALLOUT(egol_protocol, query_content, [?WILDCARD, S#state.time, S#state.id], ok); + false -> + ?EMPTY + end. + +kill_neighbour_next(S, NewPid, _Args) -> + S#state{neighbours = [NewPid|S#state.neighbours]}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% GENERATORS +cell_id_and_dim() -> + ?LET({DimX, DimY}, {dim(),dim()}, + ?LET({X,Y}, {coord(DimX), coord(DimY)}, + {{X,Y},{DimX,DimY}})). + +dim() -> + ?SUCHTHAT(N, nat(), N>2). + +coord(Max) -> + choose(0, Max-1). + +pos_int() -> + ?LET(N, nat(), N+1). + +content() -> + choose(0,1). + +neighbour(S) -> + oneof(egol_util:neighbours(S#state.id, S#state.dim)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% HELPERS + +next_content(0,3) -> 1; +next_content(C, N) when N==2; N==3 -> C; +next_content(_,_) -> 0. + +send_query_response(Id, {{XY,T}, C}) -> + try + egol_cell:query_response(Id, cell_content_msg(XY, T, C)) + catch + Error:Reason -> + exit({send_query_response, Error, Reason, Id, {XY,T}, C}) + end. + +send_query_content(Id, Time) -> + egol_cell:query_content(Id, Time, na). + + diff --git a/eqc_test/egol_eqc_SUITE.erl b/eqc_test/egol_eqc_SUITE.erl new file mode 100644 index 0000000..e77392c --- /dev/null +++ b/eqc_test/egol_eqc_SUITE.erl @@ -0,0 +1,11 @@ +-module(egol_eqc_SUITE). + +-compile(export_all). + +-include_lib("eqc/include/eqc_ct.hrl"). + +all() -> [check_prop_cell]. + +check_prop_cell(_) -> + ?quickcheck((egol_eqc:prop_cell())). + diff --git a/eqc_test/test.config b/eqc_test/test.config new file mode 100644 index 0000000..2d51983 --- /dev/null +++ b/eqc_test/test.config @@ -0,0 +1,14 @@ +[{lager, + [{handlers, + [{lager_console_backend, [critical, true]}, + {lager_file_backend, + [{"log/error.log", error, 10485760, "$D0", 5}, + {"log/console.log", info, 10485760, "$D0", 5}, + {"log/debug.log", debug, 10485760, "$D0", 5} + ]} + + ]} + ]}, + {sasl, + [{sasl_error_logger, false}]} +]. diff --git a/relx.config b/relx.config index 79af9f9..d15e190 100644 --- a/relx.config +++ b/relx.config @@ -18,6 +18,5 @@ {release, {egol, "0.0.1"}, [egol, sasl, lager, - mnesia, - gproc + mnesia, runtime_tools, inets ]}. diff --git a/src/array2.erl b/src/array2.erl new file mode 100644 index 0000000..9f02fda --- /dev/null +++ b/src/array2.erl @@ -0,0 +1,18 @@ +-module( array2 ). + +-export([create/1, + get/2, + set/3]). + +create({X, Y}) -> + array:new( [{size, X}, {default, array:new( [{size, Y}, {default, 0}] )}] ). + +get( {X, Y}, Array ) -> + array:get( Y, array:get(X, Array) ). + +set({X, Y}, Value, Array ) -> + Y_array = array:get( X, Array ), + New_y_array = array:set( Y, Value, Y_array ), + array:set( X, New_y_array, Array ). + + diff --git a/src/egol.app.src b/src/egol.app.src index f16066e..7984d1c 100644 --- a/src/egol.app.src +++ b/src/egol.app.src @@ -1,7 +1,10 @@ {application,egol, [{description,"EGOL - Erlang Game Of Life"}, {vsn,"0.0.1"}, - {modules,[egol]}, - {registered,[]}, - {applications,[kernel, stdlib, gproc, lager]}, + {modules,[egol_app, egol_sup, + egol_cell_sup, egol_cell, + egol_cell_mgr]}, + {registered,[egol_sup, egol_cell_sup, egol_cell_mgr]}, + {applications,[kernel, stdlib, lager]}, + {mod, {egol_app, []}}, {start_phases,[]}]}. diff --git a/src/egol.erl b/src/egol.erl index 7adf1eb..98fb105 100644 --- a/src/egol.erl +++ b/src/egol.erl @@ -1,107 +1,182 @@ -module(egol). + +-behaviour(gen_server). + + -export([start/3, - init/3, step/0, run/0, run/1, + run_until/1, + minmax/0, + max_time/0, + mode/0, pause/0, + kill/1, print/1, print_last/0, print_lag/0]). --export([test/1]). +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-export([test/1, + debug/0]). + -record(state, {size_x, - size_y}). + size_y, + mode + }). + +-define(SERVER, ?MODULE). + + start(N, M, InitialCells) -> - spawn(?MODULE, init, [N, M, InitialCells]). + gen_server:start({local, ?SERVER}, ?MODULE, [N, M, InitialCells], []). -init(N, M, InitialCells) -> - AllCells = all_cells(N, M), - [egol_cell:start(XY, {N,M}) - || XY <- AllCells ], - timer:sleep(10), - fill_cells(InitialCells), - register(?MODULE, self()), - loop(#state{size_x=N, - size_y=M}). +init([N, M, InitialCells]) -> + gen_server:cast(self(), {start_cells, N, M, InitialCells}), + {ok, #state{size_x=N, + size_y=M, + mode=not_started}}. +debug() -> + lager:set_loglevel(lager_console_backend, debug). +start_cell(XY, Dim, true) -> + {ok, Pid} = egol_cell_sup:start_cell(XY, Dim, 1), + Pid; +start_cell(XY, Dim, false) -> + {ok, Pid} = egol_cell_sup:start_cell(XY, Dim, 0), + Pid. step() -> - ?MODULE ! step. + gen_server:cast(?SERVER, step). run() -> - ?MODULE ! run. + gen_server:cast(?SERVER, run). run(Time) -> run(), timer:sleep(Time), pause(). +run_until(EndTime) -> + gen_server:cast(?SERVER,{run_until, EndTime}). + +minmax() -> + {egol_time:min(), egol_time:max()}. + +max_time() -> + egol_time:max(). + +min_time() -> + egol_time:min(). + + +mode() -> + gen_server:call(?SERVER, mode). + pause() -> - ?MODULE ! pause. + gen_server:cast(?SERVER, pause). + +kill(XY) -> + egol_cell:kill(XY). print(T) -> - ?MODULE ! {print, T}. + gen_server:cast(?SERVER, {print, T}). print_last() -> - ?MODULE ! print_last. + gen_server:cast(?SERVER, print_last). print_lag() -> - ?MODULE ! print_lag. - -loop(State) -> - receive - step -> - step_cells(all_cells(State)), - loop(State); - run -> - run_cells(all_cells(State)), - loop(State); - pause -> - pause_cells(all_cells(State)), - loop(State); - {print,T} -> - print(State#state.size_x, State#state.size_y, T), - loop(State); - print_last -> - MinTime = minimum_time(State), - io:format("Time is ~p.~n", [MinTime]), - print(State#state.size_x, State#state.size_y, MinTime), - loop(State); - print_lag -> - print_lag(State), - loop(State) - end. - - -minimum_time(State) -> - Times = all_times(State), - lists:min(Times). + gen_server:cast(?SERVER, print_lag). + +handle_cast({start_cells, N, M, InitialCells}, State) -> + EgolPid = self(), + spawn(fun() -> start_cells(N, M, InitialCells, EgolPid) end), + {noreply, State}; +handle_cast(init_done, State) -> + {noreply, State#state{mode=step}}; +handle_cast(step, State) -> + egol_cell_mgr:set_mode(step), + step_cells(all_cells(State)), + {noreply, State#state{mode=step}}; +handle_cast(run, State) -> + egol_cell_mgr:set_mode(run), + run_cells(all_cells(State)), + {noreply, State#state{mode=run}}; +handle_cast({run_until, EndTime}, State) -> + egol_cell_mgr:set_mode({run_until, EndTime}), + run_cells_until(all_cells(State), EndTime), + {noreply, State#state{mode={run_until,EndTime}}}; +handle_cast(pause, State) -> + pause_cells(all_cells(State)), + {noreply, State#state{mode=step}}; +handle_cast({print,T}, State) -> + print(State#state.size_x, State#state.size_y, T), + {noreply, State}; +handle_cast(print_last, State) -> + MinTime = min_time(), + io:format("Time is ~p.~n", [MinTime]), + print(State#state.size_x, State#state.size_y, MinTime), + {noreply, State}; +handle_cast(print_lag, State) -> + print_lag(State), + {noreply, State}. + +start_cells(N, M, InitialCells, EgolPid) -> + AllCells = all_cells(N, M), + lists:foreach( fun(XY) -> + start_cell(XY, {N,M}, lists:member(XY, InitialCells)) + end, + AllCells ), + gen_server:cast(EgolPid, init_done). + + +handle_call(mode, _From, State) -> + {reply, State#state.mode, State}. + +handle_info(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + all_times(State) -> Tagged = all_times_tagged(State), [ T || {_, T} <- Tagged ]. all_times_tagged(State) -> - [ {XY, egol_cell:time_sync(XY)} + [ {XY, egol_cell:time(XY)} || XY <- all_cells(State) ]. -fill_cells(Cells) -> - [ egol_cell:set(Cell, 1) - || Cell <- Cells]. - - step_cells(Cells) -> lists:foreach(fun egol_cell:step/1, Cells). run_cells(Cells) -> lists:foreach(fun egol_cell:run/1, Cells). +run_cells_until(Cells, EndTime) -> + lists:foreach(fun (Cell) -> + egol_cell:run_until(Cell, EndTime) + end, + Cells). + pause_cells(Cells) -> lists:foreach(fun egol_cell:pause/1, Cells). @@ -118,7 +193,7 @@ print(N, M, Time) -> print_row(K, N, Board) -> Values = [ cell_content({N1,K}, Board) || N1 <- lists:seq(0, N-1) ], - [format_cell_value(Value) + [egol_util:format_cell_value(Value) || Value <- Values], io:format("~n"). @@ -150,16 +225,14 @@ format_lag(N) when N < 10 -> io_lib:format("~p", [N]); format_lag(_) -> "+". - - -format_cell_value(0) -> io:format("."); -format_cell_value(1) -> io:format("*"). all_cells(N, M) -> + Xs = egol_util:shuffle(lists:seq(0, N - 1)), + Ys = egol_util:shuffle(lists:seq(0, M - 1)), [ {X, Y} - || X <- lists:seq(0, N-1), - Y <- lists:seq(0, M-1) ]. + || X <- Xs, + Y <- Ys]. all_cells(#state{size_x=N, size_y=M}) -> all_cells(N, M). @@ -168,11 +241,8 @@ cells_at(Time, Cells) -> [ {C, Time} || C <- Cells ]. query_cells(CellsAtT) -> - [ begin - {_, C} = egol_cell:get_sync(XY, T), - {XY, C} - end - || {XY, T} <- CellsAtT ]. + [ {XY, egol_cell:get(XY, T)} + || {XY, T} <- CellsAtT]. cell_content(XY, Board) -> proplists:get_value(XY, Board). @@ -182,8 +252,33 @@ test(1) -> test(2) -> [{0,0}, {1,0}, {2,0}, {2,1},{1,2}]; test(3) -> - start(8,8,test(2)). - - - - + egol_sup:start_link(), + start(8,8,test(2)); +test(4) -> + test(3), + timer:sleep(50), + run(200), + kill({0,0}), + run(200); +test(5) -> + test(3), + timer:sleep(50), + run(), + timer:sleep(200), + kill({1,1}), + timer:sleep(200), + pause(), + [ kill(C) + || C <- [{0,2}, {1,2}, {2,2}, + {0,1}, {1,1}, {2,1}, + {0,0}, {1,0}, {2,0}]]; +test(6) -> + InitialCells = [{2,3}, {2,5}, + {3,2}, {3,3}, {3,4}, + {4,1}, {4,5}], + start(7,6, InitialCells); +test(simple) -> + start(4,4,test(2)); +test(N) -> + start(N,N,test(2)). + diff --git a/src/egol_app.erl b/src/egol_app.erl new file mode 100644 index 0000000..9e3187e --- /dev/null +++ b/src/egol_app.erl @@ -0,0 +1,12 @@ +-module(egol_app). + +-behaviour(application). + +-export([start/2, + stop/1]). + +start(_StartType, _StartArgs) -> + egol_sup:start_link(). + +stop(_) -> + ok. diff --git a/src/egol_array.erl b/src/egol_array.erl new file mode 100644 index 0000000..9dc1f82 --- /dev/null +++ b/src/egol_array.erl @@ -0,0 +1,110 @@ +-module(egol_array). + +-export([start/2, + run_until/2, + run/2, + print/1]). + +-export([test/1]). + +-export([get/2]). + + +-record(board, + { matrix, + dim, + time=0}). + +start(Dim, InitialCells) -> + fill_cells(#board{ matrix = array2:create(Dim), dim = Dim}, + InitialCells). + +run(Board, Time) -> + Pid = running(Board), + timer:sleep(Time), + Pid ! {stop, self()}, + receive + Res -> + Res + end. + +running(Board) -> + spawn(fun() -> running_loop(Board) end). + +running_loop(Board) -> + receive + {stop, To} -> + To ! Board + after + 0 -> + running_loop(step(Board)) + end. + + + +run_until(Board, T) -> + run_until(Board, 0, T+1). + +run_until(Board, T, T) -> + Board; +run_until(Board, T, EndTime) -> + run_until(step(Board), T+1, EndTime). + +set_cell(#board{matrix=M}=Board, XY, Content) -> + Board#board{ matrix= array2:set(XY, Content, M)}. + +fill_cells(Board, []) -> + Board; +fill_cells(Board, [XY|Rest]) -> + NewBoard = set_cell(Board, XY, 1), + fill_cells(NewBoard, Rest). + +get(#board{matrix=M}, XY) -> + array2:get(XY, M). + +step(#board{matrix=M, dim=Dim, time=T}) -> + NewBoard = start(Dim, []), + evolve_board(NewBoard#board{time=T}, {0,0}, Dim, M). + + +evolve_board(#board{time=T}=Board, {0,Y}, {_DimX, DimY}, _M) when Y==DimY -> + Board#board{time=T+1}; +evolve_board(#board{matrix=NewM}=Board, {X,Y}=XY, {DimX, DimY}=Dim, M) when X + Content = compute_cell(XY, Dim, M), + NewBoard = Board#board{ matrix=array2:set(XY, Content, NewM)}, + evolve_board(NewBoard, {X+1,Y}, Dim, M); +evolve_board(Board, {X,Y}, {DimX, _}=Dim, M) when X==DimX -> + evolve_board(Board, {0, Y+1}, Dim, M). + +compute_cell(XY, Dim, M) -> + Neighbours = egol_util:neighbours(XY, Dim), + egol_util:next_content(array2:get(XY,M), + neighbour_count(M, Neighbours, 0)). + +neighbour_count(_M, [], NC) -> + NC; +neighbour_count(M, [XY|Rest], NC) -> + neighbour_count(M, Rest, NC + array2:get(XY, M)). + + +print(#board{matrix=M, dim={DimX, DimY}, time=T}) -> + io:format("Time: ~p~n", [T]), + print(M, DimY-1, DimX). + +print(M, 0, DimX) -> + print_row(M, 0, DimX); +print(M, Y, DimX) -> + print_row(M, Y, DimX), + print(M, Y-1, DimX). + +print_row(M, Y, DimX) -> + [ egol_util:format_cell_value(array2:get({X,Y}, M)) + || X <- lists:seq(0,DimX-1) ], + io:format("~n"). + +test(1) -> + start({8,8}, test(2)); +test(2) -> + [{0,0}, {1,0}, {2,0}, {2,1},{1,2}]; +test(N) -> + start({N,N}, test(2)). diff --git a/src/egol_cell.erl b/src/egol_cell.erl index 10a82f3..b2b37b4 100644 --- a/src/egol_cell.erl +++ b/src/egol_cell.erl @@ -2,181 +2,431 @@ -compile([{parse_transform, lager_transform}]). --export([start/2, - init/1]). +-behaviour(gen_server). + +-export([start/3, + start_link/3, + kill/1, + stop/1, + where/1]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + + -export([set/2, get/2, - get_sync/2, - history_sync/1, - time_sync/1, + history/1, + time/1, run/1, + run_until/2, pause/1, - step/1]). + step/1, + query_response/2, + query_content/3]). + +%% for testing and debugging only!! +-export([collecting_status/1]). + +-type cell_content() :: 0 | 1. +-type cell_name() :: {integer(), integer()}. +-type time() :: integer(). + +-export_type([cell_content/0, cell_name/0, time/0]). +-type mode() :: 'run' | 'step'. -record(state, { xy, dim, content=0 :: 0..1, time=0, + collector :: pid() | 'undefined', + pacer :: pid() | 'undefined', + mode=step :: mode(), future= [] :: [{pid(), pos_integer()}], history=[], neighbours}). -start({X,Y}=XY, {DimX, DimY}=Dim) when X < DimX; - 0 =< X; - Y < DimY; - 0 =< Y -> - spawn(?MODULE, init, [#state{xy=XY, dim=Dim, content=0, - neighbours=neighbours(XY, Dim)}]). +start({X,Y}=XY, {DimX, DimY}=Dim, InitialContent) + when X < DimX, + 0 =< X, + Y < DimY, + 0 =< Y -> + gen_server:start(?MODULE, + #state{xy=XY, dim=Dim, content=InitialContent, + neighbours=egol_util:neighbours(XY, Dim)}, + []). +start_link({X,Y}=XY, {DimX, DimY}=Dim, InitialContent) + when X < DimX, + 0 =< X, + Y < DimY, + 0 =< Y -> + gen_server:start_link(?MODULE, + #state{xy=XY, dim=Dim, content=InitialContent, + neighbours=egol_util:neighbours(XY, Dim)}, + []). -set(To, State) -> - cmd(To, {set, State}). -get(To, Time) -> - cmd(To, {self(), {get, Time}}). +where(XY) -> + egol_cell_mgr:lookup(XY). -get_sync(To, Time) -> - {cell_content, C} = cmd_sync(To, {get, Time}), - C. +%% kill(XY) -> +%% case where(XY) of +%% undefined -> +%% ok; +%% Pid when is_pid(Pid) -> +%% exit(Pid, stop) +%% end. -history_sync(To) -> - cmd_sync(To, history). +kill(XY) -> + call(XY, kill). -time_sync(To) -> - cmd_sync(To, time). +stop(XY) -> + call(XY, stop). -run(To) -> cmd(To, run). +set(Cell, Content) -> + cast(Cell, {set, Content}). -pause(To) -> cmd(To, pause). -step(To) -> cmd(To, step). +-spec get(pid()|cell_name(), time()) -> cell_content(). +get(Cell, Time) -> + call(Cell, {get, Time}). +history(Cell) -> + call(Cell, history). + +time(Cell) -> + call(Cell, time). + + +run(Cell) -> + cast(Cell, run). + + +run_until(Cell, EndTime) -> + cast(Cell, {run_until, EndTime}). + +pause(Cell) -> + cast(Cell, pause). + +step(Cell) -> + cast(Cell, step). + +collecting_status(Cell) -> + call(Cell, collecting_status). + +query_response(Cell, Resp) -> + cast(Cell, Resp). + +query_content(Cell, Time, FromXY) -> + cast(Cell, {query_content, Time, FromXY}). + +call(Cell, Cmd) -> + gen_server:call(cell_pid(Cell), Cmd). + +cast(Cell, Cmd) -> + gen_server:cast(cell_pid(Cell), Cmd). + +cell_pid({_,_}=XY) -> + egol_cell_mgr:lookup(XY); +cell_pid(Pid) when is_pid(Pid) -> + Pid. -cmd(To, Cmd) when is_pid(To) -> - To ! Cmd; -cmd(To, Cmd) when is_tuple(To) -> - cmd( gproc:where(cell_name(To)), Cmd ); -cmd(To, Cmd) -> - lager:error("Incorrect To:~p with Cmd:~p", [To, Cmd]). -cmd_sync(To, Cmd) -> - cmd(To, {self(), Cmd}), - receive - Res -> - Res - end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% states init(#state{xy=XY}=State) -> - reg(XY), - idle(State). - - -idle(State) -> - receive - {set, NewContent} -> - idle(State#state{content=NewContent}); - {From, {get, Time}} -> - case content_at(Time, State) of - future -> - idle(State#state{future=[{From, Time} | State#state.future]}); - C -> - From ! {cell_content, C}, - idle(State) - end; - {From, history} -> - #state{history=History} = State, - From ! History, - idle(State); - {From, time} -> - From ! State#state.time, - idle(State); - run -> - running(State); - step -> - NewState=run_step(State), - idle(NewState); - pause -> - idle(State) - end. + egol_time:set(XY, 0), + egol_cell_mgr:reg(XY, self()), + {ok, State}. + + +handle_cast({set, NewContent}, State) -> + {noreply, State#state{content=NewContent}}; +handle_cast({From, {get, Time}}, State) -> + case content_at(Time, State) of + future -> + {noreply, State#state{future=[{From, Time} | State#state.future]}}; + C -> + From ! {cell_content, C}, + {noreply, State} + end; +handle_cast(run, State) -> + case is_collector_running(State) of + true -> + {noreply, State#state{mode=run}}; + false -> + NextState = start_collector(State), + {noreply, NextState#state{mode=run}} + end; +handle_cast(step, State) -> + case is_collector_running(State) of + true -> + {noreply, State#state{mode=step}}; + false -> + NewState = start_collector(State), + {noreply, NewState#state{mode=step}} + end; +handle_cast({run_until, EndTime}, + #state{time=T}=State) -> + case is_collector_running(State) of + true -> + {noreply, State#state{mode={run_until, EndTime}}}; + false -> + case T < EndTime of + true -> + NewState = start_collector(State), + {noreply, NewState#state{mode={run_until, EndTime}}}; + false -> + {noreply, State} + end + end; +handle_cast(pause, State) -> + {noreply, State#state{mode=step}}; +handle_cast({query_content, Time, From}, State) -> + case content_at(Time, State) of + future -> + {noreply, State#state{future=[{From, Time} | State#state.future]}}; + C -> + %%io:format("query_content ~p ~p ~p~n", [Time, From, State]), + egol_protocol:query_response(From, {cell_content, C}), + {noreply, State} + end; +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(kill, State) -> + {stop, kill, State}; +handle_cast(Resp, State) -> + case is_collector_running(State) of + true -> + State#state.collector ! Resp; + false -> + ok + end, + {noreply, State}. + +handle_call(history, _From, State) -> + {reply, State#state.history, State}; +handle_call(time, _From, State) -> + {reply, State#state.time, State}; +handle_call({get, Time}, _From, State) -> + case content_at(Time, State) of + future -> + {reply, future, State}; + {_, C} -> + {reply, C, State} + end; +handle_call(collecting_status, From, State) -> + case is_collector_running(State) of + true -> + State#state.collector ! {status, From}, + {noreply, State}; + false -> + {reply, undefined, State} + end; +handle_call(stop, _From, State) -> + case is_pid(State#state.collector) of + true -> + State#state.collector ! stop; + false -> + ok + end, + {stop, normal, State#state.collector, State}; +handle_call(kill, _From, State) -> + case is_pid(State#state.collector) of + true -> + State#state.collector ! stop; + false -> + ok + end, + {stop, kill, State#state.collector, State}. -running(#state{time=T, neighbours=Neighbours}=State) -> - query_neighbours(T, Neighbours), - NewState = collecting(State, 0, neighbours_at(T, Neighbours)), - receive - pause -> - idle(NewState); - step -> - NextState = collecting(NewState, 0, Neighbours), - idle(NextState) - after - 0 -> - running(NewState) - end. -run_step(#state{time=T, neighbours=Neighbours}=State) -> - query_neighbours(T, Neighbours), - collecting(State, 0, neighbours_at(T, Neighbours)). + -collecting(#state{xy=XY, content=Content, time=T, history=History, future=Future}=State, NeighbourCount, []) -> - NextContent = next_content(Content, NeighbourCount), +handle_info({Collector, {next_content, NextContent}}, + #state{collector=Collector, + future=Future, xy=XY, time=T, + content=Content, history=History}=State) -> NewFuture = process_future(XY, T+1, NextContent, Future), - lager:info("Cell ~p changing to ~p for time ~p", [XY, NextContent, T+1]), - State#state{content=NextContent, - time=T+1, - history=[{T, Content}|History], - future=NewFuture}; -collecting(#state{}=State, NeighbourCount, WaitingOn) -> + lager:debug("Cell ~p changing to ~p for time ~p", [XY, NextContent, T+1]), + egol_time:set(XY, T+1), + NextState = State#state{content=NextContent, + collector=undefined, + time=T+1, + history=[{T, Content}|History], + future=NewFuture}, + case State#state.mode of + step -> + {noreply, NextState}; + run -> + {noreply, start_collector(NextState)}; + {run_until, EndTime} -> + case NextState#state.time < EndTime of + true -> + {noreply, start_collector(NextState)}; + false -> + {noreply, NextState#state{mode=step}} + end + end. +%% handle_info({query_content, Time, From}, State) -> +%% case content_at(Time, State) of +%% future -> +%% {noreply, State#state{future=[{From, Time} | State#state.future]}}; +%% C -> +%% egol_protocol:query_response(From, {cell_content, C}), +%% {noreply, State} +%% end. + + + +terminate(_Reason, State) -> + egol_time:clear(State#state.xy), +% stop_collector(State#state.collector), + ok. + +%% stop_collector(Collector) when is_pid(Collector) -> +%% case is_process_alive(Collector) of +%% true -> +%% io:format("killing collector ~p~n", [Collector]), +%% exit(Collector, normal), +%% timer:sleep(5), +%% stop_collector(Collector); +%% false -> +%% ok +%% end; +%% stop_collector(_) -> +%% ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +is_collector_running(#state{collector=Collector}) -> + is_pid(Collector) andalso is_process_alive(Collector). + + +start_collector(#state{time=T, neighbours=Neighbours, + content=Content, xy=XY}=State) -> + lager:debug("start_collector: neighbours=~p~n", [Neighbours]), + Cell = self(), + Collector = spawn_link( fun () -> + collector_init(T, Neighbours, Cell, XY, Content) + end ), +%% io:format("start_collector new pid ~p~n", [Collector]), + State#state{collector=Collector}. + +collector_init(Time, Neighbours, Cell, XY, Content) -> + NeighbourRefs = monitor_neighbours(Neighbours), + query_neighbours(XY, Time, Neighbours), + collector_loop(egol_util:neighbours_at(Time, Neighbours), NeighbourRefs, 0, Cell, XY, Content). + +collector_loop([], _, NeighbourCount, Cell, _ID, Content) -> + Cell ! {self(), {next_content, egol_util:next_content(Content, NeighbourCount)}}; +collector_loop(WaitingOn, NeighbourRefs, NeighbourCount, Cell, Id, Content) -> receive - {From, {get, Time}} -> - case content_at(Time, State) of - future -> - collecting(State#state{future=[{From, Time}|State#state.future]}, - NeighbourCount, WaitingOn); - C -> - From ! {cell_content, C}, - collecting(State, NeighbourCount, WaitingOn) - end; - {cell_content, {{{_,_},_}=XYatT, NeighbourContent}} -> + {cell_content, {{{_,_}=XY,_}=XYatT, NeighbourContent}} -> + %% io:format("collector ~p got cell_content ~p - ~p~n", + %% [self(), XYatT, NeighbourContent]), case lists:member(XYatT, WaitingOn) of true -> - collecting(State, NeighbourCount + NeighbourContent, lists:delete(XYatT, WaitingOn)); + {value, {Ref, _}, NewNeighbourRefs} = lists:keytake(XY, 2, NeighbourRefs), + demonitor(Ref, [flush]), + collector_loop(lists:delete(XYatT, WaitingOn), + NewNeighbourRefs, + NeighbourCount + NeighbourContent, + Cell, Id, Content); false %% ignore messages we are not waiting for -> - collecting(State, NeighbourCount, WaitingOn) + %% io:format("collector got wrong message ~p~n", + %% [Msg]), + collector_loop(WaitingOn, NeighbourRefs, NeighbourCount, Cell, Id,Content) end; - {set, NewContent} %% fun stuff can happen if you change the state while running... - -> - collecting(State#state{content=NewContent}, NeighbourCount, WaitingOn) + {status, From} -> + gen_server:reply(From, {WaitingOn, NeighbourCount}), + collector_loop(WaitingOn, NeighbourRefs, NeighbourCount, Cell, Id, Content); + {'DOWN', Ref, process, Pid, Info} -> + case lists:keytake(Ref, 1, NeighbourRefs) of + false -> + %% io:format("collector got down for unmonitored process ~p:~p~n", [Pid, Info]), + collector_loop(WaitingOn, NeighbourRefs, NeighbourCount, Cell, Id, Content); + {value, {Ref, NeighbourId}, RestNeighbourRefs} -> + %% NewRef = monitor_neighbour(NeighbourId), + %% {_,Time} = hd(WaitingOn), + %% io:format("collector loop doing a query_content(~p, ~p, ~p)~n", + %% [NeighbourId, Time, Id]), + %% egol_protocol:query_content(NeighbourId, Time, Id), + %% collector_loop(WaitingOn, [{NewRef, NeighbourId}|RestNeighbourRefs], + %% NeighbourCount, Cell, Id, Content) + Self = self(), + spawn_link(fun() -> await_new_neighbour(NeighbourId, Pid, Self) end), + %% io:format("collector_loop spawned monitor_neighbour_loop(~p)~n", [NeighbourId]), + collector_loop(WaitingOn, RestNeighbourRefs, NeighbourCount, Cell, Id, Content) + end; + {new_neighbour, {N, Pid}} -> + Ref = monitor(process, Pid), + %% io:format("collector_loop now monitoring new neighbour process for ~p(~p)~n", + %% [N, Pid]), + {_,Time} = hd(WaitingOn), + egol_protocol:query_content(N, Time, Id), + collector_loop(WaitingOn, [{Ref, N}|NeighbourRefs], NeighbourCount, Cell, Id, Content); + stop -> + exit(normal); + Garbage -> + io:format("collector got GARBAGE ~p~n", + [Garbage]), + exit(collector_got_garbage) + end. + +await_new_neighbour(N, Pid, Collector) -> + case egol_cell_mgr:lookup(N) of + NewPid when is_pid(NewPid) andalso NewPid /= Pid -> + %% io:format("monitor_neighbour_loop(~p, ~p, ~p) now found ~p~n", + %% [N, Pid, Collector, NewPid]), + Collector ! {new_neighbour, {N, NewPid}}; + _ -> + timer:sleep(3), + await_new_neighbour(N, Pid, Collector) end. + + process_future(XY, Time, Content, Future) -> - {Ready, NewFuture} = lists:partition( fun({_Pid,T}) -> + {Ready, NewFuture} = lists:partition( fun({_FromXY,T}) -> T == Time end, Future), - lists:foreach( fun({Pid,_}) -> - Pid ! {cell_content, {{XY,Time}, Content}} + lists:foreach( fun({FromXY,_}) -> + egol_protocol:query_response(FromXY, {cell_content, {{XY,Time}, Content}}) end, Ready), NewFuture. -query_neighbours(T, Neighbours) -> +query_neighbours(XY, T, Neighbours) -> lists:foreach( fun(N) -> - get(N, T) - end, - Neighbours). - -next_content(1, 2) -> 1; -next_content(_, 3) -> 1; -next_content(_, _) -> 0. + egol_protocol:query_content(N, T, XY) + end, + Neighbours). - +monitor_neighbours(Neighbours) -> + lists:map( fun monitor_neighbour/1, Neighbours ). + +monitor_neighbour(N) -> + case egol_cell_mgr:lookup(N) of + undefined -> + timer:sleep(3), + monitor_neighbour(N); + Pid -> + {erlang:monitor(process, Pid), N} + end. content_at(Time, #state{xy=XY, time=Time, content=Content}) -> {{XY,Time}, Content}; @@ -186,18 +436,3 @@ content_at(Time, #state{xy=XY, history=History}) when is_integer(Time), Time >= {_, Content} = lists:keyfind(Time, 1, History), {{XY, Time}, Content}. -reg(XY) -> - gproc:reg(cell_name(XY)). - -cell_name(XY) -> {n,l,XY}. - - - -neighbours({X,Y}, {DimX, DimY}) -> - [ {Xa rem DimX, Ya rem DimY} || - Xa <- lists:seq(X-1+DimX, X+1+DimX), - Ya <- lists:seq(Y-1+DimY, Y+1+DimY), - {Xa,Ya} /= {X+DimX,Y+DimY}]. - -neighbours_at(T, Neighbours) -> - [ {N, T} || N <- Neighbours ]. diff --git a/src/egol_cell_mgr.erl b/src/egol_cell_mgr.erl new file mode 100644 index 0000000..020f741 --- /dev/null +++ b/src/egol_cell_mgr.erl @@ -0,0 +1,100 @@ +-module(egol_cell_mgr). + +-compile([{parse_transform, lager_transform}]). + +-behaviour(gen_server). + + +-export([start_link/0]). + +-export([reg/2, + lookup/1, + set_mode/1]). + +-export([count/0]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + + + +-type cell_coordinates() :: {integer(), integer()}. + + +-record(state, + { mode = step, + monitors %%:: map {reference(), cell_coordinates()} + }). + +start_link() -> + gen_server:start_link({local,?MODULE}, ?MODULE, [], []). + + +reg(XY, Pid) when is_pid(Pid) -> + gen_server:cast(?MODULE, {reg, XY, Pid}). + +set_mode(Mode) -> + gen_server:cast(?MODULE, {set_mode, Mode}). + +lookup(XY) -> + case ets:lookup(mgr_xy, XY) of + [] -> + undefined; + [{XY, Pid}] -> + Pid + end. + +count() -> + gen_server:call(?MODULE, count). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +init([]) -> + ets:new(mgr_xy, [named_table, + {read_concurrency, true}]), + {ok, #state{monitors=gb_trees:empty()}}. + +handle_call(count, _From, State) -> + C1 = ets:size(mgr_xy), + C2 = gb_trees:size(State#state.monitors), + {reply, {C1,C2}, State}. + +handle_cast({reg, XY, Pid}, + #state{monitors=Monitors}=State) -> + lager:debug("mgr reg ~p ~p", [XY, Pid]), + NewRef = erlang:monitor(process, Pid), + ets:insert(mgr_xy, {XY, Pid}), + NextState = State#state{monitors=gb_trees:enter(NewRef, XY, Monitors)}, + kickoff_cell(Pid, State#state.mode), + {noreply, NextState}; +handle_cast({set_mode, Mode}, State) -> + {noreply, State#state{mode=Mode}}. + +handle_info({'DOWN', Ref, process, _Pid, _Info}, + #state{monitors=Monitors}=State) -> + {value, XY} = gb_trees:lookup(Ref, Monitors), + ets:delete(mgr_xy, XY), + {noreply, State#state{monitors=gb_trees:delete(Ref, Monitors)}}. + +terminate(_Reason, _State) -> + ets:delete(mgr_xy). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + + + +kickoff_cell(Pid, step) -> + EndTime = egol_time:max(), + egol_cell:run_until(Pid, EndTime); +kickoff_cell(Pid, {run_until, EndTime}) -> + egol_cell:run_until(Pid, EndTime); +kickoff_cell(Pid, run) -> + egol_cell:run(Pid). + + diff --git a/src/egol_cell_sup.erl b/src/egol_cell_sup.erl new file mode 100644 index 0000000..63f5f2f --- /dev/null +++ b/src/egol_cell_sup.erl @@ -0,0 +1,51 @@ +-module(egol_cell_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0, + start_cell/3]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +start_cell(XY, Dim, InitialContent) -> + {ok, _Pid} = Res = supervisor:start_child(?SERVER, child_spec(XY, Dim, InitialContent)), +%% egol_cell_mgr:reg(XY, Pid), + Res. + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +init([]) -> + RestartStrategy = one_for_one, + MaxRestarts = 1000, + MaxSecondsBetweenRestarts = 1, + + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + + %% Restart = transient, + %% Shutdown = 10000, + %% Type = worker, + + %% AChild = {egol_cell, {egol_cell, start_link, []}, + %% Restart, Shutdown, Type, [egol_cell]}, + + {ok, {SupFlags, []}}. + + +child_spec(XY, Dim, InitialContent) -> + {{egol_cell, XY}, {egol_cell, start_link, [XY, Dim, InitialContent]}, + transient, infinity, worker, [egol_cell]}. + + diff --git a/src/egol_protocol.erl b/src/egol_protocol.erl new file mode 100644 index 0000000..5aa035a --- /dev/null +++ b/src/egol_protocol.erl @@ -0,0 +1,21 @@ +%%% @doc The intra-cell communication is placed in a module so we can mock it when +%%% testing. + +-module(egol_protocol). + +-compile([{parse_transform, lager_transform}]). + +-export([query_content/3, + query_response/2]). + + + + +query_content(XY, Time, FromXY) -> + %% Pid = egol_cell_mgr:lookup(XY), + %% Pid ! {query_content, Time, self()}. + egol_cell:query_content(XY, Time, FromXY). + +query_response(Cell, Resp) -> + %% Pid ! Resp. + egol_cell:query_response(Cell, Resp). diff --git a/src/egol_sup.erl b/src/egol_sup.erl new file mode 100644 index 0000000..a9480d1 --- /dev/null +++ b/src/egol_sup.erl @@ -0,0 +1,35 @@ +-module(egol_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + + +-define(SERVER, ?MODULE). + +start_link() -> + egol_time:init(), + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + + +init([]) -> + RestartStrategy = one_for_all, + MaxRestarts = 1000, + MaxSecondsBetweenRestarts = 3600, + + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + + Restart = permanent, + Shutdown = 2000, + + %% Time = {egol_time, {egol_time, init, []}, + %% Restart, Shutdown, worker, [egol_time]}, + CellSup= {egol_cell_sup, {egol_cell_sup, start_link, []}, + Restart, Shutdown, supervisor, [egol_cell_sup]}, + CellMgr = {egol_cell_mgr, {egol_cell_mgr, start_link, []}, + Restart, Shutdown, worker, [egol_cell_mgr]}, + + + {ok, {SupFlags, [CellMgr, CellSup]}}. diff --git a/src/egol_time.erl b/src/egol_time.erl new file mode 100644 index 0000000..46ffeba --- /dev/null +++ b/src/egol_time.erl @@ -0,0 +1,48 @@ +-module(egol_time). + +-export([init/0, + stop/0, + set/2, + min/0, + max/0, + clear/1]). + + +init() -> + ets:new(egol_time, [named_table, public]), + ets:insert(egol_time, [{max,0}]). + +stop() -> + ets:delete(egol_time). + +set(XY, Time) -> + ets:insert(egol_time, {XY, Time}), + case Time > max() of + true -> + ets:insert(egol_time, {max, Time}); + false -> + ok + end. + + + +max() -> + [{max, Max}] = ets:lookup(egol_time, max), + Max. + +clear(XY) -> + ets:delete(egol_time, XY). + +min() -> + case ets:first(egol_time) of + '$end_of_table' -> + 0; + First -> + InitialMin = ets:lookup_element(egol_time, First, 2), + ets:foldr( fun({_,T}, Min) -> + min(T, Min) + end, + InitialMin, + egol_time) + end. + diff --git a/src/egol_util.erl b/src/egol_util.erl new file mode 100644 index 0000000..928c60a --- /dev/null +++ b/src/egol_util.erl @@ -0,0 +1,54 @@ +-module(egol_util). + +-export([neighbours/2]). +-export([neighbours_at/2]). + +-export([next_content/2]). + +-export([format_cell_value/1]). +-export([shuffle/1]). + + +neighbours({X,Y}, {DimX, DimY}) -> + [ {Xa rem DimX, Ya rem DimY} || + Xa <- lists:seq(X-1+DimX, X+1+DimX), + Ya <- lists:seq(Y-1+DimY, Y+1+DimY), + {Xa,Ya} /= {X+DimX,Y+DimY}]. + +neighbours_at(T, Neighbours) -> + [ {N, T} || N <- Neighbours ]. + + +next_content(1, 2) -> 1; +next_content(_, 3) -> 1; +next_content(_, _) -> 0. + + +format_cell_value(0) -> io:format("."); +format_cell_value(1) -> io:format("*"). + +%% from https://erlangcentral.org/wiki/index.php?title=RandomShuffle +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% shuffle(List1) -> List2 +%% Takes a list and randomly shuffles it. Relies on random:uniform +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +shuffle(List) -> +%% Determine the log n portion then randomize the list. + randomize(round(math:log(length(List)) + 0.5), List). + +randomize(1, List) -> + randomize(List); +randomize(T, List) -> + lists:foldl(fun(_E, Acc) -> + randomize(Acc) + end, randomize(List), lists:seq(1, (T - 1))). + +randomize(List) -> + D = lists:map(fun(A) -> + {random:uniform(), A} + end, List), + {_, D1} = lists:unzip(lists:keysort(1, D)), + D1.