Hlavní navigace

Erlang: procesy s využitím knihoven

6. 10. 2014
Doba čtení: 23 minut

Sdílet

Programovací jazyk Erlang je určený k vytváření distribuovaných systémů pro zpracování velkého množství paralelních úloh. V dnešním díle si ukážeme, jak se v Erlangu šíří a monitorují chyby, něco o makro preprocesoru a jak napsat jednotlivé návrhové vzory procesů z minule pomocí obecných knihoven.

Než se dostaneme k vytváření procesů, je třeba si říci něco o jejich spojování a bude se hodit ukázat si makra.

Šíření chyb

Proces v Erlangu může skončit buď normálně (nezbývá již žádný kód k vykonání), nebo abnormálně, kdy se stane nějaká chyba (např. se kódu vyskytne výjimka, do case větvení přijdou data, na která neexistuje vzor apod.). Pokud nějaký proces skončí s chybou, je dobré, aby s to někdo dozvěděl a mohl reagovat. Existují dva způsoby jak to zařídit. Linkováním a monitorováním procesů.

Linkování (spojování) procesů

Linkování (spojování) procesů je operace, kdy se mezi dvěma procesy vytvoří obousměrná vazba, která způsobí, že pokud jeden z procesů zhavaruje, je druhý také ukončen. Vazba je obousměrná a nezáleží na tom, který proces ji vytvořil. Vazbu lze zrušit funkcí unlink/1. Často je třeba spustit proces, který má být ihned slinkován s procesem, který jej nastartoval. Něco jako

Pid = spawn (my_module, init, [Arguments]),
link (Pid),

V tomto kódu se hrozí chyba souběhu. Pokud nastartovaný proces skončí dříve (např. kvůli chybě v inicializaci), než se stačí slinkovat s volajícím procesem, není Pid pro slinkování platný a volání funkce link zhavaruje. Aby se to nestávalo, existuje funkce spawn_link/1, která vytvoří nový proces a slinkuje jej ještě dříve, než se v něm začne spouštět kód.

Pid = spawn_link (my_module, init, [Arguments]),

Příklad: Erlang shell, je napsán v Erlangu, takže jeho příkazová řádka má také svůj proces a ten má svůj PID. Pokud skončí (vyskytne se v něm chyba), je automaticky spuštěn nový proces s jiným PIDem.

1> self().
<0.32.0>
2> 1/0.
** exception error: an error occurred when evaluating an arithmetic expression
     in operator  '/'/2
        called as 1 / 0
3> self().
<0.35.0>

Po dělení nulou v procesu shellu proces skončil a nastartoval se nový proces s jiným PID.

Při pokusu o spuštění procesu, jehož startovací funkce neexistuje, to vypadá takto:

4> self().
<0.35.0>
5> spawn (bad_module, wrong_function, []).
<0.39.0>
6>
=ERROR REPORT==== 28-Aug-2014::23:51:06 ===
Error in process <0.39.0> with exit value: {undef,[{bad_module,wrong_function,[],[]}]}

6> self().
<0.35.0>

Nový proces skončil abnormálně, což se projevilo logovací zprávou a proces shellu běží dál se stejným PID. Pokud se nový proces spustí jako slinkovaný,

7> self().
<0.35.0>
8> spawn_link (bad_module, wrong_function, []).
<0.43.0>

=ERROR REPORT==== 28-Aug-2014::23:54:10 ===
Error in process <0.43.0> with exit value: {undef,[{bad_module,wrong_function,[],[]}]}

** exception error: undefined function bad_module:wrong_function/0
9> self().
<0.45.0>

tak proces shellu skončí s výjimkou a je následně restartován, což se projeví novou hodnotou PID.

Pokud je proces slinkovaný s více procesy, tak chyba v jednom z nich se rozšíří na všechny ostatní. Takové chování může být zajímavé. Několik procesů společně pracuje na jedné úloze a pokud jeden zhavaruje, pak nedává smysl, aby ostatní pokračovaly a není třeba mít nějakého managera, co sleduje chyby a ukončuje ostatní procesy, pokud k nim dojde. Pokud každý proces k něčemu patří, výrazně se tím omezuje riziko, že po nějakém problému zůstanou v systému osiřelé procesy čekající na zprávy, co nikdy nepřijdou, a nelze s nimi dělat nic jiného než je ručně vyhledávat a terminovat.

Ale jen to, že proces je ukončen, když zhavaruje s ním slinkovaný proces, se může zdát poněkud málo.

