mitmario.dev

Synthese: viele Dateien auf einmal

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 6 von 7

Die letzten fünf Lektionen zusammengenommen ergeben ein Muster, das dir in echtem Node-Code ständig begegnet: eine Liste, für jeden Eintrag eine asynchrone Aufgabe, am Ende eine Auswertung.

Neun Dateien einlesen. Vierzig Adressen prüfen. Zwölf Bilder umrechnen. Hundert Datensätze in eine Schnittstelle schieben. Immer dieselbe Form.

Warum hier allSettled richtig ist

Bei einem Stapel Fremddaten ist Promise.all fast immer die falsche Wahl. Nicht weil es schlechter wäre, sondern weil die Fragestellung eine andere ist.

Promise.all sagt: Ich brauche alle, sonst kann ich nicht weiterarbeiten. Das passt, wenn du drei Teile einer Seite lädst und ohne einen davon nichts anzeigen kannst.

Promise.allSettled sagt: Ich verarbeite, was da ist, und über den Rest berichte ich. Das passt bei einem Eingangsordner, einem Import, einer Auswertung. Genau dort ist eine kaputte Datei kein Grund, die anderen acht wegzuwerfen.

Die Faustregel lautet: Ist ein Fehlschlag eine Nachricht oder ein Abbruch? Ist er eine Nachricht, nimm allSettled.

Das Ergebnis aufteilen

allSettled gibt dir eine gemischte Liste zurück, und du willst sie meistens getrennt haben. Der naheliegende Weg geht zweimal durch.

Zweimal filtern
const pruefen = async (nummer) => {
  if (nummer % 3 === 0) throw new Error(`${nummer} ist kaputt`);
  return `Datensatz ${nummer}`;
};

const ergebnisse = await Promise.allSettled([1, 2, 3, 4, 5, 6].map(pruefen));

const gute = ergebnisse.filter((e) => e.status === "fulfilled").map((e) => e.value);
const schlechte = ergebnisse.filter((e) => e.status === "rejected").map((e) => e.reason.message);

console.log(`${gute.length} gut, ${schlechte.length} kaputt`);
console.log(schlechte.join(" | "));

Das ist gut lesbar und für sechs Einträge völlig in Ordnung. Zwei Dinge stören trotzdem: Die Liste wird viermal durchlaufen statt einmal, und wichtiger, die beiden Zeilen kann man auseinanderlaufen lassen. Ändert jemand die eine Bedingung und die andere nicht, zählt am Ende etwas doppelt oder gar nicht.

In einem Durchgang aufteilen
const pruefen = async (nummer) => {
  if (nummer % 3 === 0) throw new Error(`${nummer} ist kaputt`);
  return `Datensatz ${nummer}`;
};

const ergebnisse = await Promise.allSettled([1, 2, 3, 4, 5, 6].map(pruefen));

const gute = [];
const schlechte = [];

for (const eintrag of ergebnisse) {
  if (eintrag.status === "fulfilled") gute.push(eintrag.value);
  else schlechte.push(eintrag.reason.message);
}

console.log(`${gute.length} gut, ${schlechte.length} kaputt`);
console.log(schlechte.join(" | "));

Eine Schleife, eine Entscheidung, zwei Listen. Jeder Eintrag landet garantiert in genau einer davon, und das bleibt auch so, wenn später jemand etwas ergänzt.

Wenn wirklich viele Aufgaben anstehen

Bis hierhin gilt: Alles gleichzeitig starten und einmal warten. Bei einer festen, kleinen Liste ist das richtig.

Bei einer Liste, deren Länge von außen kommt, wird es zum Problem. Zweitausend Dateien heißt zweitausend offene Dateihandles. Zweitausend Anfragen an eine fremde Schnittstelle heißt eine Sperre für deine IP-Adresse, oft schon bei deutlich weniger.

Dagegen braucht es eine Obergrenze, und die kann man von Hand bauen.

Eine Obergrenze von Hand
const mitGrenze = async (eintraege, grenze, arbeit) => {
  const ergebnisse = [];
  let naechster = 0;

  const arbeiter = async () => {
    while (naechster < eintraege.length) {
      const i = naechster;
      naechster += 1;
      ergebnisse[i] = await arbeit(eintraege[i]);
    }
  };

  await Promise.all(Array.from({ length: grenze }, arbeiter));
  return ergebnisse;
};

let gleichzeitig = 0;
let hoechststand = 0;

const ergebnis = await mitGrenze([1, 2, 3, 4, 5, 6, 7, 8], 3, async (zahl) => {
  gleichzeitig += 1;
  hoechststand = Math.max(hoechststand, gleichzeitig);
  await new Promise((fertig) => setTimeout(fertig, 20));
  gleichzeitig -= 1;
  return zahl * 2;
});

console.log(`Ergebnis: ${ergebnis.join(",")}`);
console.log(`Hoechstens gleichzeitig: ${hoechststand}`);

Der Trick sind die Arbeiter. Statt alle Aufgaben auf einmal zu starten, startest du drei Schleifen, und jede holt sich die nächste freie Aufgabe, bis keine mehr da ist. Damit laufen nie mehr als drei gleichzeitig, und trotzdem ist immer Arbeit unterwegs.

ergebnisse[i] sorgt dafür, dass die Ergebnisse an ihrer ursprünglichen Stelle landen. Wer wann fertig wird, spielt keine Rolle.

Fünfzehn Zeilen, kein Paket, keine Abhängigkeit. Es gibt fertige Bibliotheken dafür, und die können mehr. Aber wenn du das Muster einmal selbst gebaut hast, weißt du, was sie tun, und kannst entscheiden, ob du sie brauchst.

Und jetzt zusammen

In der Challenge zu dieser Lektion kommt alles zusammen: ein Ordner voller Dateien, alle gleichzeitig gelesen, kaputte gezählt statt zum Abbruch gebracht, das Ergebnis in eine Berichtsdatei geschrieben.

Das ist kein Übungsbeispiel, sondern die Sorte Skript, die in jedem Projekt irgendwann herumliegt.

Zum Mitnehmen

Eine Liste, für jeden Eintrag eine asynchrone Aufgabe, am Ende eine Auswertung. Das Muster kommt in echten Programmen ständig vor.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.