Az ERLANG programozási nyelv

Viselkedésminták

Viselkedésabsztrakció

Egy Erlangban írt szoftverben sok folyamatnak hasonló lehet a szerkezete, viselkedése. Ezeknek a mintáknak a formalizálását nevezzük behaviour-nak (viselkedésmintának). Az alapötlet, hogy a folyamathoz tartozó kódot válasszuk szét generikus (behaviour módul) illetve specifikus (callback modul) részre. A gyakori tervminták implementációját az Erlang/OTP csomag tartalmazza, a programozónak csak egy callback modult kell implementálni a specifikus viselkedés meghatározásához. Amennyiben egyes szükséges callback függvények nem kerülnek implementálásra, a fordító figyelmeztet, de nem áll le hibával. Az Erlang fejlesztési filozófiájához illeszkedően csak futás közben kapunk hibát, akkor amikor az adott függvény hívásra kerülne. A callback módul forrásfájljában a behaviour direktívával kell hivatkozni a megvalósított viselkedésmintá(k)ra. Pl.: -behaviour(gen_server).

A viselkedésminták megértéséhez learnyousomeerlang.com címet javasolt felkeresni, ami az egyik legszemléletesebben magyarázó, Erlang-ot tanító weboldalak egyike. Technikai jellegű információkért, mint például, milyen paraméterekkel kell meghívni a start_link/1 függvényt, illetve általános tervezési tanácsokért a linken elérhető címet célszerű felkeresni.

gen_server

A kliens-szerver interakció viselkedésminta implementációja. A kliens-szerver modell egy központi szerver processzból és tetszőleges számú kliensből áll. Leggyakoribb alkalmazása az erőforrás-kezelés, amikor több kliens osztozik egy közös erőforráson. Ekkor a szerver felelős ennek az erőforrásnak a kezeléséért. A generikus szerver mind szinkron és aszinkron hívásokkal elérhető, illetve belső állapottal rendelkezik.

Megjegyzés: mivel az Erlang nem tartalmaz objektum orientált lehetőségeket, elterjedt gyakorlat, hogy szükség esetén generikus szerverekkel szimulálják az objektumok viselkedését. Ez gyakori C++ függvénykönyvtárak portolásakor Erlang platformra, pl. wxWdigets – wxErlang.

A viselkedésminta által rendelkezésre bocsátott függvények:
start_link(Module, Args, Options) -> Result start_link(ServerName, Module, Args, Options) -> Result

Létrehoz egy generikus szerver processzt a supervisor fa részeként. A függvényt közvetve vagy közvetlenül a supervisor hívja..

start(Module, Args, Options) -> Result start(ServerName, Module, Args, Options) -> Result

Létrehoz egy generikus szerver processzt.

call(ServerRef, Request) -> Reply call(ServerRef, Request, Timeout) -> Reply

Interfészfüggvény szinkron (blokkoló) hívásokhoz. Visszatérési értéke a szerver válasza.

(Érdekesség: Az Erlang/OTP fejlesztőknek a 14A kiadásra sikerült elérni, hogy a függvény műveletigény konstans O(1) több millió hívás esetén is.)

multi_call(Name, Request) -> Result multi_call(Nodes, Name, Request) -> Result multi_call(Nodes, Name, Request, Timeout) -> Result

Több generikus szerver szimultán szinkron hívása. A különböző szerverektől érkező válaszokat egy listában adja vissza.

cast(ServerRef, Request) -> ok

Interfészfüggvény aszinkron (nem blokkoló) hívásokhoz.

abcast(Name, Request) -> abcast abcast(Nodes, Name, Request) -> abcast

Aszinkron üzenet szimultán küldése több szervernek.

reply(Client, Reply) -> Result

Explicit válasz küldése a kliensnek. A függvény akkor használandó, ha a válasz nem adható meg a handle_call/3 callback függvény visszatérési értékeként.