Je to málo. Proces může „přežít“ pád s ním slinkovaného procesu pomocí nastavení příznaku trap_exit funkcí process_flag/2. Pokud je tento příznak nastaven (ve výchozím stavu nastavený není), tak pád slinkovaného procesu způsobí, že runtime prostředí zašle procesu zprávu, místo aby jej ukončilo. Zároveň se tím zastaví šíření chyby na případné další procesy, se kterými je tento proces spojen (a co nejsou spojeny s havarovaným procesem přímo).

9> self().
<0.45.0>
10> process_flag(trap_exit, true).
false
11> spawn_link (bad_module, wrong_function, []).
<0.48.0>
12>
=ERROR REPORT==== 29-Aug-2014::05:26:50 ===
Error in process <0.48.0> with exit value: {undef,[{bad_module,wrong_function,[],[]}]}

12> self().
<0.45.0>
13> flush().
Shell got {'EXIT',<0.48.0>,{undef,[{bad_module,wrong_function,[],[]}]}}
ok

Zde se opět spustil proces slinkovaný se shellem, co skončil s chybou. Proces shellu se nepřerušil (PID se nezměnil) a v příchozích zprávách se objevila informace, že proces <0.48.0> zhavaroval a proč.

Příznak trap_exit je vlastnost procesu a lze ji kdykoliv zapínat a vypínat. Bývá zvykem ji nastavit (zapnout) jen jednou, a to na začátku při startu procesu. Kvůli přehlednosti programu, aby bylo vždy jasné, jak se proces chová. Kromě toho častým přepínáním tohoto příznaku by mohly vzniknout podmínky vhodné pro vznik chyby souběhu.

V příkladech z minule se v modulu s kódem procesu vytvářela funkce start, kterou se proces spouštěl. Pokud je třeba spustit proces tak, aby byl od začátku slinkovaný se spouštějícím procesem (někde v hloubi se použije spawn_link místo spawn), bývá zvykem pro to vytvořit funkci s názvem start_link.

Monitorování procesů

Stejně jako linkování slouží monitorování k tomu zjistit, zda nějaký proces skončil. Hlavní rozdíl je v tom že se jedna o jednosměrnou vazbu. Proces požádá funkcí motinor/2 o sledování nějakého jiného procesu a pokud sledovaný proces skončí (ať už s chybou nebo spořádaně), dostane o tom sledující proces zprávu. Sledovaného procesu se to nijak netýká, to, že je monitorován, se nedozví. Stejně tak se nedozví, že případný monitorující proces skončil. Monitorování lze přerušit funkcí demonitor/1.

V následujícím příkladu se používá funkce spawn_monitor/3, což je zkratka pro volání funkcí spawnmonitor jako jedna atomická operace.

