MODIFIER - Performances des requêtes :
Comme @NeilLunn l'a souligné dans ses commentaires, vous ne devriez pas filtrer les documents manuellement, mais utiliser .find(...)
plutôt pour ça :
db.snapshots.find({
roundedDate: { $exists: true },
stream: { $exists: true },
sid: { $exists: false }
})
Aussi, en utilisant .bulkWrite()
, disponible à partir de MongoDB 3.2
, sera bien plus performant que de faire des mises à jour individuelles.
Il est possible qu'avec cela, vous puissiez exécuter votre requête dans les 10 minutes de durée de vie du curseur. Si cela prend encore plus que cela, votre curseur expirera et vous aurez de toute façon le même problème, qui est expliqué ci-dessous :
Que se passe-t-il ici :
Error: getMore command failed
peut être dû à un délai d'expiration du curseur, qui est lié à deux attributs de curseur :
-
Limite de délai d'attente, qui est de 10 minutes par défaut. À partir de la documentation :
Par défaut, le serveur fermera automatiquement le curseur après 10 minutes d'inactivité, ou si le client a épuisé le curseur.
-
Taille du lot, qui est de 101 documents ou 16 Mo pour le premier lot, et de 16 Mo, quel que soit le nombre de documents, pour les lots suivants (à partir de MongoDB
3.4
). À partir de la documentation :find()
etaggregate()
les opérations ont une taille de lot initiale de 101 documents par défaut. Les opérations getMore ultérieures émises sur le curseur résultant n'ont pas de taille de lot par défaut, elles ne sont donc limitées que par la taille du message de 16 mégaoctets.
Vous consommez probablement ces 101 documents initiaux, puis vous obtenez un lot de 16 Mo, ce qui est le maximum, avec beaucoup plus de documents. Comme il faut plus de 10 minutes pour les traiter, le curseur sur le serveur expire et, au moment où vous avez terminé de traiter les documents du deuxième lot et d'en demander un nouveau, le curseur est déjà fermé :
Au fur et à mesure que vous parcourez le curseur et atteignez la fin du lot renvoyé, s'il y a plus de résultats, cursor.next() effectuera une opération getMore pour récupérer le lot suivant.
Solutions possibles :
Je vois 5 façons possibles de résoudre ce problème, 3 bonnes, avec leurs avantages et leurs inconvénients, et 2 mauvaises :
-
👍 Réduire la taille du lot pour garder le curseur vivant.
-
👍 Supprimez le délai d'attente du curseur.
-
👍 Réessayez lorsque le curseur expire.
-
👎 Interrogez manuellement les résultats par lots.
-
👎 Obtenez tous les documents avant l'expiration du curseur.
Notez qu'ils ne sont pas numérotés selon des critères spécifiques. Lisez-les et décidez lequel convient le mieux à votre cas particulier.
1. 👍 Réduire la taille du lot pour garder le curseur vivant
Une façon de résoudre ce problème est d'utiliser cursor.bacthSize
pour définir la taille du lot sur le curseur renvoyé par votre find
requête pour correspondre à celles que vous pouvez traiter dans ces 10 minutes :
const cursor = db.collection.find()
.batchSize(NUMBER_OF_DOCUMENTS_IN_BATCH);
Cependant, gardez à l'esprit que la définition d'une taille de lot très conservatrice (petite) fonctionnera probablement, mais sera également plus lente, car vous devez maintenant accéder au serveur plus de fois.
D'autre part, le définir sur une valeur trop proche du nombre de documents que vous pouvez traiter en 10 minutes signifie qu'il est possible que si certaines itérations prennent un peu plus de temps à traiter pour une raison quelconque (d'autres processus peuvent consommer plus de ressources) , le curseur expirera de toute façon et vous obtiendrez à nouveau la même erreur.
2. 👍 Supprimez le délai d'attente du curseur
Une autre option consiste à utiliser cursor.noCursorTimeout pour empêcher le curseur d'expirer :
const cursor = db.collection.find().noCursorTimeout();
Ceci est considéré comme une mauvaise pratique car vous auriez besoin de fermer le curseur manuellement ou d'épuiser tous ses résultats pour qu'il se ferme automatiquement :
Après avoir défini le
noCursorTimeout
option, vous devez soit fermer le curseur manuellement aveccursor.close()
ou en épuisant les résultats du curseur.
Comme vous voulez traiter tous les documents dans le curseur, vous n'auriez pas besoin de le fermer manuellement, mais il est toujours possible que quelque chose d'autre se passe mal dans votre code et qu'une erreur soit renvoyée avant que vous ayez terminé, laissant ainsi le curseur ouvert .
Si vous souhaitez toujours utiliser cette approche, utilisez un try-catch
pour vous assurer de fermer le curseur en cas de problème avant de consommer tous ses documents.
Notez que je ne considère pas cela comme une mauvaise solution (d'où le 👍), car même pensé que c'est considéré comme une mauvaise pratique... :
-
C'est une fonctionnalité prise en charge par le pilote. Si c'était si grave, car il existe d'autres moyens de contourner les problèmes de délai d'attente, comme expliqué dans les autres solutions, cela ne sera pas pris en charge.
-
Il existe des moyens de l'utiliser en toute sécurité, il suffit d'être très prudent avec.
-
Je suppose que vous n'exécutez pas régulièrement ce type de requêtes, donc les chances que vous commenciez à laisser des curseurs ouverts partout sont faibles. Si ce n'est pas le cas et que vous devez vraiment gérer ces situations tout le temps, il est logique de ne pas utiliser
noCursorTimeout
.
3. 👍 Réessayer lorsque le curseur expire
En gros, vous mettez votre code dans un try-catch
et lorsque vous obtenez l'erreur, vous obtenez un nouveau curseur en sautant les documents que vous avez déjà traités :
let processed = 0;
let updated = 0;
while(true) {
const cursor = db.snapshots.find().sort({ _id: 1 }).skip(processed);
try {
while (cursor.hasNext()) {
const doc = cursor.next();
++processed;
if (doc.stream && doc.roundedDate && !doc.sid) {
db.snapshots.update({
_id: doc._id
}, { $set: {
sid: `${ doc.stream.valueOf() }-${ doc.roundedDate }`
}});
++updated;
}
}
break; // Done processing all, exit outer loop
} catch (err) {
if (err.code !== 43) {
// Something else than a timeout went wrong. Abort loop.
throw err;
}
}
}
Notez que vous devez trier les résultats pour que cette solution fonctionne.
Avec cette approche, vous minimisez le nombre de requêtes au serveur en utilisant la taille de lot maximale possible de 16 Mo, sans avoir à deviner combien de documents vous pourrez traiter en 10 minutes au préalable. Par conséquent, elle est également plus robuste que l'approche précédente.
4. 👎 Interrogez manuellement les résultats par lots
Fondamentalement, vous utilisez skip(), limit() et sort() pour effectuer plusieurs requêtes avec un certain nombre de documents que vous pensez pouvoir traiter en 10 minutes.
Je considère cela comme une mauvaise solution car le pilote a déjà la possibilité de définir la taille du lot, il n'y a donc aucune raison de le faire manuellement, utilisez simplement la solution 1 et ne réinventez pas la roue.
Aussi, il convient de mentionner qu'elle présente les mêmes inconvénients que la solution 1,
5. 👎 Obtenez tous les documents avant l'expiration du curseur
Votre code prend probablement un certain temps à s'exécuter en raison du traitement des résultats, vous pouvez donc d'abord récupérer tous les documents, puis les traiter :
const results = new Array(db.snapshots.find());
Cela récupérera tous les lots les uns après les autres et fermera le curseur. Ensuite, vous pouvez parcourir tous les documents dans results
et faites ce que vous devez faire.
Cependant, si vous rencontrez des problèmes de délai d'attente, il est probable que votre ensemble de résultats soit assez volumineux. Par conséquent, tout extraire en mémoire n'est peut-être pas la chose la plus conseillée à faire.
Remarque sur le mode instantané et les documents en double
Il est possible que certains documents soient renvoyés plusieurs fois si des opérations d'écriture intermédiaires les déplacent en raison d'une augmentation de la taille du document. Pour résoudre ce problème, utilisez cursor.snapshot()
. À partir de la documentation :
Ajoutez la méthode snapshot() à un curseur pour basculer en mode « snapshot ». Cela garantit que la requête ne renverra pas un document plusieurs fois, même si des opérations d'écriture intermédiaires entraînent un déplacement du document en raison de l'augmentation de la taille du document.
Cependant, gardez à l'esprit ses limites :
-
Cela ne fonctionne pas avec les collections fragmentées.
-
Cela ne fonctionne pas avec
sort()
ouhint()
, cela ne fonctionnera donc pas avec les solutions 3 et 4. -
Il ne garantit pas l'isolement de l'insertion ou de la suppression.
Notez qu'avec la solution 5, la fenêtre de temps pour avoir un déplacement de documents pouvant entraîner la récupération de documents en double est plus étroite qu'avec les autres solutions, vous n'aurez donc peut-être pas besoin de snapshot()
.
Dans votre cas particulier, comme la collection s'appelle snapshot
, il est peu probable qu'il change, donc vous n'avez probablement pas besoin de snapshot()
. De plus, vous effectuez des mises à jour sur des documents en fonction de leurs données et, une fois la mise à jour effectuée, ce même document ne sera plus mis à jour même s'il est récupéré plusieurs fois, car le if
condition l'ignorera.
Remarque sur les curseurs ouverts
Pour voir le nombre de curseurs ouverts, utilisez db.serverStatus().metrics.cursor
.