Pit: javascript race condition

Hallo,

wenn ich serverseitig eine Datei einlese (in diesem Fall mit fs.readJSON aus dem Modul fs-extra) und an diese etwas anhänge, laufe ich dann Gefahr, race conditions zu erzeugen?

const nlFile2 = './data/file.json';
  fs.readJson(nlFile2, function(err, data) {
  data.push({
    email: req.body.mail,
    name: req.body.Name
});
fs.writeJson('./data/file.json', data)
.then(() => {
  console.log('success!')
})
.catch(err => {
  console.error(err)
})
})

Pit

  1. Hello,

    Du müsstest einfach nur mal überlegen, wieviele Clients parallel diesen Prozess anstoßen dürfen und ob die gemeinsame Datenbasis innerhalb des kritischen Prozesses für andere (zumindest gegen Schreiben) gesperrt bleibt, oder nicht. Liegt also bezüglich der Datenbasis Multithreading-Verhalten vor?

    Üblicherweise muss in einem Multiuser(Multi-Thread)-System eine Datenbasis (der kritische Abschnitt) vom Zeitpunkt VOR dem Lesen bis zum Zeitpunkt NACH dem Schreiben gesperrt werden, bzw. ihre Integrität gewahrt bleiben.

    Das kann auch mittels Write-Conflict-Counter stattfinden.

    Beim Lesen wird der Counter ausgelesen und es darf nur zurückgeschrieben werden, wenn der Counter nicht verändert wurde. Der erfolgreiche Schreibvorgang erhöht den Counter.

    Der Counter muss gegen irreguläre Veränderung geschützt sein!

    Hier hilft also nur eine Kapselung der Prozesse.

    Liebe Grüße
    Tom S.

    --
    Es gibt nichts Gutes, außer man tut es!
    Das Leben selbst ist der Sinn.
    1. Hallo TS,

      zuerstmal gings mir wirklich nur um die Frage, ob hier Potential für race condition vorliegt.

      Wie wird file locking (und Freigabe) in JS gemacht?

      Das kann auch mittels Write-Conflict-Counter stattfinden.

      Beim Lesen wird der Counter ausgelesen und es darf nur zurückgeschrieben werden, wenn der Counter nicht verändert wurde. Der erfolgreiche Schreibvorgang erhöht den Counter.

      Wo ist der counter gespeichert?

      gruß, Pit

      1. Hello,

        zuerstmal gings mir wirklich nur um die Frage, ob hier Potential für race condition vorliegt.

        JA: Könnte sein. Und ein "könnte" ist hinreichend für das "JA".

        Wie wird file locking (und Freigabe) in JS gemacht?

        Ist nicht wirklich meine Baustelle, aber Du meinst sicher "Node.JS". Und da habe ich mir diesen Link zum Thema Locking gemerkt.

        Das kann auch mittels Write-Conflict-Counter stattfinden.

        Den müsstest Du über die Session retten. Ist also deine Designaufgabe.

        Liebe Grüße
        Tom S.

        --
        Es gibt nichts Gutes, außer man tut es!
        Das Leben selbst ist der Sinn.
        1. Hi Tom,

          JA: Könnte sein. Und ein "könnte" ist hinreichend für das "JA".

          Könnte ich, ehrlich gesagt, mal simmulieren... für den Ja-Fall meld' ich mich nochmal. Edit: Nein, könnt ich doch nicht...

          Wie wird file locking (und Freigabe) in JS gemacht?

          Ist nicht wirklich meine Baustelle, aber Du meinst sicher "Node.JS". Und da habe ich mir diesen Link zum Thema Locking gemerkt.

          Der sieht doch gut aus, dank Dir.

          Pit

          1. Hello,

            ob der Zugriff auf die Ressourcen nun mit Node.JS, Perl, PHP, C++ oder sonstigen Modulen auf dem Server stattfindet, ist für die Lockingstrategie irrelevant. Immer dann, wenn mehrere Clients oder Requester dieselbe Ressource zeitgleich benutzen dürfen, dann muss man unterscheiden:

            • Connected Requests: Es können Handles des Betriebssystems/Filesystems übergeben und beachtet werden. Die Zeit zwischen Holen und Wegschreiben ist extrem kurz.
            • Disconnected Requests (z. B. via HTTP): Es ist nicht sicherzustellen, dass eine permanente Verbindung aufrecht erhalten wird zwischen Holen und Wegschreiben der Daten
            • "akademischer Request" (ct), zwischen Holen und Wegschreiben der Daten liegt mehr Zeit, als man für eine permanente totale Sperre im System verwenden möchte/darf. Das kann auch bei obigen Fällen zutreffen.

            Das Ganze haben Oberschlaue dann auch mal unter dem TOCTTOU-Problem beschrieben. Früher, als wir noch Deutsch reden durften, hieß das "Zeitrelevante Änderungsanforderungen an den Datenbestand" und das hat dann z. B. zur Einführung von "Beipiel für Nummernkreis"en geführt.

            Jede Subsidarität verwaltet ihren Subnummern derartig streng, dass keine Doppelungen auftreten können. In Gesamtheit sind die Nummern dann wieder durch die Trennung in die Nummernkreise eineindeutig. Die Media Access Control (MAC) ist ein Beispiel dafür.  
            

            #Kurz und gut:
            Bei Zeitversatz und/oder Verbindungslosigkeit musst Du selber für die notwendige Sicherheit für die Datenintegrität sorgen.

            Liebe Grüße
            Tom S.

            --
            Es gibt nichts Gutes, außer man tut es!
            Das Leben selbst ist der Sinn.
    2. Du müsstest einfach nur mal überlegen, wieviele Clients parallel diesen Prozess anstoßen dürfen und ob die gemeinsame Datenbasis innerhalb des kritischen Prozesses für andere (zumindest gegen Schreiben) gesperrt bleibt, oder nicht. Liegt also bezüglich der Datenbasis Multithreading-Verhalten vor?

      Ich verstehe, was du sagen möchtest, aber halte es trotzdem für wichtig ein wenig mehr Klarheit in die Terminologie zu bringen. Entscheidend ist nicht die Multi-Threading-Fähigkeit oder die Fähigkeit parallele Berechnungen auszuführen, sondern der konkurrierende Zugriff auf geteilte Resourcen. Ein Node-Prozes ist in der Regel eine single-threaded Laufzeit-Umgebung und trotzdem ist das gezeigte Beispiel anfällig für Race-Conditions: Das Verhalten des Prozesses ist abhängig davon, wann die Datei gelesen und beschrieben wird, dabei spielt es keine Rolle, ob die Zugriffe durch den selben Node-Prozess oder völlig fremde Prozesse verursacht werden. Wenn man ausschließen kann, dass die Datei von anderen Prozessen genutzt wird, dann lässt sich die Race-Condition innerhalb von Node beseitigen, indem man die Lese-Schreib-Zugriffe mit einem Semaphore ordnet. Wenn man das nicht ausschließen kann, braucht man einen Zugriffs-Regler auf OS- oder Dateisystem-Ebene, für maximalen Schutz bspw. Mandatory Locking. Dafür gibt es sicherlich ein paar fertige Node-Module.

      1. Hello,

        Du müsstest einfach nur mal überlegen, wieviele Clients parallel diesen Prozess anstoßen dürfen und ob die gemeinsame Datenbasis innerhalb des kritischen Prozesses für andere (zumindest gegen Schreiben) gesperrt bleibt, oder nicht. Liegt also bezüglich der Datenbasis Multithreading-Verhalten vor?

        Ich verstehe, was du sagen möchtest, aber halte es trotzdem für wichtig ein wenig mehr Klarheit in die Terminologie zu bringen. Entscheidend ist nicht die Multi-Threading-Fähigkeit oder die Fähigkeit parallele Berechnungen auszuführen, sondern der konkurrierende Zugriff auf geteilte Resourcen.

        Korrigiere:

        #Der konkurrierende datenverändernde Zuriff

        Ein Node-Prozes ist in der Regel eine single-threaded Laufzeit-Umgebung und trotzdem ist das gezeigte Beispiel anfällig für Race-Conditions: Das Verhalten des Prozesses ist abhängig davon, wann die Datei gelesen und beschrieben wird, dabei spielt es keine Rolle, ob die Zugriffe durch den selben Node-Prozess oder völlig fremde Prozesse verursacht werden. Wenn man ausschließen kann, dass die Datei von anderen Prozessen genutzt wird, dann lässt sich die Race-Condition innerhalb von Node beseitigen,

        korrigiere:
        innerhalb eines Node-JS-Prozesses,

        kann man jegliche Racecondition durch geordnete Abarbeitung ausschließen

        indem man die Lese-Schreib-Zugriffe mit einem Semaphore ordnet. Wenn man das nicht ausschließen kann, braucht man einen Zugriffs-Regler auf OS- oder Dateisystem-Ebene, für maximalen Schutz bspw. Mandatory Locking. Dafür gibt es sicherlich ein paar fertige Node-Module.

        Liebe Grüße
        Tom S.

        --
        Es gibt nichts Gutes, außer man tut es!
        Das Leben selbst ist der Sinn.
  2. Hallo Pit,

    die anderen Antworten haben sich auf externe race-conditions bezogen, also Konkurrenz mit anderen Instanzen deines Programms. Ich habe noch eine andere.

    JavaScript ist single-threaded, solange Du keine Webworker einsetzt und keine Requeste auf externe Dienste machst. Innerhalb deines JS wirst Du keine race condition erhalten.

    Dein Callback, in dem Du den push machst, läuft ab nachdem die Daten von readJson empfangen wurden.

    Problematisch ist dagegen dein writeJson, weil das außerhalb des readJson Callbacks läuft. D.h. dein Programm setzt den readJson Request ab, der registriert unter der Haube einen Handler, der auf die empfangenen Daten reagiert. UND DANN IST readJson ZU ENDE!

    D.h. der writeJson Aufruf läuft, während noch auf den Empfang von readJson gewartet wird. Eine Variable data gibt es jetzt gar nicht. Falls Du sie global definiert hättest, wäre sie jetzt noch undefined. Und das ist ein race, den Du nur verlieren kannst. Du solltest ggf. mit dem Promise arbeiten, das von readJson zurückgegeben wird, dann kann man das schön verketten:

    const nlFile2 = './data/file.json';
    fs.readJson(nlFile2)
    .then(data => {
      data.push({
        email: req.body.mail,
        name: req.body.Name
      });
      return fs.writeJson('./data/file.json', data);
    })
    .then(() => {
      console.log('success!')
    })
    .catch(err => {
      console.error(err)
    });
    

    Wenn ein .then Handler ein Promise zurückgibt, wartet ein nachgelagerter .then Handler auf die Erfüllung dieses Promise. Dadurch wird dein 'success' erst ausgegeben, wenn der Write fertig ist.

    Wichtig ist jedenfalls, dass writeJson von der erfolgreichen Ausführung von readJson abhängig ist, und das geht nur wenn es in einem Callback steht. Entweder direkt, oder über ein Promise gekapselt.

    Rolf

    --
    sumpsi - posui - clusi
    1. JavaScript ist single-threaded, solange Du keine Webworker einsetzt und keine Requeste auf externe Dienste machst. Innerhalb deines JS wirst Du keine race condition erhalten.

      Ich halte dir zu Gute, dass wir zeitgleich geantwortet haben, und du meine Antwort daher noch nicht gelesen haben konntest. Trotzdem, muss ich das gleich nochmal korriegeren: JavaScript hat Race-Conditions. Einfaches Beispiel: https://jsfiddle.net/017my4wz/8/. Drückt man den Button wiederholt mit mehr als einer halben Sekunde Abstand, dann inkremintiert der Counter wie gewollt. Wartet man nicht so lange zwischen den Klicks, dann tritt eine Race-Condition auf und der Counter zählt falsch. Ausschlaggebend dafür ist, dass bei jedem Klick und jedem Timeout ein neuer Task in die Event-Queue gepusht wird und die Tasks greifen auf die geteilte Variable i zu. Die Tasks greifen konkurrierend auf die geteilte Zählvariable i zu.

      1. Hallo 1unitedpower,

        dein Beispiel zeigt handgemachtes, kooperatives Multitasking mit Hilfe einer externen Ressource (die Event Queue). Und du "rettest" einen globalen Wert vorsätzlich über einen Thread-Wechsel hinweg. Ts ts ts... 😉

        Unter einer Race-Condition verstehe ich, dass zwei Programmsegmente echt parallel laufen und man nicht weiß wer zuerst fertig ist. Das ist zunächst nicht schlimm. Bugs durch Race-Conditions entstehen, wenn der Programmierer dieses Unwissen nicht beachtet und Annahmen über die zeitliche Abfolge trifft. Und ja, natürlich hast Du recht, wenn ich eine definierte zeitliche Abfolge brauche, muss ich Serialisierungshilfen wie Semaphore und Locks verwenden.

        Die für JavaScript wichtige Erkenntnis ist aber: Ein JavaScript-Programm wird in Zyklen ausgeführt. Jeder Zyklus beginnt mit der Entnahme eines Auftrags aus der Event-Queue. Und dann wird er durchlaufen, von Anfang bis Ende, ohne Unterbrechung. Er kann weitere Aufträge in die Event-Queue einstellen, die werden dann später ausgeführt. Funktionen wie setTimeout oder fs.readJson hinterlassen Einträge in der Event-Queue, mal direkt, mal indirekt. Innerhalb eines Zyklus gibt es keine Race-Condition. Wenn logische Abläufe über mehrere Zyklen gehen, dann schon.

        Rolf

        --
        sumpsi - posui - clusi
        1. dein Beispiel zeigt handgemachtes, kooperatives Multitasking mit Hilfe einer externen Ressource (die Event Queue). Und du "rettest" einen globalen Wert vorsätzlich über einen Thread-Wechsel hinweg. Ts ts ts... 😉

          Das Beispiel ist zugegeben eher pathalogischer Natur, das ist dem Umstand geschuldet, dass ich ein poentiertes, nachvollziehbares Beispiel finden wollte. Aber das heißt nicht, dass solche Fälle in der Praxis nicht auch auftreten. Beispiel aus dem Forum, davon ließen sich hier bestimmt auch hundert weitere Threads finden.

          Unter einer Race-Condition verstehe ich, dass zwei Programmsegmente echt parallel laufen und man nicht weiß wer zuerst fertig ist.

          Ich verstehe eure Erklärungsversuche auch, aber ich erinnere mich auch noch an eine meine eigene Zeit als Anfänger und wie sehr ich mich damals von der Thematik rund um Multi-Threading, Parallelität und Concurrency habe verwirren lassen. Rückwirkend weiß ich, dass das zum Teil auch am schwammigen Gebrauch des Vokabulars gelegen haben muss. Das möchte ich Pit und unseren anderen AnfängerInnen gerne ersparen, deswegen mahne ich das an. Ich glaube mir wäre in vielen Fällen geholfen gewesen, wenn man mir komplizierte Dinge, wie Race Conditions, nicht mit einem intuitivem, aber fachlich ungenauem Sprachgebrauch erklärt hätte. Das hat mich oft auf falsche Fährten gelockt, deshalb plädiere ich hier für den präzisen Einsatz der Fachsprache. In diesem Fall musste das Stichwort ergo Concurrency und nicht Parallelität oder Multi-Threading lauten.

          Die für JavaScript wichtige Erkenntnis ist aber: Ein JavaScript-Programm wird in Zyklen ausgeführt. Jeder Zyklus beginnt mit der Entnahme eines Auftrags aus der Event-Queue. Und dann wird er durchlaufen, von Anfang bis Ende, ohne Unterbrechung. Er kann weitere Aufträge in die Event-Queue einstellen, die werden dann später ausgeführt. Funktionen wie setTimeout oder fs.readJson hinterlassen Einträge in der Event-Queue, mal direkt, mal indirekt. Innerhalb eines Zyklus gibt es keine Race-Condition. Wenn logische Abläufe über mehrere Zyklen gehen, dann schon.

          Das fasst die Arbeitsweise des Event-Loops gut zusammen.

    2. Hallo Rolf,

      die anderen Antworten haben sich auf externe race-conditions bezogen, also Konkurrenz mit anderen Instanzen deines Programms. Ich habe noch eine andere.

      ok.

      D.h. der writeJson Aufruf läuft, während noch auf den Empfang von readJson gewartet wird.

      Stimmt.

      Eine Variable data gibt es jetzt gar nicht. Falls Du sie global definiert hättest, wäre sie jetzt noch undefined. Und das ist ein race, den Du nur verlieren kannst. Du solltest ggf. mit dem Promise arbeiten, das von readJson zurückgegeben wird, dann kann man das schön verketten:

      const nlFile2 = './data/file.json';
      fs.readJson(nlFile2)
      .then(data => {
        data.push({
          email: req.body.mail,
          name: req.body.Name
        });
        return fs.writeJson('./data/file.json', data);
      })
      .then(() => {
        console.log('success!')
      })
      .catch(err => {
        console.error(err)
      });
      

      Stehen diese .then-Handler für Promisses?

      Wenn ein .then Handler ein Promise zurückgibt, wartet ein nachgelagerter .then Handler auf die Erfüllung dieses Promise.

      Woher weiß man sowas?

      Pit

      1. Tach!

        Stehen diese .then-Handler für Promisses?

        Jein. Sie sind Methoden des Promise-Objekts.

        Wenn ein .then Handler ein Promise zurückgibt, wartet ein nachgelagerter .then Handler auf die Erfüllung dieses Promise.

        Woher weiß man sowas?

        Ob irgendetwas ein Promise zurückgibt, steht in dessen Dokumentation. Wenn das der Fall ist, geht es in der allgemeinen Dokumentation des Promise-Objekts weiter. Da ist definiert, dass ein Promise unter anderem die Methode then() hat, die ihrerseits wieder ein Promise zurückgibt. Und davon kann man dann wieder then() aufrufen, und so weiter.

        dedlfix.

      2. Hallo Pit,

        Stehen diese .then-Handler für Promises?

        Wenn man ein Muster der Art

        tuWas(bla, bla, bla)
        .then(function(data) {
        })
        .then(function(data) {
        })
        .then(function(data) {
        })
        .catch(function(error) {
        });
        

        findet, dann verspreche ich Dir, dass da ein Promise unterwegs ist. jQuery bildet da ein bisschen eine Sonderrolle, die verwenden keine nativen Promises, sondern ein eigenes Deferred-Objekt (was ähnlich funktioniert, aber ein etwas anderes API hat).

        Woher weiß man sowas?

        Durch Lesen des WIKI

        Bzw. in meinem Fall durch Schreiben; ich habe den ursprünglichen, knappen Artikel von Matthias Scharwies vor gut einem Jahr kräftig aufgeblasen.

        Ich gebe aber zu, es ist nicht ganz trivial sich im Wiki über die aktuellen JavaScript-Techniken einen Überblick zu verschaffen, wenn man gar nicht weiß, wonach man sucht.

        Wenn man node.js verwendet, ist die Kenntnis von Promises aber unabdingbar; alle Zugriffe auf externe Ressourcen werden damit gesteuert.

        Rolf

        --
        sumpsi - posui - clusi