5> spawn_monitor(io, format, ["Hello world~n"]).
Hello world
{<0.41.0>,#Ref<0.0.0.49>}
6> flush().
Shell got {'DOWN',#Ref<0.0.0.49>,process,<0.41.0>,normal}
ok
7> spawn_monitor(bad_module, wrong_function, []).
{<0.44.0>,#Ref<0.0.0.61>}
8>
=ERROR REPORT==== 29-Aug-2014::06:05:48 ===
Error in process <0.44.0> with exit value: {undef,[{bad_module,wrong_function,[],[]}]}


8> flush().
Shell got {'DOWN',#Ref<0.0.0.61>,process,<0.44.0>,
                  {undef,[{bad_module,wrong_function,[],[]}]}}
ok

V první části se spustil proces, který vypsal text a spořádaně skončil. V návratových hodnotách je reference na monitor (#Ref<0.0.0.49>). Tu je třeba si schovat pro případné rušení monitoru. Spuštěný proces ihned skončil a v příchozích zprávách (vybráno příkazem shellu flush()) je o tom zpráva. V ní je reference na monitor, který ji způsobil, PID procesu, co skončil a to, že skončil spořádaně (atom normal na konci).

V druhé části příkladu se spouští a monitoruje proces co zhavaruje (neexistující startovací funkce). V příchozí zprávě je uvedeno, že důvodem ukončení bylo volání nedefinované funkce.

Ukončením monitorovaného procesu končí pochopitelně i jeho monitorování (netřeba volat funkci demonitor).

Linkování procesů je určeno spíše pro trvalé spojení (při startu se procesy spojí a tak to zůstane), zatímco monitorování se používá pro dočasné sledování.

Příklad: Proces získá zámek, manager zámku jej začne sledovat a po uvolnění zámku jej sledovat přestane. Pokud by monitorovaný proces skončil, aniž by zámek uvolnil, tak se to manager zámku dozví, zapíše varování do logu a zámek uvolní.

Makra

Při kompilaci zdrojových kódu lze využívat makra. Myšlenka je stejná jako v jiných jazycích. Nějaká textová konstanta se před kompilací expanduje na hodnotu, kterou zastupuje a takto upravený zdrojový kód se začne kompilovat.

Makro se definuje atributem modulu -define a vyvolává jménem makra uvozeným otazníkem.

-define(FOO, 42).
-define(BAR, 43).

test1 () -> ?FOO + ?BAR.  % vrati 85

Makra mohou mít parametry.

-define(DEBUG(Msg, Params), io:format("[debug] " ++ Msg ++ "~n", Params))

test2 ->
   ?DEBUG ("text1 form PID ~w", [self()]),
   ok.

Lze testovat existenci makra a podle toho větvit kód IFfy pomocí -ifdef(MACRO_NAME).

-ifdef(FOO).

...

-else.

...

-endif.

Pro testování neexistence makra, je určeno -ifndef(MACRO_NAME). Makro lze zrušit pomocí -undef(MACRO_NAME).

Makro lze definovat zvenku jako parametr při kompilaci přepínačem d.

c(some_modue, [{d, use_debug}]).

Existuje několik předdefinovaých maker.

  • ?MODULE – atom s názvem modulu.
  • ?MODULE_STRING – string s názvem modulu.
  • ?FILE – zdrojový soubor, který byl použit.
  • ?LINE – řádek zdrojového souboru, který byl použit.

S parametrizovanými makry se lze setkat při porovnávání vzorů. V nich je možno za klíčovým slovem when psát porovnávací výrazy a různě je spojovat logickými spojkami. Povoleno jen několik vestavěných funkcí (testování datového typu a pod.). Vlastní funkce nelze používat, i kdyby se skládaly jen z povolených výrazů. To je mrzuté. Pokud by bylo třeba nějaký složitý výraz používat na více místech, lze nemožnost napsat si na to funkci vyřešit použitím parametrizovaného makra.

S preprocesorem, který se stará o makra souvisí include soubory. Ty se vkládají pomocí atributu modulu -include.

-include("debug_macros.hrl").

Do include souborů se obvykle umisťují sdílená makra a definice na struktur (o strukturách jindy). Include souborům se dává přípona .hrl.

Obecný proces typu server

Obecný proces typu server funguje tak, že se spustí a při startu se řekne modul co obsahuje funkce, které se mají volat v jednotlivých fázích života procesu. Obecný kód zajišťuje cyklus pro čtení zpráv, meziprocesový komunikační protokol (strukturu zpráv) a pod. Uživatelský modul obsahuje funkce na inicializaci, různé formy obsluhy událostí a terminaci. Pro vytvoření procesu typu server se používá modul gen_server.

Aby nějaký modul mohl být použit v obecném serveru, musí exportovat předepsanou skupinu funkcí. Něco jako rozhraní (interface) z jiných jazyků. To se zajišťuje pomocí atributu modulu -behaviour. Parametrem atributu je rozhraní. Což je sada funkcí, které se modul zavazuje exportovat. Pokud tak neučiní, je při kompilaci vypsáno varování, že některé funkce chybí.

Příklad – modul co neexportuje nic, co by měl

%%
%% dummy_server.erl
%% modul kde neni exportovano co je nutne
%%
-module (dummy_server).

-behaviour(gen_server).

Kompilace modulu se povede, ale obsahuje sérii ošklivých varování.

dummy_server.erl:7: Warning: undefined callback function code_change/3 (behaviour 'gen_server')
dummy_server.erl:7: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')
dummy_server.erl:7: Warning: undefined callback function handle_cast/2 (behaviour 'gen_server')
dummy_server.erl:7: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')
dummy_server.erl:7: Warning: undefined callback function init/1 (behaviour 'gen_server')
dummy_server.erl:7: Warning: undefined callback function terminate/2 (behaviour 'gen_server')
{ok,dummy_server}

Funkce je třeba do modulu napsat. Pokud je jisté, že v dané aplikaci některá z nich není potřeba, je nutné ji uvést a uvnitř vyvolat výjimku (nebo nedělat nic), aby se tato varování nezobrazovala.

Obecný proces typu gen_server je se spouští funkcí gen_server:start nebo gen_server:start_link (pokud má být slinkovaný se startujícím procesem). Tím se spustí nový proces, který mimo jiné zavolá callback init/1, jehož úkolem je z inicializačního parametru vytvořit prvotní stavová data. Procesu lze zasílat synchronní události (požadavek s odpovědí) funkcí gen_server:call. Což vyvolá v procesu zavolání callbacku handle_call. Obdobně se zasílají asynchronní události (požadavek bez odpovědi) funkcí gen_server:cast, která se způsobí zavolání callbacku handle_cast. Funkce handle_info se volá pokud se ve frontě příchozích zpráv objeví cokoliv mimo zprávičkový protokol obecného procesu. Např. zprávy od runtime prostředí, co chodí v souvislosti s monitorováním jiných procesů. Při ukončení se volá funkce terminate.

Poslední callback code_change, se volá během výměny kódů za běhu. Při upgrade modulu se může stát, že stavová data je třeba změnit. Např. v nich přibude nějaká nová hodnota. Úkolem tohoto callbacku je učinit nutné operace, které s sebou změna kódu nese. Pokud se dostanete do situace, že budete tyto problémy řešit (plánovaný upgrade software na živých systémech za běhu), je to důležitá funkce, ale jinak se zde nedělá nic.

Příklad – počítadlo výskytu událostí z předminule zkusíme napsat s použitím obecného serveru.

%%
%% priklad obecneho serveru - pocitadlo udalosti
%% soubor: counter_proc2.erl
%%
-module (counter_proc2).
-behaviour(gen_server).

% O & M funlce + uzivatelske API
-export([start_link/0,start_link/1, stop/0, increment/0, reset/0, get_counter/0]).

% callbacky pro gen_server
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%%
%% O & M funkce
%% funkce pro vypinani a zapinani procesu
%%

% start - zkratka pro start s vychozi hodnotou
start_link() ->
  start_link (infinity).

% start - volani funkce gen_server:start_link
% start jako proces s registrovanym jmenem counter_proc2 (vyznam prvniho parametru)
% modul s callbacky je counter_proc2, parametr pro inicializaci je seznam s jednim prvke [MaxValue]
% a posledni parametr jsou options pro proces gen_server, ktere jsou v tomto pripade prazdny
% seznam (viz dokumentace co zde muze byt)
start_link(MaxValue) ->
  gen_server:start_link({local, counter_proc2}, counter_proc2, [MaxValue], []).

% vypnuti procesu - asynchronni zaslani atomu stop
stop() ->
  gen_server:cast (counter_proc2, stop).

%%
%% uzivatelelske API
%%

% proces bezi pod registrovanym jmenem counter_proc2, na tuto adresu
% se zasilaji veskere pozadavky. Jinak by bylo treba mit ve funkcich argument
% s PIDem procesu, kam je posilat.

% zvetseni citace - zaslani atomu increment
increment () ->
  gen_server:cast (counter_proc2, increment).

% reset counteru - zaslani atomu reset
reset() ->
  gen_server:cast (counter_proc2, reset).

% ziskani hodnoty - synchronni volani (call) zaslanim atomu get_counter
get_counter () ->
  gen_server:call (counter_proc2, get_counter).

%%
%% gen_server callbacky
%%

% inicializace - vstupni parametr musi korespondovat s inicializacni hodnotou (treti parametr)
% volani funkce gen_server:start_link ve funkci start_link/1
init ([MaxValue]) ->
  State = {0, MaxValue},
  {ok, State}.

% obsluha asynchronnich pozadavku (cast) - zde musi byt
% nejakym zpusobem (porovnanavanim vzoru) popsany moznosti se kterymi
% je volana funkce gen_server:cast

handle_cast (increment, {Counter, MaxValue}) ->
  NewConter = increment_counter (Counter, MaxValue), % nova hodnota citace
  NewState = {NewConter, MaxValue},                  % modifikace stavovych dat
  {noreply, NewState};

handle_cast (reset, {_Counter, MaxValue}) ->
  NewState = {0, MaxValue},    % modifikace stavovych dat
  {noreply, NewState};

% Pozadavek na zastaveni se pretlumoci obecnemeu serveru navratovou hodnotou ktera
% znamena stop. Druhy parametr (atom normal) ma vyznam duvodu zastaveni (Reason) a prenasi
% se do prvniho parametru ve funkci terminate. Pokud je zde neco jineho nez normal, projevi
% se to v logu jako chyba.
handle_cast (stop, State) ->
  {stop, normal, State}.


% obsluha synchronnich pozadavku (call). Opet to musi odpovidat kombinacim parametru
% se kterymi se vola funkce gen_server:call (v tomto pripade jedna).
handle_call (get_counter, _From, {Counter, _MaxValue} = State) ->
  Reply = {ok, Counter},  % sestaveni odpovedi na pozadavek
  {reply, Reply, State}.  % navratova hodnota znamenajici ze se ma odeslat odpoved

% reakce na zpravu "mimo protokol" ve fronte
handle_info (Msg, State) ->
  io:format ("unexpected message: ~w~n", [Msg]),
  {noreply, State}.

% ukonceni procesu, nic se zde nedela, na navratove
% hodnote nezalezi.
terminate (_Reason, _State) ->
  ok.

% zmena kodu - nic se zde nedela, gen_server to vyzaduje.
% Da se to chapat jako potvrzeni, ze zmena kodu procesu nevadi.
code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%%
%% lokalni pomocne funkce
%%
increment_counter(Counter, MaxValue) when ((MaxValue == infinity) or (Counter < MaxValue)) ->
    Counter + 1;
increment_counter(_Counter, _MaxValue) -> 0.

V kódu jsou komentáře. Takže spíš obecněji. Uživatelské funkce zasílají voláním funkcí gen_server:callgen_server:cast různá data (v tomto případě zcela náhodou jednotlivé atomy, pokud je třeba do serveru dostat nějakou hodnotu, bývají to spíše n-tice). Každou variantu vstupních dat je třeba ošetřit ve funkcích handle_callhandle_cast. Bývá zvykem jednotlivé varianty vstupních dat ošetřit pomocí porovnávání vzorů na úrovni vstupních parametrů a tak mít pro každá vstupní data jednu variantu obsluhující funkce.

Při obsluze událostí se návratovou hodnotou příslušného handleru dává najevo serveru, co se má dál dělat. Zda se má pokračovat, odeslat odpověď (pokud to dává smysl), nebo se má proces zastavit. Existují i další možnosti a kombinace. Např. že se má proces hibernovat, což je velmi zhruba řečeno – uložit jej úsporněji do paměti, za cenu, že další přepnutí na něj (při příchozí zprávě) bude náročnější. To se může hodit při nějakém ladění výkonu. Co může přicházet v parametrech a co se může vracet po obsluze událostí, je popsáno v dokumentaci.

Modul funguje dle očekávání.

1> c(counter_proc2).
{ok,counter_proc2}
2> counter_proc2:start_link().
{ok,<0.40.0>}
3> counter_proc2:start_link().
{error,{already_started,<0.40.0>}}
4> counter_proc2:get_counter().
{ok,0}
5> counter_proc2:increment().
ok
6> counter_proc2:increment().
ok
7> counter_proc2:increment().
ok
8> counter_proc2:get_counter().
{ok,3}
9> counter_proc2:reset().
ok
10> counter_proc2:get_counter().
{ok,0}
11> counter_proc2:stop().
ok

Obecný proces typu FSM

Proces typu stavový automat (finite state machine – FSM) lze také vytvořit pomocí obecného modulu (gen_fsm), ke kterému je třeba dodat modul se sadou callbacků, které obsahují aplikační logiku (co má proces dělat).

Na rozdíl od serveru ve FSM existují stavy, mez kterými se přepíná. Reakce na události bývají v různých stavech jiné, takže obsluha událostí (nějaká funkce rozdělená na více variant porovnáváním vzorů ve vstupních datech) existuje pro každý stav zvlášť.

Občas se může hodit zaslat do FSM událost, která nezáleží na stavu procesu (např. požadavek na zastavení). Ty se odesílají zvláštními funkcemi a ve FSM na ně jsou určené handlery. Jedná se o analogii call a cast událostí v procesu typu gen_server.

Příklad – model zámku na dveřích, který je ovládán klávesnicí, na které je třeba zadat správný kód, aby se zámek otevřel. Klávesnice obsahuje tlačítko clear pro opravu (vymažou se doposud vložené číslice). Po zadání správného kódu se zámek na tři sekundy otevře. Zámek může být zablokován. Pak otevřít nelze. Blokování lze zrušit.

Zámek má tři stavy – locked, open a block. Vstupem je klávesnice, která zasílá zmáčknutá tlačítka a nějaké dálkové ovládání, které zámek zablokovává a odblokovává. Proces na vstupy reaguje přepínáním se mezi stavy a změnami stavových dat.

%%
%% prikald FSM - zamek u dveri ovladany klavesnici, na kterou
%% je potreba zadat spravny kod
%% soubor: code_lock.erl
%%
-module(code_lock).
-behaviour(gen_fsm). % zavazek bude exportovat jistou sadu funkci

-export([start_link/1, stop/0]).
-export([button/1, button_clear/0, block_start/0, block_stop/0]).
-export([locked/2, open/2, block/2]).
-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, code_change/4, terminate/3]).

%%
%% O & M funkce
%%

% start - parametr je kod zamku
% proces se startuje s registrovanym jmenem, kterym je jmeno modulu (makro ?MODULE)
% stejne tak modul ktery obsahuje callbacky (druhy parametr) je urcen timto makrem.
start_link(Code) ->
    gen_fsm:start_link({local, ?MODULE}, ?MODULE, Code, []).

% stop - zasle udalost (atom stop) pro vsechny stavy
% procesu ktery ma stejne jmeno jako je nazev modulu
stop() ->
  gen_fsm:send_all_state_event(?MODULE, stop).

%%
%% uzivatelske funkce
%%

% zaslani cislice z klavesnice
button(Digit) ->
    gen_fsm:send_event(?MODULE, {button, Digit}).

% zaslani tlacitka "oprava" z klavesnice
button_clear () ->
    gen_fsm:send_event(?MODULE, {button, clear}).

% prisel pokyn, ze se maji dvere zablokovat
block_start() ->
  gen_fsm:sync_send_all_state_event(?MODULE, block_start).

% prisel pokyn, ze dvere maji odblokovat (mohou nechat odemknout s pravym kodem)
block_stop() ->
  gen_fsm:sync_send_all_state_event(?MODULE, block_stop).


%%
%% gen_fsm callbacky
%%

% Inicializace - vstupem kod, ktery otevira zamek.
% Ve stavovych datech je buffer, kam se budou hromadit doposud
% zmacknute cislice. Protoze v Erlangu je vyhodne do listu hromadit
% prvky zleva, je kod zamku interne ulozeny v opacnem poradi, aby
% bylo snadne buffer a kod porovnavat. Pri inicializaci se vyvola
% zamknuti zamku a FSM se pusti ve stavu locked.
init(Code) ->
    CodeRev = lists:reverse(Code),
    do_lock(),
    {ok, locked, {[], CodeRev}}.

% obsluha stavu locked

% resetovaci tlacitko - vymaze se buffer s doposud ulozenymi znaky kodu
% ve stavovych datech
locked({button, clear}, {_Buffer, CodeRev}) ->
  {next_state, locked, {[], CodeRev}};

% reakce na tlacitko s cislici kodu
locked({button, Digit}, {Buffer, CodeRev}) ->
    case [Digit|Buffer] of
        % buffer s cislici je to co je v kodu, zamek se odemkne
        % a FSM se prepne do stavu open na 3000 milisekund
        CodeRev ->
            do_unlock(),
            {next_state, open, {[], CodeRev}, 3000};

        % buffer jeste nema delku kodu zamku, FSM zustane
        % v locked stavu a buffer ve stavovych datech ze zvetsi
        Incomplete when length(Incomplete) < length(CodeRev) ->
            {next_state, locked, {Incomplete, CodeRev}};

        % chybny kod, zamek zapipa a zustane ve stavu locked,
        % buffer na cislice se vyprazdni
        _Wrong ->
            wrong_code(),
            {next_state, locked, {[], CodeRev}}
    end.

% rekace na udalosti ve stavu open

% po uplynuti casu prijde atom timeout
% po nem je treba zamek zamknout a prepnout FS do stavu locked
open(timeout, State) ->
  do_lock(),
  {next_state, locked, State};

% tlacitko nedela nic, nicmene vyskyt teto udalosti
% prerusi automaticky timeout, takze je treba se rozhodnout
% co dal. Zde se zamkne zamek a FSM se precvakne do stavu locked
open({button, _Digit}, State) ->
  button_ignored(),
  do_lock(),
  {next_state, locked, State}.

% obsluha udalosti, ktere nezavisi na stavu (odeslany funkcemi
% send_all_state_event nebo sync_send_all_state_event)

% udalost stop - zastavi se FSM pomoci navratove hodnoty
handle_event(stop, _StateName, StateData) ->
  {stop, normal, StateData}.

% udalost zablokovani zamku - podle aktualniho stavu (promenna StateName)
% se zamek pripadne zamkne a FSM se prepne do stavu block
handle_sync_event(block_start, _From, StateName, {_Buffer, CodeRev} = StateData) ->
  case StateName of
    open ->
      do_lock(),
      {reply, ok, block, {[], CodeRev}};
    locked ->
      {reply, ok, block, {[], CodeRev}};
    block ->
      {reply, {error, already_blocked}, block, StateData}
  end;

% odblokovani zamku - FSM ze stavu block prepne do locked, pokud nebylo
% ve stavu block, necha se jak bylo
handle_sync_event(block_stop, _From, StateName, StateData) ->
  case StateName of
    block ->
      {reply, ok, locked, StateData};
    _Other ->
      {reply, {error, not_blocked}, StateName, StateData}
  end.

% obsluha udlalosti ve stavu block - tlacitka se ignoruji
block({button, _Digit}, State) ->
  button_ignored(),
  {next_state, block, State}.

% reakce na obecne zpravy - nic se neceka, nic se nedela
handle_info(_Info, StateName, StateData) ->
  {next_state,StateName,StateData}.

% reakce na vymenu kodu - nic se nedela
code_change(_OldVsn, StateName, StateData, _Extra) ->
  {ok, StateName, StateData}.

% ukonceni procesu - zamkne se zamek, pokud je otevreny
% jinak se nedela nic
terminate(_Reason, open, _StateData) -> do_lock();
terminate(_Reason, _StateName, _StateData) -> ok.

%%
%% interni funknce
%%
button_ignored () ->
  io:format("beep - button ignored~n").

do_unlock () ->
  io:format("bzzz - unlocked~n").

do_lock() ->
  io:format("cvak - locked~n").

wrong_code() ->
  io:format("beep - wrong code~n").

Startování se provádí obdobně jako v obecném serveru. Funkce init kromě stavových dat a inicializačních operací musí říci, v jakém stavu se proces nachází na začátku. Události se z uživatelských funkcí zasílají pomocí send_event. Ty jsou obsluhovány ve funkcích locked, unlockedblock. Tyto funkce jsou asynchronní (synchronní varianta existuje, ale zde není použita). Události nezávislé na stavu jsou zasílány funkcemi send_all_state_eventsync_send_all_state_event. Jejich obsluha se děje v handlerech handle_eventhandle_sync_event. Chování procesu po obsluze události se řídí návratovou hodnotou příslušných handlerů funkcí. Např. po zadání správného kódu se zámek přepne do stavu unlocked na dobu 3 sekund. Pokud nepřijde žádná jiná událost, po uplynutí této doby přijde událost timeout, na kterou se dá nějak zareagovat. Podrobnosti v dokumentaci

Po zkompilování opět funguje vše dle očekávání.

1> c(code_lock).
{ok,code_lock}
2> code_lock:start_link([1,2,3]).
cvak - locked
{ok,<0.40.0>}
3> code_lock:button(1).
ok
4> code_lock:button(2).
ok
5> code_lock:button(3).
bzzz - unlocked
ok
cvak - locked
6> code_lock:block_start().
ok
7> code_lock:button(1).
beep - button ignored
ok
8> code_lock:block_stop().
ok
9> code_lock:button(1).
ok
10> code_lock:button(1).
ok
11> code_lock:button(1).
beep - wrong code
ok

Proces typu FSM vypadá jen jako mírná modifikace obecného procesu typu server. Liší se nutností udržovat stav a mohou mít oddělené handlery událostí pro různé stavy. Proč jsou řešeny zvlášť? Je to proto že Erlang vznikl v prostředí telekomunikací. Zde se lze s FSM setkat na každém kroku. Náramně hodí např. na udržování stavu spojení s nějakou protistranou. Linka se spojila, linka se rozpojila. Po spojení přišla inicializační zpráva, inicializace se povedla / nepovedla, spojení se přeruší, je třeba na to reagovat podle toho, co bylo v předchozích krocích (v jakém bylo spojení stavu).

Nebo stav pevné linky. Klasická pevná linka je analogové zařízení bez chytrosti připojené „zvonkovými dráty“ do telefonní ústředny. Telefon posílá ústředně události (zvedlo se sluchátko, navolila se číslice …). Ústředna může posílat události telefonu (příchozí hovor – začni vyzvánět). Podle toho, co se děje s pevnou linkou, se pak se v ústředně se „něco“ překlápí do různých stavů. Kdysi to byla paní, co přemisťovala konektor linky do různých zásuvek, pak obvody postavené z relátek, později digitální hardwarová zařízení. V případě Erlangu to mohou být procesy typu FSM.

Takto zhruba vypadaly úlohy, které autoři Erlangu řešili, když ho vynalézali. Hardware, do kterého vedly pevné linky. Události, které na jednotlivých zásuvkách vznikaly, byly (např. po sériové lince) posílány do počítače, který je zpracovával. Zásuvek může být hodně. Pokud se na nějaké zásuvce vyskytne chyba, bylo by škoda, kdyby se kvůli tomu musely restartovat ostatní. Pokud se software v počítači aktualizuje, bylo by dobré, kdyby nebylo kvůli tomu nutné resetovat všechny zásuvky a přerušit v nich běžící hovory.

Obecný event handler

Zbývají event handlery. Ty jsou obslouženy modulem gen_event. Proti serveru a FSM procesu je rozdíl v tom, že proces pro příjem zpráv (event manager) je obecný kód a netřeba je modifikovat nějakými callbacky. Event manager příjmá zprávy (eventy) a sám o sobě s nimi nic nedělá. To mají na starosti event handlery. Modul event handleru se již vytváří tak, že obsahuje předepsané callbacky (behaviour gen_event). Jednotlivé event handlery se pak registrují do event manageru. Podrobně to bylo popsáno minule.

Příklad: event handler co počítá, kolikrát přišla událost do event manageru.

%%
%% event handler, ktery pocita kolik udalosti dostale event manager
%% kam je zaregistrovan
%% soubor: handler_counter.erl
%%
-module (handler_counter).
-behaviour(gen_event).

-export([start/1, stop/1, get_count/1, reset/1]).
-export([init/1, handle_call/2, handle_event/2, handle_info/2, code_change/3, terminate/2]).

%%
%% Uzivatelske funkce
%%

% registrace handleru
start(EventMgrRef) ->
  gen_event:add_handler (EventMgrRef, ?MODULE, []).

% smazani handleru
stop(EventMgrRef) ->
  gen_event:delete_handler(EventMgrRef, ?MODULE, []).

% zjisteni poctu udalosti ze stavovych dat
get_count(EventMgrRef) ->
  gen_event:call(EventMgrRef, ?MODULE, get_count).

% reset poctu udalosti ze stavovych dat
reset(EventMgrRef) ->
  gen_event:call(EventMgrRef, ?MODULE, reset).

%%
%% gen_event callbacky
%%

% inicializace - stavova data jsou 0
init(_) -> {ok, 0}.

% udalost v event manageru - zvyseni pocitadla
handle_event (_Event, N) -> {ok, N+1}.

% reakce na udalosti poslane tomuto handleru pres gen_event:call
handle_call (get_count, N) -> {ok, {ok, N}, N}; % vrati se hodnota citace v n-tici {ok, N}
handle_call (reset, _N) -> {ok, ok, 0}.         % vrati se ok, citac ve stavovych datech se nastavi na nulu

% pri obecne zprave mimo protokol (co ji dostal event manager), zmene
% kodu evet handleru ani pri terminaci se ni nedela.
handle_info(_Info, State) -> {ok, State}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
terminate(_Arg, _State) -> ok.

Kromě očekávaných funkcí jak je init, code_change, handle_infoterminate je ze vlastní obsluha dat co přijal event manager ve funkci handle_event. Dále je pak možno číst nebo měnit stavová data event handleru voláním funkce gen_event:call, na kterou se reaguje callbackem handle_call. Návratovými hodnotami handleru se dá ovlivňovat chování procesu (v tomto případě event manageru). Podrobnosti opět v dokumentaci.

Použití:

UX DAy - tip 2

1> c(handler_counter).
{ok,handler_counter}
2> {ok, MyManager} = gen_event:start_link().
{ok,<0.40.0>}
3> handler_counter:start(MyManager).
ok
4> gen_event:notify(MyManager, m1).
ok
5> gen_event:notify(MyManager, m2).
ok
6> gen_event:notify(MyManager, m3).
ok
7> handler_counter:get_count(MyManager).
{ok,3}
8> handler_counter:reset(MyManager).
ok
9> handler_counter:get_count(MyManager).
{ok,0}
10> handler_counter:stop(MyManager).
ok
11> handler_counter:get_count(MyManager).
{error,bad_module}

Moduly gen_server, gen_fsmgen_event jsou součástí OTP knihovny (Open Telecom Platform). Jedná se o součást distribuce Erlangu. Historicky tato knihovna vznikla v době, kdy začaly první pokusy využít Erlang pro vývoj větších projektů. Pokrývá problematiku vytváření procesů a komunikace mezi nimi (což bylo dnes naznačeno), jejich následné spojování do větších celků, řešení výpadků (supervize) procesů (spadlo to, pustíme to znovu a restartujeme závislé procesy), logování a jiné.

Příště se podíváme trochu do jiné oblasti. Na práci s větším množstvím dat (ETS tabulky), k tomu budou potřeba struktury a jak to celé obsloužit dnes popsanými procesy, ke kterým si napíšeme tzv. aplikaci s využitím dalších částí OTP knihovny.

Byl pro vás článek přínosný?