Ping Ring del 5: Erlang


søndag 12. september 2010 Diverse prosjekter Ping Ring Erlang Actor model Samtidighet

Dette er del 5 i artikkelserien Ping Ring hvor jeg implementerer et og samme program i et utall ulike programmeringsspråk - for å se om det er noe å lære gjennom å gjøre det. Introduksjonen kan du lese her.

erlang_posterJeg har ikke kunnet komme med noen banebrytende oppdagelser sålangt i denne serien. Språkene jeg har valgt har vært for like. Jeg har egentlig implementert nøyaktig det samme programmet i alle språkene, og ikke funnet noen forskjeller av stor nok betydning til å utrope ett av språkene som mere egnet for oppgaven enn de andre. Det er på tide å forsøke noe radikalt anderledes…

Derfor har jeg nå valgt Erlang. Mens C#, Ruby og Boo er objektorienterte språk, er Erlang funksjonsbasert. Dette betyr blant annet at jeg bør finne en alternativ måte å løse shared state på – i de tidligere implementasjonene har listener- og alerter-trådene begge hatt tilgang til en variabel med tidspunktet for sist innkommende ping. Slikt fnyser man av i den funksjonelle verden.

Viktigere er det at Erlang er designet med fokus på samtidighet, og dessuten mye brukt i løsninger som baserer seg på TCP og andre lavnivå-protokoller. Jeg hadde derfor store forventninger til Erlang-implementasjonen av Ping-Ring.

Actor model

Et sentralt begrep i Erlang er Actor Model. Det språket i praksis støtter er at man oppretter isolerte prosesser (actors), og at prosessene deretter enkelt kan sende meldinger til hverandre. Hver prosess har en inbox i form av en kø, og leser og behandler meldinger asynkront i forhold til resten av programmet.

Figuren nedenfor er et gjensyn fra Ping Ring del 2, og illustrerer designet jeg brukte da jeg har implementert løsningene i C# / Ruby / Boo. Her ser du at jeg hadde to tråder som delte en variabel, og som opprettet egne tråder hver gang et ping skulle sendes.

pingring_alg_1

Neste figur illustrerer hvordan Erlang-løsningen skiller seg fra de foregående. Her har jeg tre selvstendige prosesser som hver holder på sin egen state, og som kommuniserer via meldinger. Både Listener og Alerter sender for eksempel melding til Pinger når de ønsker å få sendt en ping.

pingring_alg_2

Mange ser på actor model som en bedre/mere riktig form for objektorientering, og spår at vi kommer til å få se flere språk med innebygd støtte for denne modellen i årene som kommer.

Problemene

Erlang gir meg altså muligheten til å lage et ganske elegant design for å løse denne oppgaven. Problemet ligger derimot i detaljene. Før du tar en titt på koden vil jeg gå gjennom noen av de tingene jeg ikke liker med løsningen jeg har kommet opp med.

Merk at noen av mine erfaringer helt sikkert kan skyldes at jeg fortsatt er ganske fersk i Erlang. Har du mer kunnskap enn meg er det bare å sette meg på plass.

Tidligere har jeg jobbet endel med lister i Erlang, og til det er det veldig egnet. Da jeg nå skulle gjøre helt enkle ting som å konvertere en streng til en integer, og konkatinere to strenger, oppdaget jeg at språket ikke var like elegant på alle områder. Jeg savnet også if, og måtte ty til en bråkete switch-case når jeg skulle avgjøre om det skulle sendes en initiell ping ved oppstart (se linje 22 til 25).

TCP-grensesnittet var også mye mere komplisert (low level) enn det jeg hadde å forholde meg til i de andre språkene (linje 36 til 51). Jeg skulle for eksempel gjerne hatt en "read_to_end" funksjon; do_recv() er min variant av den. Generelt føltes det som om jeg måtte skrive mere kode enn hva som burde være nødvendig, nesten samme hva jeg holdt på med.

Det mest utfordrende var derimot å klare å kjøre programmet fra kommandolinjen. Til slutt måtte jeg legge til et par ekstra overloads av start-funksjonen får å klare å håndtere kommandolinje-parametrene riktig (linje 4 til 16).

Ok, nok klaging – her er kildekoden i sin helhet:

