Vraag Denken in JavaScript-beloften (in dit geval Bluebird)


Ik probeer mijn hoofd te krijgen rond een aantal niet zo alledaagse belofte / asynchrone use-cases. In een voorbeeld waar ik op dit moment mee worstel, heb ik een reeks boeken teruggestuurd van een knex-query (array die kan worden overgenomen) die ik in een database wil invoegen:

books.map(function(book) {

  // Insert into DB

});

Elk boekitem ziet er als volgt uit:

var book = {
    title: 'Book title',
    author: 'Author name'
};

Voordat ik echter elk boek toevoeg, moet ik de ID van de auteur uit een afzonderlijke tabel ophalen, omdat deze gegevens zijn genormaliseerd. De auteur kan al dan niet bestaan, dus ik moet:

  • Controleer of de auteur aanwezig is in de database
  • Als dit het geval is, gebruikt u deze ID
  • Plaats anders de auteur en gebruik de nieuwe ID

De bovenstaande bewerkingen zijn echter ook allemaal asynchroon.

Ik kan gewoon een belofte gebruiken in de originele kaart (halen en / of invoegen ID) als een vereiste voor de invoegbewerking. Maar het probleem is hier dat, omdat alles asynchroon wordt uitgevoerd, de code mogelijk dubbele auteurs invoegt omdat de aanvankelijke controle-als-auteur bestaat is ontkoppeld van het invoegen-een-nieuw-auteurblok.

Ik kan een paar manieren bedenken om het bovenstaande te bereiken, maar ze houden allemaal verband met het opsplitsen van de belofteketen en lijken in het algemeen een beetje rommelig. Dit lijkt het soort probleem dat vrij vaak moet voorkomen. Ik weet zeker dat ik hier iets fundamenteel mis!

Enige tips?


12
2018-05-12 14:05


oorsprong


antwoorden:


Laten we aannemen dat je elk boek parallel kunt verwerken. Dan is alles vrij eenvoudig (met alleen de ES6 API):

Promise
  .all(books.map(book => {
    return getAuthor(book.author)
          .catch(createAuthor.bind(null, book.author));
          .then(author => Object.assign(book, { author: author.id }))
          .then(saveBook);
  }))
  .then(() => console.log('All done'))

Het probleem is dat er een raceconditie is tussen het ophalen van de auteur en het maken van een nieuwe auteur. Overweeg de volgende volgorde van gebeurtenissen:

  • we proberen auteur A voor boek B te krijgen;
  • auteur A faalt;
  • we vragen auteur A aan te maken, maar deze is nog niet gemaakt;
  • we proberen auteur A voor boek C te krijgen;
  • auteur A faalt;
  • we vragen auteur A (opnieuw!) aan te maken;
  • eerste aanvraag is voltooid;
  • tweede aanvraag is voltooid;

Nu hebben we twee instanties van A in auteurstabel. Dit is slecht! Om dit probleem op te lossen kunnen we de traditionele aanpak gebruiken: vergrendeling. We moeten een tabel met per auteurssloten bijhouden. Wanneer we een verzoek tot creatie verzenden, vergrendelen we het juiste slot. Nadat het verzoek is voltooid, ontgrendelen we het. Alle andere bewerkingen waarbij dezelfde auteur betrokken is, moeten eerst de vergrendeling verkrijgen voordat ze iets doen.

Dit lijkt moeilijk, maar kan in ons geval aanzienlijk worden vereenvoudigd, omdat we onze verzoekbeloften kunnen gebruiken in plaats van sloten:

const authorPromises = {};

function getAuthor(authorName) {

  if (authorPromises[authorName]) {
    return authorPromises[authorName];
  }

  const promise = getAuthorFromDatabase(authorName)
    .catch(createAuthor.bind(null, authorName))
    .then(author => {
      delete authorPromises[authorName];
      return author;
    });

  authorPromises[author] = promise;

  return promise;
}

Promise
  .all(books.map(book => {
    return getAuthor(book.author)
          .then(author => Object.assign(book, { author: author.id }))
          .then(saveBook);
  }))
  .then(() => console.log('All done'))

Dat is het! Als een verzoek om auteur aan boord is, wordt dezelfde belofte teruggegeven.


8
2018-05-12 14:40



Dit is hoe ik het zou implementeren. Ik denk dat een aantal belangrijke vereisten zijn:

  • Er worden nooit dubbele auteurs gemaakt (dit zou ook een beperking in de database moeten zijn).
  • Als de server niet in het midden antwoordt, worden er geen inconsistente gegevens ingevoegd.
  • Mogelijkheid om meerdere auteurs in te voeren.
  • Maak niet n┬ávragen naar de database voor n┬ádingen - het klassieke "n + 1" probleem vermijden.

Ik zou een transactie gebruiken om ervoor te zorgen dat updates atomair zijn - dat wil zeggen als de bewerking wordt uitgevoerd en de client in het midden sterft - er geen auteurs zonder boeken worden gemaakt. Het is ook belangrijk dat een tijdelijke storing geen geheugenlek veroorzaakt (zoals in het antwoord met de auteurskaart die mislukte beloften houdt).

knex.transaction(Promise.coroutine(function*(t) {
    //get books inside the transaction
    var authors = yield books.map(x => x.author);
    // name should be indexed, this is a single query
    var inDb = yield t.select("authors").whereIn("name", authors);
    var notIn = authors.filter(author => !inDb.includes("author"));
    // now, perform a single multi row insert on the transaction
    // I'm assuming PostgreSQL here (return IDs), this is a bit different for SQLite
    var ids = yield t("authors").insert(notIn.map(name => {authorName: name });
    // update books _inside the transaction_ now with the IDs array
})).then(() => console.log("All done!"));

Dit heeft het voordeel dat het alleen een vast aantal vragen maakt en waarschijnlijk veiliger is en beter presteert. Bovendien bevindt uw database zich niet in een consistente staat (hoewel u de bewerking wellicht opnieuw moet uitvoeren voor meerdere exemplaren).


3
2018-05-13 06:10