Summary
In this post, I discuss the symptoms and workaround for an atomicity bug in MongoDB. This error manifests itself when performing collection updates from multiple threads, with upsert:true, on a unique key. The bug has been open since 2014 and still unresolved. https://jira.mongodb.org/browse/SERVER-14322
Bug Set Up
/*jshint esversion: 6 */ 'use strict'; 'use esversion 6'; const MongoClient = require('mongodb').MongoClient; const CONNECTION_URL = 'mongodb://admin:mongo@localhost:27017'; const DB_NAME = 'testDB'; const COLLECTION = 'testCollection'; let connection; let promises = []; function update(i, db, query, value, upsrt) { return db.collection(COLLECTION).updateOne(query, value, {'upsert' : upsrt}) .then((result) => { return new Promise((resolve, reject) => { resolve('i: ' + i + ' time: ' + new Date().getTime());}); }) .catch(err => { throw err; }); } MongoClient.connect(CONNECTION_URL, {useNewUrlParser : true}) .then((result) => { connection = result; const db = connection.db(DB_NAME); for (let i=0; i<5 .then="" :="" addtoset="" db="" funds="" i="" id="" indices="" johndoe="" let="" p="" promise.all="" promises.push="" promises="" query="" results="" return="" set="" true="" value=""> { results.forEach((result) => { console.log('result - ' + result); }); }) .catch(err => { console.log(err); }) .finally(() => { console.log('closing db connection'); connection.close(); }); 5>
Explanation
The Node.js code above creates an array of promises, each of which attempts to perform a db update with upsert equal to 'true'. The promises sent thru Promise.all to execute them all as a batch.
Results
The duplicate key error is generated, as expected.
{ MongoError: E11000 duplicate key error collection: testDB.testCollection index: _id_ dup key: { : "johnDoe" } at Function.create (/nas/archive/dev/workspace/blackjack/node_modules/mongodb-core/lib/error.js:43:12) at toError (/nas/archive/dev/workspace/blackjack/node_modules/mongodb/lib/utils.js:149:22) at coll.s.topology.update (/nas/archive/dev/workspace/blackjack/node_modules/mongodb/lib/operations/collection_ops.js:1399:39) at /nas/archive/dev/workspace/blackjack/node_modules/mongodb-core/lib/connection/pool.js:532:18 at process.internalTickCallback (internal/process/next_tick.js:70:11) driver: true, name: 'MongoError', index: 0, code: 11000, errmsg: 'E11000 duplicate key error collection: testDB.testCollection index: _id_ dup key: { : "johnDoe" }', [Symbol(mongoErrorContextSymbol)]: {} } closing db connectionBelow is the output of a read of this collection. One update (#4 index) completed before the error was thrown and halted the execution.
> db.testCollection.find({}); { "_id" : "johnDoe", "funds" : 400, "indices" : [ 4 ] }
Workaround
function updateWithRetry(i, db, query, value, upsrt) { return db.collection(COLLECTION).updateOne(query, value, {'upsert' : upsrt}) .then((result) => { return new Promise((resolve, reject) => { resolve('i: ' + i + ' time: ' + new Date().getTime());}); }) .catch(err => { if (err.code === 11000){ console.log('i: ' + i + ' 11000 error'); return updateWithRetry(i, db, query, value, false); } else { throw err; } }); }
Explanation
In this revised update block, I added a check for the 11000 error code. If that's, in fact, the source of the error, I retry the update with a recursive call.
Results
i: 0 11000 error i: 2 11000 error i: 3 11000 error i: 4 11000 error result - i: 0 time: 1546649403149 result - i: 1 time: 1546649403146 result - i: 2 time: 1546649403149 result - i: 3 time: 1546649403149 result - i: 4 time: 1546649403149 closing db connection
> db.testCollection.find({}); { "_id" : "johnDoe", "funds" : 400, "indices" : [ 1, 0, 2, 3, 4 ] }
Source
Copyright ©1993-2024 Joey E Whelan, All rights reserved.