1 -module(ring_server).
2 -export([start/1, start/4]).
3
4 start(Args) -> 
5   % when started from command line args are collected in a list
6   [A1, A2, A3, A4] = Args,
7   start(A1, A2, A3, A4).
8
9 start(ThisPort, OtherPort, MaxDelay, InitialPing)
10 when is_list(ThisPort) -> 
11   % when started from command line some conversion is needed
12   start(
13     as_int(ThisPort),
14     as_int(OtherPort),
15     as_int(MaxDelay),
16     InitialPing);
17
18 start(ThisPort, OtherPort, MaxDelay, InitialPing) ->
19   io:format("** Erlang Ring Server (~p)~n", [ThisPort]),
20   Message = lists:concat(["Ping from ", ThisPort]),
21   Pinger = spawn(fun() -> ping_sender(OtherPort, Message) end),
22   case InitialPing of 
23     "true" -> Pinger ! ping;
24     _ -> no_action_needed 
25   end,
26   Watcher = spawn(fun() -> ping_watcher(Pinger, MaxDelay, 0) end),
27   spawn(fun() -> ping_listener(ThisPort, Pinger, Watcher) end),
28   timer:sleep(infinity). % let the actors do the job forever
29
30 as_int(String) -> % simplified string to integer conversion
31   {Int, _Rest} = string:to_integer(String),
32   Int.
33
34 %% --- Actor 1 : Ping listener ---
35
36 ping_listener(ThisPort, Pinger, Watcher) ->
37   {ok, LSock} = gen_tcp:listen(ThisPort, [binary, {packet, 0}, {active, false}]),
38   ping_listener_loop(LSock, Pinger, Watcher).
39  
40 ping_listener_loop(LSock, Pinger, Watcher) ->
41   {ok, Sock} = gen_tcp:accept(LSock),
42   {ok, Bin} = do_recv(Sock, []),
43   ok = gen_tcp:close(Sock),
44   process_incoming_ping(Bin, Pinger, Watcher),
45   ping_listener_loop(LSock, Pinger, Watcher).
46  
47 do_recv(Sock, Bs) ->
48   case gen_tcp:recv(Sock, 0) of
49     {ok, B} -> do_recv(Sock, [Bs, B]);
50     {error, closed} -> {ok, list_to_binary(Bs)}
51   end.
52
53 process_incoming_ping(Message, Pinger, Watcher) ->
54   io:format("Received ~p~n", [Message]),
55   Watcher ! ping, % tell the watcher about it
56   Pinger ! ping. % and the pinger, so he can forward it
57
58 %% --- Actor 2 : Ping sender ---
59
60 ping_sender(Port, Message) ->
61   receive ping ->
62       timer:sleep(1000),
63       case gen_tcp:connect("localhost", Port, [binary, {packet, 0}]) of
64         {ok, Sock} ->
65           ok = gen_tcp:send(Sock, Message),         
66           ok = gen_tcp:close(Sock);
67         {error, _} ->
68           io:format("*** Failed sending ping~n")
69       end,
70       ping_sender(Port, Message) % loop to wait for more ping requests
71   end.
72
73 %% --- Actor 3 : Missing pings watcher
74
75 ping_watcher(Pinger, MaxDelay, DelayCount) ->
76   receive ping -> 
77       ping_watcher(Pinger, MaxDelay, 0) % all is well, watch again
78   after MaxDelay * 1000 ->
79       NewDelayCount = DelayCount + 1,
80       io:format("*** ALERT, RING BROKEN! No ping in ~p seconds.~n",
81         [MaxDelay * NewDelayCount]),
82       Pinger ! ping, % try to wake up ping ring
83       ping_watcher(Pinger, MaxDelay, NewDelayCount) % watch again
84   end.

Det jeg derimot følte ble veldig bra var det jeg kalte actor 2 og 3 (ping sender, linje 60, og ping watcher, linje 75). Watcheren ble spesielt elegant synes jeg. Her utnytter jeg Erlangs evne til å fyre av et event når jeg har ventet på en melding i x antall millisekunder. Jeg venter nemlig på pings på linje 76, og når ping kommer repeterer jeg bare (halerekursjon). Etter så mange sekunder som det er tillatt å gå uten pings (linje 78: after MaxDeley * 1000) aktiverer jeg derimot alarmen.

På den måten trenger jeg heller ikke å gjøre beregninger basert på klokketid, antall sekunder siden sist ping er jo MaxDelay ganger DelayCount.

Konklusjon

Actor-modellen er elegant, og passer veldig bra for denne oppgaven. Språket er derimot ikke så veldig bra å implementere i (eller eventuelt min kunnskap om det er for dårlig). Det er alt i alt lite tilfredstillende - kildekoden ser ikke fin ut, det er for mye av den, og den føles gammeldags. Jeg merker at jeg ønsker meg en fullgod implementasjon av actor model i et annet språk.

I neste runde av denne serien vil jeg bruke et språk som er enda eldre enn Erlang, men som har en syntax jeg synes er super-elegant…

Tidligere i serien: Introduksjon | Del 2 (C#) | Del 3 (Ruby) | Del 4 (Boo).

Kildekoden fra denne blogposten er tilgjengelig på Github. Der står du fritt til å forgrene løsningen og gjøre egne modifikasjoner om du ønsker det (for å illustrere et poeng eller lignende). Som alt annet på bloggen er koden lisensiert under Creative Commons.


comments powered by Disqus