May 2009

Erlang D.A.CH.

Erlang D.A.CH. besteht im Kern derzeit aus sechs Personen. Wir sind jedoch immer auf der Suche nach Verstärkung. Es werden keine Wunder erwartet. Schon ein spannender neuer Link, ein kleiner Blog-Beitrag oder Hinweise auf Fehler in unserer Webseite sind hilfreich. Zudem findest du unter Xing sowie auf Trap-Exit ein Erlang-D.A.CH. Forum. Beiträge sind auch hier erwünscht. Also: Wenn du Spass an Erlang hast, uns ein wenig helfen magst oder nur Fragen hast. Just do it.

Kontakt: volkert (at) erlang-dach.org

Hot Code Replacement

Höchstverfügbare Systeme, bei denen eine Downtime keine Option ist, brauchen Verfahren um Korrekturen in das produktive System einzuspielen, ohne das dieses runtergefahren und neugestartet werden muss. Erlang bietet hier die Möglichkeit des Codeaustauches zur Laufzeit.

Beispiel:

-module(m).
-export([loop/0, start/0]).

start() ->
  spawn(m, loop, []).

tu_dies() ->
  io:format("Falsche Implementierung~n").

tu_das() ->
  io:format("jawoll~n").

%
% Process Loop
%
loop() ->
  receive
    code_switch ->
      m:loop();
    tu_dies ->
      tu_dies(),
      loop();
    tu_das ->
      tu_das(),
      loop()
  end.

Starten wir zwei Prozesse in der Shell und senden Ihnen einige Meldungen:
> P1 = m:start().
...
> P2 = m:start().
...
> P1 ! tu_dies.
Fehlerhafte Implementierung
> P1 ! tu_das.
ok
> P2 ! tu_dies.
Fehlerhafte Implementierung

Die Funktion tu_dies/0 hat einen Fehler, den wir nun korrigieren wollten. Nehmen wir im Modul m folgende Korrektur vor ...

tu_dies() ->
  io:format("Korrekte Implementierung~n").

und compilieren und laden es mit:
> c(m).

Schauen wir was passiert:
> P1 ! tu_dies.
Fehlerhafte Implementierung

> P2 ! tu_dies.
Fehlerhafte Implementierung


Beide Prozess verwenden also noch die alte Implementierung.

Nun Informieren wir P1, dass er das korrigierte Modul nehmen soll:
> P1 ! code_switch.

Und prüfen dies auch gleich:
> P1 ! tu_dies.
Korrekte Implementierung

> P2 ! tu_dies.
Fehlerhafte_Implementierung

Der qualitfizierte Aufruf m:loop() im Rumpf zu code_switch ist per Definition immer an das aktuelle Module m gebunden; in unserem Fall also das korrigierte und neu geladene Modul. Auffällig ist, dass bestehende Prozesse (hier etwa P2) weiter mit der alten Implementierung arbeiten können. Das Erlang-System kann nämlich zwei Versionen eines Moduls, die alte und die aktuelle, halten. Dieses Verfahren ist sinnvoll, da P2 ja gerade wichtige Aufgaben bearbeiten könnte, die erst abgeschlossen werden sollten, bevor die neue Implementierung verwendet werden soll. Hierzu noch ein Hinweis: Wird versucht eine weitere Version zu laden, wird die alte Version aus dem System entfernt und die bisher aktive zur alten Version. Etwaige Prozesse, die das alte Modul verwenden, werden dann jedoch terminiert.

Erlang - The Smarter Way Of Programming

Fehlerbehandlung in nebenläufigen und verteilten Prozessen

Erlang hat einen eingebauten Mechanismus zur Fehlerbehandlung zwischen Prozessen. Ein abgestürtzter Prozess sendet hierbei allen „gelinkten“ Prozessen eine Meldung der Form {'EXIT', SomePid, FehlerGrund}, auch Exit-Meldung genannt. Die Empfängerprozesse können diese Meldung auswerten und entsprechend reagieren (Protokollierung, Prozess neu starten, Alarm auslösen, ...). Mit dieser Eigenschaft können auch einfach hierarchische Strukturen zur Überwachung von Prozessen gebaut werden. Das Erlang/OTP macht mit seinen Supervisor-Trees davon gebrauch. In einem späteren Eintrag mehr dazu.