enter_loop(Module, Options, State)
enter_loop(Module, Options, State, ServerName)
enter_loop(Module, Options, State, Timeout)
enter_loop(Module, Options, State, ServerName, Timeout)

Egy létező processzből generikus szervert processzt készít. A függvény nem tér vissza, hanem átadja a vezérlést a generikus szerver loopjának. Csak olyan processzek esetén használható, amelyek a a proc_lib modul valamely megfelelő függvényével lettek létrehozva.

Definiálandó callback függvények:

(A Module helyére az adott callback modul neve értendő.)

Module:init(Args) -> Result

A processz létrehozásakor meghívódó függvény. Argumetumait a start függvénytől kapja, visszatérési értéke a szerver kezdeti állapota.

Megjegyzés: OOP szimulációjakor ez tulajdonképpen a konstruktor.

Figyelem: enter_loop használata esetén a függvény NEM hívódik meg!

Module:handle_call(Request, From, State) -> Result

Szinkron hívások kezelése. A különböző hívások kezelése a callback függvény különböző klózaiban történik.

Module:handle_cast(Request, State) -> Result

Aszinkron hívások kezelése. A különböző hívások kezelése a callback függvény különböző klózaiban történik.

Module:handle_info(Info, State) -> Result

Nem behaviour specifikus üzenetek kezelése. Ha a szerver processznek nem az interfészfüggvények használatával küldenek processznek (tehát ! haszálatával), akkor ez a függvény kezeli az üzenetet.

Ökölszabály: NE támaszkodjunk rá, csak hibakezelésre használjuk. Sok kellemetlen pillanattól szabadíthatjuk meg a kódunkat utólag módosító, debuggoló fejlesztőket.

Module:terminate(Reason, State)

A processz terminálásakor meghívódó függvény.

Megjegyzés: OOP szimulációjakor ez tulajdonképpen a destruktor.

Module:code_change(OldVsn, State, Extra) -> {ok, NewState}

Kódfrissítéskor hívódik meg. Akkor használandó ha a generikus szerver belső állapotát módosítani kell a programkód cseréjekor.

Module:format_status(Opt, [PDict, State]) -> Status

A belső állapotinformáció egyéni formázása adható meg a függvénnyel.

Megjegyzés: OOP szimulációjakor ez tulajdonképpen a toString metódus.

gen_fsm

A véges állapotautomata viselkedésminta implementációja. Az állapotautomatákat leggyakrabban protokollok implementációra használják. Elméleti definíciót lásd a Wikipedián.

A viselkedésminta által rendelkezésre bocsátott függvények:
start_link(Module, Args, Options) -> Result start_link(FsmName, Module, Args, Options) -> Result

Létrehoz egy FSM processzt a supervisor fa részeként. A függvényt közvetve vagy közvetlenül a supervisor hívja.

start(Module, Args, Options) -> Result start(FsmName, Module, Args, Options) -> Result

Létrehoz egy FSM processzt.

send_event(FsmRef, Event) -> ok

Esemény aszinkron küldése az FSM processznek.

send_all_state_event(FsmRef, Event) -> ok

Állapotfüggetlen esemény aszinkron küldése az FSM processznek.

sync_send_event(FsmRef, Event) -> Reply sync_send_event(FsmRef, Event, Timeout) -> Reply

Esemény szinkron küldése az FSM processznek.

sync_send_all_state_event(FsmRef, Event) -> Reply sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply

Állapotfüggetlen esemény szinkron küldése az FSM processznek.

reply(Caller, Reply) -> true

Explicit válasz küldése a kliensnek. A függvény akkor használandó, ha a válasz nem adható meg a Module:State/3 vagy a Module:handle_sync_event/4 callback függvény visszatérési értékeként.

send_event_after(Time, Event) -> Ref

Késleltetett esemény küldése az FSM-nek. A visszatérési érték egy referencia, amivel meg lehet szakítani az időzítőt ha szükséges.

