Felix Riesterer: Parallelisierung verhindern

Liebe Mitlesende,

ich habe für mein Projekt einen Uploader gebaut, der im Prinzip das umsetzt, was mein Artikel zu segmentierten Uploads beschreibt. Damit die Segmente in der richtigen Reihenfolge hochgeladen werden können, verwende ich eine Funktion, die sich immer wieder selbst aufruft, wenn ein weiteres Segment übertragen werden muss:

const
  chunkSize = 2*1000*1000, // 2MB
  chunks = chunks = Math.ceil(myFile.size / chunkSize);

let chunk = 0;

const uploader = () => {
  ... // formData befüllen
  fetch(
    myURL,
    { method: "POST", body: formData }
  )
  .then(response => response.json())
  .then(data => {
    if ("error" in data) {
      handleError(data.error);
    } else {
      chunk++;
      if (chunk < chunks) {
        uploader();
      }
    }
  })
  .catch(error => {
    //
  });
};

uploader();

Jetzt habe ich das Problem, dass ich eine Liste an Dateien übertragen möchte. Mein erster Versuch war der, die Liste in einer Schleife abzuarbeiten. Das Ergebnis ist, dass die uploader()-Aufrufe nicht in der richtigen Reihenfolge angestoßen werden. Bei Dateien, die in nur einem Segment übertragen werden können, stört das nicht, aber bei Dateien, die in mehreren Segmenten übertragen werden, stört das sehr wohl, weil das Backend für jeden User nur eine temporäre Datei anbietet, in der die Segmente gesammelt werden.

Frage: Wie kann ich es erreichen, dass eine Datei erst übertragen wird, wenn eine andere definitiv (also auch mit egal wievielen Segmenten) komplett übertragen worden ist? Muss ich mir da ein Event bauen, das mein Uploader sendet, wenn er fertig ist, damit ein passender EventHandler dann die nächste Datei abarbeitet? Oder gibt es da eine Lösung mit Promises?

Was würdet ihr empfehlen?

Liebe Grüße

Felix Riesterer