Beispiel:
Wir haben in diesem Beisipel zwei Prozesse, einen Error_Handler Prozess und einen Worker Prozess. Der Worker wird beim Erzeugen des Error_Handlers durch die Funktion link registriert. Damit der Error_Handler Exit-Meldungen vom Worker empfangen kann, ist er zudem als Systemprozess zu markieren. Dieses geschieht durch den Funktionsaufruf process_flag(trap_exit, true). Andernfalls würde der Error_Handler nach Erhalt der Exit-Meldung ebenfalls terminieren.

Unser Worker kann zwei Messages behandeln: msg_ok und msg_error. Bei der Bearbeitung der Message msg_error wird eine nicht zulässige Division durch Null ausgeführt, was zu einer unbehandelten Exception führt und ein Terminieren des Workers bewirkt. Diese unbehandelte Exception wird dem Error_Handler durch die Exit-Meldung signalisiert (FehlerGrund).

-module(process).
-export([start_error_handler/1, error_handler/1, start/0, proc/0]).

%
% Error_Handler
%
start_error_handler(Pid) ->
  spawn(process, error_handler, [Pid]).

error_handler(Pid) ->
  process_flag(trap_exit, true),
  link(Pid),
  receive
    {'EXIT', SomePid, FehlerGrund} ->
      io:format("~p terminiert, da ~p~n", [SomePid,FehlerGrund])
  end.

%
% Prozess Worker
%
start() ->
  spawn(process, proc, []).

proc() ->
  receive
    msg_ok ->
      io:format("Alles ok~n"),
      proc();
    msg_error ->
      io:format("Autsch ... Fehler ~n"),
      1/0 % HIER FEHLER
  end.

In der Shell:

> Worker = process:start().

> process:start_error_handler(Worker).

> Worker ! msg_ok.
Alles ok

> Worker ! msg_error.
Autsch ... Fehler
...
<0.38.0> terminiert, da {badarith,[{process,proc,0}]}

Es können mehrere Error_Handler auf dem Worker Prozess registriert werden. Diese Handler erhalten beim Absturz des Workers alle eine Exit-Meldung.

Dieses Trap-Exit Verfahren funktioniert natürlich auch bei auf mehrere Erlang-Knoten verteilte Prozesse.

Prozesse verteilen

Weil es so schön einfach ist, gleich die Verteilung der Prozesse auf unterschiedliche Rechner hinterher: Erlang Prozesse, die in unterschiedlichen Erlang Laufzeitumgebungen (Erlang-Knoten) existieren, können sich gegenseitig ebenso einfach Meldungen zukommen lassen, wie solche in einer Umgebung. Dabei ist es egal, auf welchem physikalischen Rechner die Erlang Knoten effektiv laufen.

Beispiel:
Angenommen der kleine Robi aus dem vorherigen Bespiel steht über das Netzwerk (WLAN) mit einem Kommandierungsrechner in Verbindung, und auf Robi (mit der Rechneradresse: robi.robot.net) sowie auf dem Rechner laufen eigene Erlang Umgebungen (Namen der Erlang Knoten: node).

Wenn auf Robi der Steuerungsprozess gestartet (robot:start()) wurde, können wir in der Erlang-Shell des Kommandierungsrechners durch die schlichte Eingabe von

> {robi,’node@robi.robot.net’} ! {vorwaerts, 10}

Robi einen Befehl zustellen.

Vorausetzung ist lediglich, dass die beiden Knoten mit entsprechenden Parametern (Knotenname, gemeinsames Magic Cookie) gestartet werden.

MilesSmile