start_timer(Time, Msg) -> Ref

Késleltetett timeout esemény küldése az FSM-nek. A visszatérési érték egy referencia, amivel meg lehet szakítani az időzítőt ha szükséges.

cancel_timer(Ref) -> RemainingTime | false

A referencia által meghatározott időzítő megszakítása.

enter_loop(Module, Options, StateName, StateData)
enter_loop(Module, Options, StateName, StateData, FsmName)
enter_loop(Module, Options, StateName, StateData, Timeout)
enter_loop(Module, Options, StateName, StateData, FsmName, Timeout)

Egy létező processzből FSM processzt készít. A függvény nem tér vissza, hanem átadja a vezérlást az FSM loopjának. Csak olyan processzek esetén használható, amelyek a a proc_lib módul valamely megfelelő függvényével lettek létrehozva.

Definiálandó callback függvények:
Module:init(Args) -> Result

A processz létrehozásakor meghívódó függvény. Argumentumait a start függvénytől kapja, visszatérési értéke a szerver kezdeti állapota.

Figyelem: enter_loop használata esetén a függvény NEM hívódik meg!
Module:StateName(Event, StateData) -> Result

Aszinkron események kezelése egy adott állapotban, ahol StateName az állapot neve. Timeout esetén is ez a függvény hívódik meg.

Module:handle_event(Event, StateName, StateData) -> Result

Állapotfüggetlen események kezelése.

Module:StateName(Event, From, StateData) -> Result

Szinkron események kezelése az adott állapotban.

Module:handle_sync_event(Event, From, StateName, StateData) -> Result

Állapotfüggetlen, szinkron események kezelése.

Module:handle_info(Info, StateName, StateData) -> Result

Nem szabványos üzenetek kezelése. Használatára a gen_server-nél leírtak vonatkoznak.

Module:terminate(Reason, StateName, StateData)

A processz terminálásakor meghívódó függvény.

Module:code_change(OldVsn, StateName, StateData, Extra) -> {ok, NextStateName, NewStateData}

Kódfrissítéskor hívódik meg. Akkor használandó ha a generikus szerver belső állapotát módosítani kell a programkód cseréjekor.

Module:format_status(Opt, [PDict, StateData]) -> Status

A belső állapotinformáció egyéni formázása adható meg a függvénnyel.

Supervisor

Az Erlangban előre definiált viselkedés minták közül az egyik legfontosabb és a gyakorlatban gyakran alkalmazott viselkedésminta a supervisor. A supervisor mintát arra használjuk, hogy folyamatokat felügyeljünk. A supervisor-t próbáljuk elképzelni nagy vonalakban úgy, mint egy folyamatot, aminek az a feladata, hogy indítson el és felügyeljen más supervisor-okat és dolgozókat. Abban az esetben, ha bármelyik általa felügyelt supervisor vagy dolgozó terminál, akkor indítsa újra azt. A dolgozókat képzeljük el, mint olyan folyamatokat, amik felelősek egy -egy munka valós elvégzéséért, akár úgy is, hogy végrehajtás közben terminálhatnak is. Az egy supervisor által felügyelt dolgozókat és supervisor-okat összefoglaló néven felügyeltnek vagy gyereknek is nevezünk a továbbiakban.

A supervisor-ok többszöri alkalmazásával és hierarchizálásával alkalmazásunkban „supervisor tree”-t építhetünk, ezzel téve még stabilabbá és hibatűrőbbé alkalmazásunkat. Fontos megjegyezni, hogy a supervisor tree -kre mindig igaz, hogy a dolgozó típusú folyamatok csak a levelekben jelenhetnek meg.

Újraindítási stratégiák

A supervisor-oknak megadhatjuk, hogy milyen stratégiát alkalmazzanak akkor, ha valamelyik felügyelt folyamatuk terminál.