akzeptierte Antworten

  1. Moin Felix,

    Oder gibt es da eine Lösung mit Promises?

    Was würdet ihr empfehlen?

    In der Vergangenheit bin ich da mit Array.prototype.reduce eigentlich ganz gut gefahren.

    Alex MacArthur hat da etwas aufgeschrieben.

    Kannst du deine Segmente entsprechend gestalten?

    Das Entwurfsmuster wird gerne Wasserfall genannt: https://github.com/dotSlashLu/promise-waterfall

    Gruß,

    --
    a.k.a. André
    1. Lieber Ryuno-Ki,

      In der Vergangenheit bin ich da mit Array.prototype.reduce eigentlich ganz gut gefahren.

      ja, das ist eine der Methoden, die ich bisher nur dem Namen nach kenne und die ich in meinen Projekten so noch nie eingesetzt habe. Prinzipiell weiß ich, was sie tut.

      Alex MacArthur hat da etwas aufgeschrieben.

      Oha, eine wirklich steile Lernkurve für mich. Aber es sieht so aus, als würde sie mein Problem so elegant lösen, wie ich mir erhoffe. Muss ich also ausprobieren. Hatte ja schon den Verdacht, dass ich Promises benötigen würde, und mit Array.reduce() scheint das der Weg zu sein.

      Kannst du deine Segmente entsprechend gestalten?

      Verstehe nicht, was Du meinst. Inwiefern gestalten?

      Das Entwurfsmuster wird gerne Wasserfall genannt: https://github.com/dotSlashLu/promise-waterfall

      Oh, schon wieder npm. Das meide ich wie der Teufel das Weihwasser. Aber in der Datei index.js wird in Zeile 23 list.reduce(function(l, r){...} verwendet, das dem entspricht, was Alex MacArthur in seinem Artikel beschreibt. Folgt also prinzipiell der gleichen Idee. Wenn das Wasserfall genannt wird, dann ist ein Wasserfall wohl meine Lösung.

      Herzlichen Dank für Deine Hilfe! Wenn ich es getestet habe und es passt, mache ich Deine Antwort zu einer aktzeptierten Antwort.

      Liebe Grüße

      Felix Riesterer

      1. Moin Felix,

        Kannst du deine Segmente entsprechend gestalten?

        Verstehe nicht, was Du meinst. Inwiefern gestalten?

        Kannst du dir ein Array bauen oder brauchst du zwingend eine for-Schleife?

        Das Entwurfsmuster wird gerne Wasserfall genannt: https://github.com/dotSlashLu/promise-waterfall

        Oh, schon wieder npm. Das meide ich wie der Teufel das Weihwasser.

        Viele Pakete von npm sind quelloffen. Eine der Freiheiten ist das studieren von offener Software. Du musst es dann nicht als Abhängigkeit einbinden, wenn du dich damit aufschlauen konntest (ich kommentiere aber regelmäßig, von wo ich etwas gelernt habe).

        Herzlichen Dank für Deine Hilfe! Wenn ich es getestet habe und es passt, mache ich Deine Antwort zu einer aktzeptierten Antwort.

        Falls das nicht tut, überlegen wir uns etwas anderes.

        Gruß,

        --
        a.k.a. André
        1. Lieber Ryuno-Ki,

          Kannst du dir ein Array bauen oder brauchst du zwingend eine for-Schleife?

          ich mache mir mit Array.from(fileInput.files) ein Array, welches ich dann mittels reduce() auf die vorgeschlagene Art abarbeiten lasse.

          (ich kommentiere aber regelmäßig, von wo ich etwas gelernt habe).

          Ja, das tue ich auch. Aktuell habe ich die URL zu MacArthurs Artikel in meinem Quelltext an der entsprechenden Stelle hinterlegt. War ja nicht meine Idee, sondern seine. Und Du hast sie mir vermittelt.

          Falls das nicht tut, überlegen wir uns etwas anderes.

          Es hat getan. Auf Anhieb! Bin total begeistert! Nochmals vielen herzlichen Dank!

          Liebe Grüße

          Felix Riesterer

  2. Hi,

    const
      chunkSize = 2*1000*1000, // 2MB
      chunks = chunks = Math.ceil(myFile.size / chunkSize);
    

    Frage: was hat es mit dem zweifachen chunks = auf sich?
    Wozu die doppelte Zuweisung?

    cu,
    Andreas a/k/a MudGuard

    1. Lieber MudGuard,

        chunks = chunks = Math.ceil(myFile.size / chunkSize);
      

      bad copy pasta 😉

      Liebe Grüße

      Felix Riesterer

  3. Hallo Felix Riesterer,

    Ich würde empfehlen, für jede Datei, die hochgeladen werden soll, eine eigene Temp-Datei zu erstellen und das Problem damit obsolet zu machen.

    Alternativ könntest du auch die Fragmente einzeln speichern und erst zusammenfügen, wenn alle da sind.

    Kurz: synchronisiere parallele Abläufe nur, wenn es unbedingt nötig ist.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Lieber Rolf,

      ich sehe wirklich keinen Vorteil darin, dass fetch-Aufrufe, die in der Reihenfolge ABCDE abgesetzt werden, in einer anderen Reihenfolge (z.B. ACDEB) am Server ankommen, wenn das nicht wirklich sein muss. Das ist wie das Sortieren eines Arrays. Das kann man aus Gründen so haben wollen.

      Ich würde empfehlen, für jede Datei, die hochgeladen werden soll, eine eigene Temp-Datei zu erstellen und das Problem damit obsolet zu machen.

      Also eine Verkomplizierung auf Server-Seite, weil der Client Mist baut? Noch dazu welchen, den ich vermeiden könnte? Das sehe ich jetzt nicht gerade als Vorteil. Was übersehe ich, das Du als Vorteil siehst?

      Kurz: synchronisiere parallele Abläufe nur, wenn es unbedingt nötig ist.

      Richtig, ist hier (für mich offensichtlich) nötig.

      Liebe Grüße

      Felix Riesterer

      1. Hallo Felix,

        fetch-Aufrufe, die in der Reihenfolge ABCDE abgesetzt werden, in einer anderen Reihenfolge (z.B. ACDEB) am Server ankommen

        Du hast zu wenig Code gezeigt, um erkennen zu können, wie der Multifile-Upload gesteuert wird. Möglicherweise ist da was falsch.

        Für eine einzelne Datei sollten die Chunks immer in richtiger Reihenfolge kommen. Wenn Du aber mehrere Dateien hast, bspw mit Chunks ABCD für Datei 1, KLM für Datei 2 und PQR für Datei 3, und dann in einer Schleife für alle 3 Dateien den Upload startest, würde ich beim Server eine Ankunft in Reihenfolge AKPBLQCMRD erwarten. Muss aber nicht sein, wenn der Server mehr als einen Request gleichzeitig verarbeiten kann, dann können auch 2 Segmente echt parallel ankommen. DESWEGEN habe ich Dir eine Temp-Datei pro Upload-Datei empfohlen.

        Wenn Du mit einer Temp- Datei pro User auskommen willst, musst du den Übergang von Datei i nach i+1 in dem .then Handler lösen, wo du auch die Chunks verwaltest. Wenn Chunk m/m von Datei 1 fertig ist, machst Du mit Chunk 1/n von Datei 2 weiter.

        Mit await ließe sich das allerdings deutlich bequemer formulieren.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Lieber Rolf,

          würde ich beim Server eine Ankunft in Reihenfolge AKPBLQCMRD erwarten.

          das war bei mir nicht zu beobachten.

          Mit await ließe sich das allerdings deutlich bequemer formulieren.

          Wahrscheinlich, ja. Dafür müsste ich aber viel an meinem Code umstricken. Wenn mal ein Refactoring anstehen sollte, dann ja; dann lohnt sich der Aufwand.

          Da mein Projekt noch immer mit Funktionalitäten und Zuverlässigkeit überzeugen muss, um überhaupt weitere Verwendung zu finden, kann ich mir ein Refactoring gerade nicht leisten, wenn es keine Not tut. Würde ein Refactoring grundlegende Probleme lösen, wäre ich sofort dabei.

          Liebe Grüße

          Felix Riesterer

          1. Hallo Felix,

            du brauchst nur eine async-Funktion, die den Upload durchführt.

            Aufruf im Eventhandler für den Submit

            // Variante 1
            runUploads(fileInput.files)
            .then ( runCleanup );
            .catch ( error => ... );
            
            // Variante 2
            try {
               await runUploads(fileInput.files);
               runCleanup();
            }
            catch (error) {
               ... behandle error ...
            }
            

            Variante 1 ist klassische Promise-Verarbeitung und läuft überall. Variante 2 verwendet await und muss seinerseits in einer async-Funktion stehen. Aber der Eventhandler, der den Upload anstößt. kann ja problemlos async sein.

            Ich habe mal unterstellt, dass Du nach dem Upload noch etwas Cleanup machen musst. Wenn nicht, ok, dann ist das überflüssig.

            Die runUploads-Funktion:

            async function runUploads(fileList) {
               for (const file of fileList) {
                  const chunks = Math.ceil(file.size / CHUNKSIZE);
                  for (i=0; i<chunks; i++) {
                     const formData = new FormData();
                     // Chunk i in formData eintragen
                     const response = await fetch(
                         myURL,
                         { method: "POST", body: formData });
                     const jsonResult = await response.json();
                     if (jsonResult.error) {
                        handleError(jsonResult.error);
                        break;            // Dieses File abbrechen
                     }
                  }
               }
            }
            

            Da ist jetzt kein try/catch drin, d.h. FALLS ein Error fliegt, bricht runUploads ab und das Promise für ihren Aufruf wird gebrochen. Deshalb entweder ein .catch() an den Aufruf oder den Aufruf in try/catch kapseln.

            Wenn Du feingranularer auf Fehler reagieren willst, musst Du weitere try/catch einbauen.

            Ob Du bei result.error (was äquivalent zu deinem "error" in data sein dürfte nur einen break machst und damit die aktuelle Datei abbrichst oder gleich return durchführst und den Upload komplett beendest, ist ebenfalls deine Entscheidung.

            Ich finde, dass das eine sehr einfache Art ist, die Chunks sauber serialisieren zu können. async/await sollte man heutzutage überall voraussetzen können.

            Rolf

            --
            sumpsi - posui - obstruxi