Magic Cookie, was’n das? Erlang Knoten auf unterschiedlichen Rechnern können sich nur gegenseitig Meldungen zustellen, wenn sie eine gemeinsame Zeichenkette - den Magic Cookie - kennen. Eine solche gemeinsame Zeichenkette muss bei jeder an der Kommunikation teilnehmenden Umgebung hinterlegt werden.

Selective Receive

Der Meldungsaustausch zwischen Prozessen basiert auf asynchroner Kommunikation über Mailboxen. Der Vorteil hierbei ist, dass Sender und Empfänger weitgehend autonom voneinander bleiben können. Sendet ein Prozess A eine Meldung an einen Prozess B, kann sich Prozess A direkt nach dem Absenden der Meldung wieder anderen Aufgaben widmen. Ebenso ist es egal, was Prozess B zur Zeit des Absendens macht, denn die Meldung von Prozess A wird zunächst in der Mailbox (Briefkasten) vom Prozess B abgelegt.

Das „Selective Receive“ vom Prozess B prüft mit der Möglichkeit des Musterabgleichs (engl. Pattern Matching), ob eine Meldung aus der Mailbox einem der Meldungsmuster entspricht. Ist das der Fall und ist zusätzlich der entsprechende Bewacher true, wird die Meldung aus der Mailbox genommen und der zugehörige Rumpf-Ausdruck ausgewertet. Befindet sich keine Meldung in der Prozessmailbox wird, wenn definiert, nach einer Zeit TimeOut der TimeOutAusdruck ausgewertet.

Hier das Schema des Receive-Ausdrucks:

receive
  MeldungsMuster1 [
when Bewacher1] ->
   Ausdruck1;
  MeldungsMuster2 [
when Bewacher2] ->
   Ausdruck2;
  ...
[after
  TimeOut ->
   TimeOutAusdruck]
end

Beispiel:
Hier ein kleines Beispiel für den „Steuerungsprozess“ für den „Roboter“ Robi:

-module(robot).
-export([start/0, loop/1]).

start() ->
  register(robi, spawn(robot, loop, [10000])).

loop(TimeOut) ->
  receive
    {vorwaerts, AnzahlSchritte} ->
       io:format("gehe ~p schritte vorwaerts~n", [AnzahlSchritte]),
       loop(TimeOut);
    {rueckwaerts, AnzahlSchritte} ->
       io:format("gehe ~p schritte rueckwaerts~n", [AnzahlSchritte]),
       loop(TimeOut);
    {drehe_rechts, Grad} ->
       io:format("drehe ~p grad rechts~n", [Grad]),
       loop(TimeOut);
    {drehe_links, Grad} ->
       io:format("drehe ~p grad links~n", [Grad]),
       loop(TimeOut);
    halt ->
       io:format("halte an~n"),
       loop(TimeOut)
    after TimeOut->
       io:format("piep piep piep ~n"),
       loop(TimeOut)
   end.

In der Shell den Steuerungsprozess starten ...
>robot:start().

... und Befehle senden
>robi ! {vorwaerts, 20}.
gehe 20 schritte vorwaerts


>robi ! {drehe_rechts, 45}.
drehe 45 grad rechts

>robi ! halt.
halte an

Und falls keine Befehle kommen:
piep piep piep

Noch ein paar Hinweise zur Arbeitsweise vom Receive:
  • Meldungen in der Mailbox werden in der Reihe des Eintreffens abgearbeitet.
  • Passt keine Meldung auf ein entsprechendes Muster im receive-Ausdruck, wird der Prozess suspendiert und erst durch eine neueintreffende Meldung, bzw. einen TimeOut, reaktiviert.
  • Meldungen für die der Musterabgleich nicht erfolgreich war, werden in eine „Sichere Warteschlange“ abgelegt, und zwar solange, bis eine neue Meldung in der Mailbox landet oder der Timer abläuft. In diesem Falls werden die Meldungen aus der „Sicheren Warteschlange“ wieder in die Mailxbox übernommen.
Munter bleiben.