A 'one_for_one' stratégiát akkor célszerű alkalmazni, mikor a felügyelt folyamatok egymástól függetlenek. Ha egy folyamat terminál, akkor a supervisor csak a terminált folyamatot indítja újra, a többi már elindított és még futó folyamatot nem befolyásolja. Példának képzeljük el azt, hogy van egy alkalmazásunk, mely minden klienst külön folyamatban szolgál ki. A kliensek száma és típusa az alkalmazás indításakor már ismert és rögzített. A klienseket kiszolgáló folyamatok lesznek a dolgozók és felettük lesz egy supervisor. Ha „A” klienset kiszolgáló folyamat terminál mielőtt elvégezné munkáját, akkor „A” kliensnek kell egy új dolgozó folyamatot indítanunk, de a többi kliens kiszolgálására ez nem szabad, hogy kihatással legyen.

A 'simply_one_for_one' stratégiát akkor célszerű alkalmazni, mikor a felügyelt folyamatok egymástól függetlenek. Ha egy folyamat terminál, akkor a supervisor csak a terminált folyamatot indítja újra, a többi már elindított és még futó folyamatot nem befolyásolja. Példának képzeljük el azt, hogy van egy alkalmazásunk, mely minden klienst külön folyamatban szolgál ki. A kliensek száma az alkalmazás indításakor nem ismert, típusuk viszont csak egymással megegyező lehet. A klienseket kiszolgáló folyamatok lesznek a dolgozók és felettük lesz egy supervisor. Ha érkezik egy új kliens, indítunk neki egy új dolgozót, ami külön folyamatban fogja kiszolgálni a klienst. Ha „A” klienset kiszolgáló folyamat terminál mielőtt elvégezné munkáját, akkor „A” kliensnek kell egy új dolgozó folyamatot indítanunk, de a többi kliens kiszolgálására ez nem szabad, hogy kihatással legyen.

Hasonlít a 'one_for_one' stratégiára? A 'simply_one_for_one' stratégia valóban nagyon hasonlít a 'one_for_one' stratégiához, annak egy speciális változata, de a két stratégia közti különbségeket a legfontosabb megérteni. A 'simply_one_for_one' stratégia csak egyfajta típusú dolgozó folyamatok indítását teszi lehetővé, a 'one_for_one' ezt nem korlátozza. A 'simply_one_for_one' stratégiát használva akár az indítás után is, dinamikusan hozhatunk létre új dolgozó folyamatokat, míg a 'one_for_one' stratégia bár nem várja el, hogy indításkor tudjuk statikusan definiálni az összes dolgozó folyamatot, de gyakorlatban ebben az esetben szoktuk használni. Azt is fontos megemlíteni, hogy nagy számosságú dolgozók mellett a 'simply_one_for_one' startégiával dolgozó supervisor-nak lesz kevésbé költséges új dolgozó indítása végrehajtási idő szempontjából.

A 'one_for_all' stratégiát akkor célszerű alkalmazni, mikor a felügyelt folyamatok egymással együtt-dolgozva oldják meg a feladatot. Ezekre a dolgozókra igaz az, hogy viselkedésük erősen függ saját és testvéreik állapotától. Ez esetben, ha az egyik dolgozó terminál, akkor nem csak a terminált folyamatot, hanem az összes testvérét is újra kell indítanunk, hogy a közöttük lévő konzisztenciát megtartsuk.

A 'rest_for_one' stratégia nagyon speciális stratégia, amit akkor célszerű alkalmazni, mikor a felügyelt folyamatok indítási időik mentén rendezve függenek mindenkitől, aki előttük került indításra, ezzel láncot alkotva. Ha így indítjuk el dolgozóinkat: A,B,C ezt a stratégiát alkalmazva, akkor A nem fog függeni senkitől, B csakis A-tól függ és C A-tól és B-től is függ. Ha valamelyik dolgozó terminál, akkor a terminált folyamat után indított összes folyamatot terminálni kell, majd az eredeti indítási sorrendben újraindítani azokat. Példának képzeljünk el egy olyan alkalmazást, mely minden beérkező forráskódot több lépésben, lexikális, szintaktikai, szemantikai elemzéssel (ezek lesznek a dolgozók) dolgoz fel. Ha a lépések sorrendje nem felcserélhető, és lépések egymásra építkeznek, akkor a 'rest_for_one' stratégiát javasolt választani.

