Blocking Call

Aus FHEMWiki
Version vom 18. Juli 2022, 09:29 Uhr von Beta-User (Diskussion | Beiträge) (typo)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen

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 der 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 Ergebnis 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, die "non-blocking" ausgeführt werden soll (ohne Klammern, nur der reine Funktionsname).
$arg nein "DeviceName|Argument1|Argument2" Das Funktionsargument, das der blockingFn übergeben werden soll.
$finishFn ja "speedtest_SpeedtestDone" Die Funktion, die 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 blockigen, der mit diesem Aufruf abgearbeitet wird.
$hash->{finishFn} → Der Name der finishFn, die diesen Aufruf nach Erfolg verarbeiten wird.
$hash->{abortFn} → Der Name der abortFn, die im Fehlerfall aufgerufen wird.
$hash->{abortArg} → Das Argument, das 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, der sich nach Abschluss des BlockingCall selbst zerstört.
  • Wenn viele Definitionen aktiv sind, welche BlockingCalls parallel starten kann es zu einem relativ hohen Memory-Footprint kommen aufgrund mehrfach laufenden Fork-Prozessen. Dies kann auf schwachbrüstiger Hardware zu Speicherengpässen führen. Ab FHEM Revision 11917 lassen sich die maximal parallel laufenden BlockingCalls durch das globale Attribut blockingCallMax begrenzen (Standardwert: unbegrenzt). Sofern die maximale parallele Anzahl an BlockingCalls erreicht ist, werden weitere Calls in eine Warteschlange eingereiht und ausgeführt, sobald laufende Calls beendet werden.

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 rein theoretisch 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, dass 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});

  Log3 $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.

Eine weitere Möglichkeit auch kompliziertere Datenstrukturen zurückzugeben ist JSON->new->encode() (Perl-Modul JSON) oder toJSON (aus fhem.pl) zum serialisieren und JSON->new->decode() zum deserialisieren zu verwenden. Achtung: Man kann hierfür zwar auch encode_json bzw. decode_json aus dem Perl-Modul JSON verwenden, sollte dabei aber beachten, dass hierdurch auch das encoding geändert wird! Außerdem führen unerwartete Rückgaben (mit Ausnahme von der Serialisierung durch toJSON) ggf. dazu, dass FHEM komplett beendet wird; alle decoding- und encoding-Anweisungen aus dem JSON-Modul sollten daher unbedingt durch eine (Block-) eval-Anweisung geschützt werden:

my $decoded;
if ( !eval { $decoded  = JSON->new->decode($content) ; 1 } ) {
   Log3($hash->{NAME}, 1, "JSON decoding error in BlockingCall return: $@");
   #weiterer Code für Fehlerbehandlung...
   return;
}

Rückgabewert muss unbedingt den Namen des Devices enthalten, von dem der Call gestartet wurde

Um einen Rückgabewert dem korrekten Device zuordnen zu können muss im Rückgabewert der Funktion, die 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 und ShutdownFn 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 und ShutdownFn 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;
}

Anzeige der laufenden Blocking Calls

Mit dem FHEM-Befehl blockinginfo lassen sich die laufenden Blocking Calls anzeigen.

Begrenzen der Blocking Calls

Mit dem globalen Attribut blockingCallMax können die gleichzeitig laufenden Blocking Calls begrenzt werden.

Module, die Blocking.pm verwenden

Folgende Module verwenden aktuell Blocking.pm um langwierige Funktionalitäten auszulagern:

  • 10_pilight_ctrl.pm
  • 23_KOSTALPIKO.pm
  • 23_LUXTRONIK2.pm
  • 38_JawboneUp.pm
  • 42_SYSMON.pm
  • 55_GDS.pm
  • 59_OPENWEATHER.pm
  • 59_PROPLANTA.pm
  • 70_EFR.pm
  • 70_JSONMETER.pm
  • 70_Jabber.pm
  • 70_SML.pm
  • 72_FRITZBOX.pm
  • 73_MPD.pm
  • 73_PRESENCE.pm (siehe dazu Anwesenheitserkennung)
  • 76_SMAInverter.pm
  • 93_DbLog.pm (nicht alle Funktionen)
  • 93_DbRep.pm
  • 98_HMinfo.pm
  • 98_Text2Speech.pm
  • 98_UbiquitiMP.pm
  • 98_WOL.pm
  • 98_update.pm