Blocking Call
Dieser Artikel soll Hinweise und Best Practices im Umgang mit dem Modul Blocking.pm und den daraus bereitgestellten Funktionen bieten.
Allgemein
Das Modul Blocking.pm wurde von Rudolf König entwickelt um in FHEM Funktionsaufrufe zu ermöglichen die relativ viel Zeit in Anspruch nehmen und normalerweise FHEM damit zum Stillstand für die Dauer der Ausführung einer solchen Funktion bringen würde.
Um so etwas zu verhindern kann man mit Hilfe von Blocking.pm eine Funktion über einen Fork des Hauptprozesses unabhängig davon abarbeiten und das Ergebniss dieser Funktion optional an den Hauptprozess übergeben.
Benutzung
Das Modul stellt primär die Funktion BlockingCall() zur Verfügung um eine Perl-Funktion "non-blocking" auszuführen.
BlockingCall($blockingFn, $arg, $finishFn, $timeout, $abortFn, $abortArg);
Argument | Optional | Beispiel | Beschreibung |
---|---|---|---|
$blockingFn |
nein | "speedtest_DoSpeedtest" | Der Name der Perlfunktion welcher "non-blocking" ausgeführt werden soll (ohne Klammern, nur der reine Funktionsname). |
$arg |
nein | "DeviceName|Argument1|Argument2" | Das Funktionsargument, welches der blockingFn übergeben werden soll. |
$finishFn |
ja | "speedtest_SpeedtestDone" | Die Funktion, welche mit dem Funktionsergebnis der blockingFn als Parameter aufgerufen werden soll, sobald diese zuende ist. Diese Funktion wird dabei mit dem Returnwert als erstes Argument aufgerufen. |
$timeout |
ja | 120 | Sofern eine finishFn verwendet wird, kann ein optionales Timeout gesetzt werden, sobald der Aufruf abgebrochen wird, sobald dieser timeout in Sekunden verstrichen ist. |
$abortFn |
ja | speedtest_SpeedtestAborted | Wenn der Aufruf aufgrund eines überschrittenen Timeouts abgebrochen wird, so wird die abortFn aufgerufen, sofern eine definiert ist. |
$abortArg |
ja | $hash | Im Falle eines Abruchs soll die abortFn mit diesem Argument aufgerufen werden. (Dient primär der Zuordnung eines solchen Abbruchs für Modulentwickler. |
Die Funktion BlockingCall() gibt bei Erfolg eine Hashreferenz mit mehreren Items zurück. Dieser Hash ist folgendermaßen aufgebaut.
$hash->{pid} |
→ Die Prozess-Id unter der dieser Aufruf abgearbeitet wird. |
$hash->{fn} |
→ Der Name der blockingFn welcher mit diesem Aufruf abgearbeitet wird. |
$hash->{finishFn} |
→ Der Name der finishFn welche diesen Aufruf nach Erfolg verarbeiten wird. |
$hash->{abortFn} |
→ Der Name der abortFn welche im Fehlerfall aufgerufen wird. |
$hash->{abortArg} |
→ Das Argument, welches im Fehlerfall an die abortFn übergeben werden soll. |
Einschränkungen
Aktuell sind beim Einsatz von Blocking.pm folgende Einschränkungen zu beachten.
- Veränderungen an internen Variablen von FHEM (Device Hashes, Timers, usw.) werden innerhalb eines BlockingCalls durchgeführt und sind dort auch sichtbar, haben aber keinerlei Einfluss auf den eigentlichen FHEM Hauptprozess und alle Definitionen. Solche Veränderungen müssen an die finishFn delegiert werden (z.B. durch zusätzliche Rückgabewerte), da es sich um einen Fork-Prozess handelt, welcher sich nach Abschluss des BlockingCall selbst zerstört.
Blocking.pm für Modulentwickler
Blocking.pm ist aktuell sowohl für Enduser, als auch für Modulentwickler gedacht. Die Benutzung in einem Modul birgt allerdings einige Stolperfallen auf die hier näher eingegangen wird.
return-Wert von BlockingCall immer in $hash abspeichern
Die Funktion BlockingCall() gibt als return-Wert einen Hash mit mehreren Informationen zurück. Dieser Ergebnis-Hash ist in mehrerer Hinsicht sehr wichtig für die Benutzung in einem Modul, dazu in den folgenden Punkten mehr.
Eine gute Möglichkeit ist es dieses Ergebnis unterhalb von $hash->{helper} zu platzieren. So sieht es der Enduser nicht, kann aber bei Bedarf via list-Befehl angezeigt werden.
Beispiel:
$hash->{helper}{RUNNING_PID} = BlockingCall( … );
Sicherstellen, dass immer nur ein BlockingCall gleichzeitig läuft
Sobald ein BlockingCall läuft könnte man reintheoretisch direkt einen weiteren BlockingCall starten usw. Dadurch würden mehrer parallele Durchläufe nebenher laufen und könnten den FHEM-Server dadurch unnötig belasten oder sogar auch überlasten. Daher sollte man immer beim Start eines BlockingCall prüfen, ob bereits ein weiterer BlockingCall läuft oder bei mehreren parallelen Blocking Calls eine entsprechende Prüfung einführen um die maximale Anzahl an gleichzeitigen BlockingCalls innerhalb einer Definition zu begrenzen.
$hash->{helper}{RUNNING_PID} = BlockingCall(…) unless(exists($hash->{helper}{RUNNING_PID}));
Dies funktioniert nur dann, wenn man in der finishFn (und später auch abortFn) $hash->{helper}{RUNNING_PID} wieder löscht. Somit ist immer sichergestellt, das nur ein BlockingCall zur gleichen Zeit läuft. Wenn gerade ein BlockingCall aktiv ist aufgrund eines Set-Befehls, könnte man eine Meldung ausgeben.
Beispiel:
BlockingCall("speedtest_DoSpeedtest", $name."|".$server, "speedtest_SpeedtestDone", 120, …);
speedtest_SpeedtestDone($) { … delete($hash->{helper}{RUNNING_PID}); … }
Nutzung von abortFn und abortArg
Mal angenommen man führt einen BlockingCall aus und dieser wird innerhalb des gesetzten Timeouts von 5 Sekunden nicht durchgeführt. Dann erhält man kein Ergebnis via der gesetzten finishFn. Das kann dazu führen, das ein InternalTimer nicht mehr neu gestartet werden kann, der zyklisch diesen BlockingCall ausführen soll um Daten zu ermitteln oder einen Status.
Um einen solchen Abbruch aufgrund des erreichten Timeouts innerhalb eines Moduls zu erkennen, muss der BlockingCall mit einer abortFn und einem abortArg gestartet werden.
- abortFn - die Funktion die im Falle eines Abbruch des Aufrufs gestartet werden soll
- abortArg - das Argument mit der diese Funktion aufgerufen werden soll. Dies díent in einer Modulumgebung der Zuordnung des korrekten Devices bei mehreren Definitionen. Üblicherweise wird hierfür $hash verwendet.
BlockingCall("speedtest_DoSpeedtest", $name."|".$server,"speedtest_SpeedtestDone", 120, "speedtest_SpeedtestAborted", $hash);
sub speedtest_SpeedtestAborted($) { my ($hash) = @_; delete($hash->{helper}{RUNNING_PID}); Log GetLogLevel($hash->{NAME}, 3), "BlockingCall for ".$hash->{NAME}." was aborted"; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+10, …) # falls mit disable-Attribut gearbeitet wird, muss dieses hier geprüft werden }
BlockingCall muss als Argument eine Referenz auf das Ursprungs-Device enthalten
Um später die Ergebnisse korrekt zuordnen zu können, ist es wichtig das korrekte Device zu kennen. Normalerweise wird in FHEM dazu der $hash-Zeiger verwendet um auf das entsprechende Device zu verweisen. Als Argumente für die BlockingFn können Hashes, Zeiger und sonstiges verwendet werden. Wichtig dabei ist, dass man aus den Argumenten irgendwie den Device-Namen ableiten kann, da man diesen später in der finishFn über die Variable $defs{…} wieder in die $hash-Referenz umwandeln kann.
Beispiel
BlockingCall("speedtest_DoSpeedtest", $hash->{NAME}."|".$server,…);
sub speedtest_DoSpeedtest($) { my ($string) = @_; my ($name, $server) = split("\\|", $string); … return "$name|$speedarr[0]|$speedarr[1]|$speedarr[2]"; }
sub speedtest_SpeedtestDone($) { my ($string) = @_; return unless(defined($string)); my @a = split("\\|",$string); my $hash = $defs{$a[0]}; … }
Rückgabewerte des BlockingCalls nur als Einzeiler-String
Bei Funktionen die mit BlockingCall aufgerufen werden muss der Rückgabewert entweder eine Zahl, oder ein einzeiliger String sein, da dieser über den Telnetprompt als Perl-Befehl ( {finishFn("returnvalue")} ) zurückgegeben wird.
Wenn man mehrere separate Werte zurückgeben möchte, kann man diese entweder mit einem Trennzeichen (z.B. Pipe) versehen, oder bei einem Text mit Zeilenumbrüchen alles mit Base64 oder Hex encoden.
So kann man z.B. den Rückgabestring "Wert1|Wert2|Wert3" verwenden und diesen anschließend mit split() wieder in ein Array verwandeln und dann via Index gezielt darauf zugreifen.
Rückgabewert muss umbedingt den Namen des Devices enthalten, von welchem der Call gestartet wurde
Um einen Rückgabewert dem korrekten Device zuordnen zu können muss im Rückgabewert der Funktion, welche mit BlockingCall gestartet wird, der Device-Name enthalten sein, um damit die Referenz auf das richtige Device herstellen zu können.
z.B. als Returnstring "Devicenamen|Readingwert1|Readingwert2|…"
Daher ist es wichtig, den Devicenamen beim Start des Blockingcalls zu übergeben.
Verwenden einer UndefFn um laufende BlockingCalls zu beenden
Wenn gerade ein BlockingCall läuft und man will FHEM herunterfahren oder neustarten kann das problematisch werden, da ja immer noch Unterprozesse laufen und so einen kompletten Stop verhindern können.
Daher muss in der UndefFn des Moduls sichergestellt werden, dass ein laufender BlockingCall beendet wird. Dazu wird als Argument der Return-Hash von BlockingCall benötigt in dem unter anderem die PID des Unterprozesses enthalten ist. Die Funktion BlockingKill() erledigt dann den Rest.
Beispiel
sub speedtest_Undefine($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); return undef; }
Module, welche Blocking.pm verwenden
Folgende Module verwenden aktuell Blocking.pm um langwierige Funktionalitäten auszulagern:
- 32_speedtest.pm
- 73_PRESENCE.pm (siehe dazu Anwesenheitserkennung)