Újraindítási próbálkozások korlátozása

A supervisor-oknak megadhatunk maximális próbálkozási darabszámot és időt. Ez azt jelenti, hogyha a megadott idő alatt megadott darabszámnyi újraindítás is sikertelennek bizonyult, akkor adja fel, termináltassa összes felügyeltjét, majd saját magát is. Ha a terminált supervisor nem egy supervisor tree része, akkor nem javasolt ezen limitek beállítása, ugyanis a supervisor úgy terminál, hogy sosem tér vissza, így blokkolja teljes alkalmazásunkat.

Child specifications

A Child specification segítségével megadjuk a supervisor-nak, hogy milyen elindítandó dolgozói vagy supervisor-ai vannak, velük szemben hogyan járjon el. Azaz, amíg az újraindítási stratégia globálisnak számított a felügyeltekre nézve, addig a Child specification-ökköl a felügyeltekre nézve lokális konfigurációkat tehetünk meg, melyek a következőek: ChildId, StartFunc, Restart, Shutdown, Type, Modules.

A ChildId egy belső, supervisor által használt azonosító, mely az adott gyereket azonosítja.

StartFunc egy tuple, ami azt a függvényt azonosítja, amit a supervisor-nak kell meghívnia a gyerek indításakor.

Restart értéke azt határozza meg, hogyan fog viselkedni a supervisor, ha a gyerek terminál. Ha a Restart értéke permanent, akkor a supervisor a terminált gyereket minden körülmények között újraindítja. Ha a Restart értéke temporary, akkor a supervisor nem kísérli meg a terminált gyerek újraindítását. Ha a Restart értéke transient, akkor a supervisor mindaddig újraindítja a terminált gyereket, míg az abnormálisan terminál.

A Shutdown értéke azt határozza meg, hogy a supervisor mennyi időt (ms) hagyjon a gyereknek terminálásra. Ha nem akarunk korlátot adni, az infinity atomot kell értékül adni. Ha semennyi időt nem akarunk hagyni, akkor a brutal_kill atomot adhatjuk meg értékül, mely esetben megszakíthatatlanul és azonnal elindítja a gyerek terminálását. Ha adtunk meg időkorlátot, és a terminálási idő már túllépte azt, akkor a gyerek terminálása ugyanolyan lesz, mint brutal_kill esetében. Fontos, még megjegyezni, hogy a 'simply_one_for_one' stratégiájú supervisor-okra nincs hatással ez a paraméter, terminálás esetén csak a supervisor terminál, a gyerekeket magára hagyja.

Type értéke azt határozza meg, hogy a gyerek dolgozó (worker) vagy supervisor (supervisor) legyen.

Modules értéke azon modulok listája, amikben a gyerek callback függvényei implementálásra kerültek. Ha ez nem ismert előre, akkor a dynamic atomot kell értékül adni.

Források:

Egyedi viselkedésminták

A programozó saját maga is definiálhat egyedi behaviourokat, ehhez pedig felhasználhat létező viselkedésmintákat is. Így gyakorlatilag az objektum orientált paradigmához hasonló öröklődést lehet használni. Ehhez mindössze implementálni kell a generikus kódot, a callback módul elvárt interfészét megadó behaviour_info/1 függvényt pedig exportálni kell.

Például:

behaviour_info(callbacks) -> [{init,1}, {handle, 1}, {sync, 2}]; behaviour_info(_Other) -> undefined.

Egyéb viselkedésminták, ld. dokumentáció.