parallel - Pensando en las promesas de JavaScript(Bluebird en este caso)
promise javascript bluebird (2)
Supongamos que puede procesar cada libro en paralelo. Entonces, todo es bastante simple (usando solo 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''))
El problema es que hay una condición de carrera entre obtener autor y crear un nuevo autor. Considere el siguiente orden de eventos:
- tratamos de obtener el autor A para el libro B;
- obtener el autor A falla;
- solicitamos la creación del autor A, pero aún no se ha creado;
- tratamos de obtener el autor A para el libro C;
- obtener el autor A falla;
- solicitamos crear el autor A (otra vez!);
- la primera solicitud se completa;
- segunda solicitud completa;
Ahora tenemos dos instancias de A en la tabla de autor. ¡Esto es malo! Para resolver este problema, podemos usar el enfoque tradicional: bloqueo. Necesitamos mantener una tabla de cerraduras por autor. Cuando enviamos una solicitud de creación, bloqueamos el bloqueo apropiado. Una vez completada la solicitud, la desbloqueamos. Todas las demás operaciones que involucren al mismo autor deben adquirir el candado primero antes de hacer cualquier cosa.
Esto parece difícil, pero se puede simplificar mucho en nuestro caso, ya que podemos utilizar nuestras promesas de solicitud en lugar de bloqueos:
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''))
¡Eso es! Ahora, si una solicitud de autor es a bordo, se devolverá la misma promesa.
Estoy tratando de entender algunos casos de uso no tan triviales y asincrónicos. En un ejemplo con el que estoy luchando en este momento, tengo una serie de libros devueltos por una consulta de knex (la matriz habilitable) que deseo insertar en una base de datos:
books.map(function(book) {
// Insert into DB
});
Cada elemento del libro se ve así:
var book = {
title: ''Book title'',
author: ''Author name''
};
Sin embargo, antes de insertar cada libro, necesito recuperar el ID del autor de una tabla separada ya que estos datos están normalizados. El autor puede o no existir, entonces necesito:
- Compruebe si el autor está presente en la base de datos
- Si es así, use esta ID
- De lo contrario, inserte el autor y use la nueva ID
Sin embargo, las operaciones anteriores también son todas asincrónicas.
Solo puedo usar una promesa dentro del mapa original (buscar y / o insertar ID) como un requisito previo de la operación de inserción. Pero el problema aquí es que, debido a que todo se ejecuta de forma asíncrona, el código puede insertar autores duplicados porque el check-if-author-exists inicial está desacoplado del bloque insert-a-new-author.
Puedo pensar en algunas formas de lograr lo anterior, pero todas implican dividir la cadena de promesas y, en general, parecen un poco desordenadas. Este parece ser el tipo de problema que debe surgir con bastante frecuencia. ¡Estoy seguro de que me estoy perdiendo algo fundamental aquí!
¿Algun consejo?
Aquí es cómo lo implementaría. Creo que algunos requisitos importantes son:
- Nunca se crean autores duplicados (esto debería ser una restricción en la base de datos también).
- Si el servidor no responde en el medio, no se insertan datos incoherentes.
- Posibilidad de ingresar a múltiples autores.
- No realice
n
consultas a la base de datos porn
cosas, evitando el problema clásico de "n + 1".
Usaría una transacción, para asegurarme de que las actualizaciones sean atómicas, es decir, si la operación se ejecuta y el cliente muere en el medio, no se crean autores sin libros. También es importante que una falla temporal no provoque una pérdida de memoria (como en la respuesta con el mapa de autores que mantiene las promesas incumplidas).
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!"));
Esto tiene la ventaja de hacer solo un número fijo de consultas y es probable que sea más seguro y tenga un mejor rendimiento. Además, su base de datos no está en un estado consistente (aunque es posible que deba volver a intentar la operación para varias instancias).