EDIT - Desempenho da consulta:
Como @NeilLunn apontou em seus comentários, você não deve filtrar os documentos manualmente, mas usar
.find(...)
para isso em vez disso:db.snapshots.find({
roundedDate: { $exists: true },
stream: { $exists: true },
sid: { $exists: false }
})
Além disso, usando
.bulkWrite()
, disponível a partir do MongoDB 3.2
, terá muito mais desempenho do que fazer atualizações individuais. É possível que, com isso, você consiga executar sua consulta dentro do tempo de vida de 10 minutos do cursor. Se ainda demorar mais do que isso, seu cursor expirará e você terá o mesmo problema de qualquer maneira, explicado abaixo:
O que está acontecendo aqui:
Error: getMore command failed
pode ser devido a um tempo limite do cursor, que está relacionado a dois atributos do cursor:-
Limite de tempo limite, que é de 10 minutos por padrão. Dos documentos:
Por padrão, o servidor fechará automaticamente o cursor após 10 minutos de inatividade ou se o cliente tiver esgotado o cursor.
-
Tamanho do lote, que é de 101 documentos ou 16 MB para o primeiro lote e 16 MB, independentemente do número de documentos, para lotes subsequentes (a partir do MongoDB3.4
). Dos documentos:
find()
eaggregate()
operações têm um tamanho de lote inicial de 101 documentos por padrão. As operações getMore subsequentes emitidas no cursor resultante não têm tamanho de lote padrão, portanto, são limitadas apenas pelo tamanho da mensagem de 16 megabytes.
Provavelmente você está consumindo esses 101 documentos iniciais e depois obtendo um lote de 16 MB, que é o máximo, com muito mais documentos. Como está demorando mais de 10 minutos para processá-los, o cursor no servidor expira e, quando você termina de processar os documentos no segundo lote e solicita um novo, o cursor já está fechado:
À medida que você percorre o cursor e chega ao final do lote retornado, se houver mais resultados, cursor.next() executará uma operação getMore para recuperar o próximo lote.
Soluções possíveis:
Eu vejo 5 maneiras possíveis de resolver isso, 3 boas, com seus prós e contras, e 2 ruins:
-
👍 Reduzindo o tamanho do lote para manter o cursor vivo.
-
👍 Remova o tempo limite do cursor.
-
👍 Tente novamente quando o cursor expirar.
-
👎 Consulte os resultados em lotes manualmente.
-
👎 Obtenha todos os documentos antes que o cursor expire.
Observe que eles não são numerados seguindo nenhum critério específico. Leia-os e decida qual deles funciona melhor para o seu caso particular.
1. 👍 Reduzindo o tamanho do lote para manter o cursor ativo
Uma maneira de resolver isso é usar
cursor.bacthSize
para definir o tamanho do lote no cursor retornado por seu find
query para corresponder àqueles que você pode processar dentro desses 10 minutos:const cursor = db.collection.find()
.batchSize(NUMBER_OF_DOCUMENTS_IN_BATCH);
No entanto, lembre-se de que definir um tamanho de lote muito conservador (pequeno) provavelmente funcionará, mas também será mais lento, pois agora você precisa acessar o servidor mais vezes.
Por outro lado, configurá-lo para um valor muito próximo ao número de documentos que você pode processar em 10 minutos significa que é possível que, se algumas iterações demorem um pouco mais para serem processadas por qualquer motivo (outros processos podem estar consumindo mais recursos) , o cursor expirará de qualquer maneira e você receberá o mesmo erro novamente.
2. 👍 Remova o tempo limite do cursor
Outra opção é usar cursor.noCursorTimeout para evitar que o cursor expire:
const cursor = db.collection.find().noCursorTimeout();
Isso é considerado uma prática ruim, pois você precisaria fechar o cursor manualmente ou esgotar todos os seus resultados para que ele fosse fechado automaticamente:
Depois de definir onoCursorTimeout
opção, você deve fechar o cursor manualmente comcursor.close()
ou esgotando os resultados do cursor.
Como você deseja processar todos os documentos no cursor, não precisaria fechá-lo manualmente, mas ainda é possível que algo dê errado no seu código e um erro seja lançado antes de você terminar, deixando o cursor aberto .
Se você ainda quiser usar essa abordagem, use um
try-catch
para certificar-se de fechar o cursor se algo der errado antes de consumir todos os seus documentos. Note que não considero isso uma solução ruim (por isso o 👍), pois até pensei que é considerada uma má prática...:
-
É um recurso suportado pelo driver. Se fosse tão ruim, como existem maneiras alternativas de contornar problemas de tempo limite, conforme explicado nas outras soluções, isso não será suportado.
-
Existem maneiras de usá-lo com segurança, é apenas uma questão de ser mais cauteloso com ele.
-
Suponho que você não esteja executando esse tipo de consulta regularmente, portanto, as chances de você começar a deixar cursores abertos em todos os lugares são baixas. Se este não for o caso, e você realmente precisar lidar com essas situações o tempo todo, então faz sentido não usarnoCursorTimeout
.
3. 👍 Tente novamente quando o cursor expirar
Basicamente, você coloca seu código em um
try-catch
e quando você recebe o erro, você recebe um novo cursor pulando os documentos que você já processou: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;
}
}
}
Observe que você precisa classificar os resultados para que essa solução funcione.
Com essa abordagem, você está minimizando o número de solicitações ao servidor usando o tamanho de lote máximo possível de 16 MB, sem precisar adivinhar quantos documentos você poderá processar em 10 minutos antes. Portanto, também é mais robusto do que a abordagem anterior.
4. 👎 Consulte os resultados em lotes manualmente
Basicamente, você usa skip(), limit() e sort() para fazer várias consultas com vários documentos que você acha que pode processar em 10 minutos.
Considero isso uma solução ruim porque o driver já tem a opção de definir o tamanho do lote, então não há motivo para fazer isso manualmente, basta usar a solução 1 e não reinventar a roda.
Além disso, vale a pena mencionar que tem as mesmas desvantagens que a solução 1,
5. 👎 Obtenha todos os documentos antes que o cursor expire
Provavelmente seu código está demorando para ser executado devido ao processamento de resultados, então você pode recuperar todos os documentos primeiro e depois processá-los:
const results = new Array(db.snapshots.find());
Isso recuperará todos os lotes um após o outro e fechará o cursor. Em seguida, você pode percorrer todos os documentos dentro de
results
e fazer o que você precisa fazer. No entanto, se você estiver tendo problemas de tempo limite, é provável que seu conjunto de resultados seja muito grande, portanto, puxar tudo na memória pode não ser a coisa mais aconselhável a fazer.
Observação sobre o modo instantâneo e documentos duplicados
É possível que alguns documentos sejam retornados várias vezes se operações de gravação intervenientes os moverem devido a um aumento no tamanho do documento. Para resolver isso, use
cursor.snapshot()
. Dos documentos:
Anexe o método snapshot() a um cursor para alternar o modo “snapshot”. Isso garante que a consulta não retornará um documento várias vezes, mesmo que as operações de gravação intervenientes resultem em uma movimentação do documento devido ao aumento do tamanho do documento.
No entanto, tenha em mente suas limitações:
-
Não funciona com coleções fragmentadas.
-
Não funciona comsort()
ouhint()
, portanto não funcionará com as soluções 3 e 4.
-
Não garante o isolamento de inserções ou exclusões.
Observe com a solução 5 que a janela de tempo para ter uma movimentação de documentos que pode causar a recuperação de documentos duplicados é mais estreita do que com as outras soluções, portanto, você pode não precisar de
snapshot()
. No seu caso particular, como a coleção é chamada de
snapshot
, provavelmente não é provável que mude, então você provavelmente não precisa de snapshot()
. Além disso, você está fazendo atualizações em documentos com base em seus dados e, uma vez feita a atualização, esse mesmo documento não será atualizado novamente, mesmo que seja recuperado várias vezes, como o if
condição irá ignorá-lo. Observação sobre cursores abertos
Para ver uma contagem de cursores abertos use
db.serverStatus().metrics.cursor
.