diff options
author | Randolph Tan <randolph@10gen.com> | 2014-03-05 12:22:58 -0500 |
---|---|---|
committer | Randolph Tan <randolph@10gen.com> | 2014-03-14 10:29:28 -0400 |
commit | e44682821c37fdf3d4fd8cb58dcf5c34181ddbde (patch) | |
tree | 55b6d6b53a0f13703de7d9d433cb6f53a5b97c95 | |
parent | cde863db6f180479e250ea3e2172f624be3d2b18 (diff) | |
download | mongo-e44682821c37fdf3d4fd8cb58dcf5c34181ddbde.tar.gz |
SERVER-12954 w parameter is not propagated correctly on compatibility writeMode
Send resetError only when absolutely necessary.
-rw-r--r-- | jstests/core/bulk_legacy_enforce_gle.js | 100 | ||||
-rw-r--r-- | jstests/multiVersion/mongos_downconvert_writes.js | 14 | ||||
-rw-r--r-- | src/mongo/s/write_ops/batch_downconvert.cpp | 143 | ||||
-rw-r--r-- | src/mongo/s/write_ops/batch_downconvert.h | 6 | ||||
-rw-r--r-- | src/mongo/s/write_ops/batch_downconvert_test.cpp | 36 | ||||
-rw-r--r-- | src/mongo/s/write_ops/batch_upconvert.cpp | 11 | ||||
-rw-r--r-- | src/mongo/s/write_ops/dbclient_safe_writer.cpp | 16 | ||||
-rw-r--r-- | src/mongo/s/write_ops/dbclient_safe_writer.h | 2 | ||||
-rw-r--r-- | src/mongo/shell/bulk_api.js | 34 |
9 files changed, 280 insertions, 82 deletions
diff --git a/jstests/core/bulk_legacy_enforce_gle.js b/jstests/core/bulk_legacy_enforce_gle.js new file mode 100644 index 00000000000..bec11749274 --- /dev/null +++ b/jstests/core/bulk_legacy_enforce_gle.js @@ -0,0 +1,100 @@ +/** + * Tests the resetError logic when the bulk api enforces the write concern for unordered + * writes. The tests indirectly checks whether resetError was called by inspecting the + * response of the getLastError command after executing the bulk ops. + */ + +var coll = db.bulk_legacy_enforce_gle; + +// batch of size 1 no error case. +coll.drop(); +var bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +assert.writeOK(bulk.execute()); + +var gle = db.runCommand({ getLastError: 1 }); +assert(gle.ok, tojson(gle)); +assert.eq(1, gle.n); + +// batch of size 1 should not call resetError even when it errors out. +coll.drop(); +coll.insert({ _id: 1 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +assert.writeError(bulk.execute()); + +gle = db.runCommand({ getLastError: 1 }); +assert(gle.ok, tojson(gle)); +assert.neq(null, gle.err); + +// batch with all error except last should not call resetError. +coll.drop(); +coll.insert({ _id: 1 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 0 }); +var res = assert.writeError(bulk.execute()); +assert.eq(2, res.getWriteErrors().length); + +gle = db.runCommand({ getLastError: 1 }); +assert(gle.ok, tojson(gle)); +assert.eq(1, gle.n); + +// batch with error at middle should not call resetError. +coll.drop(); +coll.insert({ _id: 1 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 0 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 2 }); +res = assert.writeError(bulk.execute()); +assert.eq(1, res.getWriteErrors().length); + +gle = db.runCommand({ getLastError: 1 }); +assert(gle.ok, tojson(gle)); +// mongos sends the bulk as one while the shell sends the write individually +assert.gte(gle.n, 1); + +// batch with error at last should call resetError. +coll.drop(); +coll.insert({ _id: 2 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 0 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 2 }); +res = assert.writeError(bulk.execute()); +assert.eq(1, res.getWriteErrors().length); + +gle = db.runCommand({ getLastError: 1 }); +assert(gle.ok, tojson(gle)); +assert.eq(0, gle.n); + +// batch with error at last should not call resetError if { w: 1 } +coll.drop(); +coll.insert({ _id: 2 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 0 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 2 }); +res = assert.writeError(bulk.execute()); +assert.eq(1, res.getWriteErrors().length); + +gle = db.runCommand({ getLastError: 1, w: 1 }); +assert(gle.ok, tojson(gle)); +assert.neq(null, gle.err); + +// batch with error at last should not call resetError if { w: 0 } +coll.drop(); +coll.insert({ _id: 2 }); +bulk = coll.initializeUnorderedBulkOp(); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 0 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 1 }); +bulk.find({ none: 1 }).upsert().updateOne({ _id: 2 }); +res = assert.writeError(bulk.execute()); +assert.eq(1, res.getWriteErrors().length); + +gle = db.runCommand({ getLastError: 1, w: 0 }); +assert(gle.ok, tojson(gle)); +assert.neq(null, gle.err); + diff --git a/jstests/multiVersion/mongos_downconvert_writes.js b/jstests/multiVersion/mongos_downconvert_writes.js new file mode 100644 index 00000000000..7361cae7f14 --- /dev/null +++ b/jstests/multiVersion/mongos_downconvert_writes.js @@ -0,0 +1,14 @@ +/** + * Tests the mongos downconversion, specifically the resetError logic when it enforces + * the write concern. + */ + +var st = new ShardingTest({ shards: 1, other: { shardOptions: { binVersion: '2.4' }}}); +st.stopBalancer(); + +var conn = st.s; +conn.forceWriteMode('commands'); +db = conn.getDB('test'); +load('./jstests/core/bulk_legacy_enforce_gle.js'); + +st.stop(); diff --git a/src/mongo/s/write_ops/batch_downconvert.cpp b/src/mongo/s/write_ops/batch_downconvert.cpp index 66475d8a39a..e3d706d0a83 100644 --- a/src/mongo/s/write_ops/batch_downconvert.cpp +++ b/src/mongo/s/write_ops/batch_downconvert.cpp @@ -28,6 +28,7 @@ #include "mongo/s/write_ops/batch_downconvert.h" +#include "mongo/db/write_concern_options.h" #include "mongo/util/assert_util.h" namespace mongo { @@ -175,19 +176,14 @@ namespace mongo { break; BatchItemRef itemRef( &request, static_cast<int>( i ) ); - bool isLastItem = ( i == request.sizeWriteOps() - 1 ); - - BSONObj writeConcern; - if ( isLastItem && request.isWriteConcernSet() ) { - writeConcern = request.getWriteConcern(); - // Pre-2.4.2 mongods react badly to 'w' being set on config servers - if ( nss.db() == "config" ) - writeConcern = fixWCForConfig( writeConcern ); - } BSONObj gleResult; GLEErrors errors; - Status status = _safeWriter->safeWrite( conn, itemRef, writeConcern, &gleResult ); + Status status = _safeWriter->safeWrite( conn, + itemRef, + WriteConcernOptions::Acknowledged, + &gleResult ); + if ( status.isOK() ) { status = extractGLEErrors( gleResult, &errors ); } @@ -200,6 +196,10 @@ namespace mongo { return; } + if ( errors.wcError.get() ) { + response->setWriteConcernError( errors.wcError.release() ); + } + // // STATS HANDLING // @@ -227,67 +227,88 @@ namespace mongo { response->setLastOp( stats.lastOp ); - // - // WRITE ERROR HANDLING - // - - // If any error occurs (except stale config) the previous GLE was not enforced - bool enforcedWC = !errors.writeError.get() - || errors.writeError->getErrCode() == ErrorCodes::StaleShardVersion; - // Save write error if ( errors.writeError.get() ) { errors.writeError->setIndex( i ); response->addToErrDetails( errors.writeError.release() ); } + } - // - // WRITE CONCERN ERROR HANDLING - // + // + // WRITE CONCERN ERROR HANDLING + // + + // The last write is weird, since we enforce write concern and check the error through + // the same GLE if possible. If the last GLE was an error, the write concern may not + // have been enforced in that same GLE, so we need to send another after resetting the + // error. + + BSONObj writeConcern; + if ( request.isWriteConcernSet() ) { + writeConcern = request.getWriteConcern(); + // Pre-2.4.2 mongods react badly to 'w' being set on config servers + if ( nss.db() == "config" ) + writeConcern = fixWCForConfig( writeConcern ); + } - // The last write is weird, since we enforce write concern and check the error through - // the same GLE if possible. If the last GLE was an error, the write concern may not - // have been enforced in that same GLE, so we need to send another after resetting the - // error. - if ( isLastItem ) { - - // Try to enforce the write concern if everything succeeded (unordered or ordered) - // OR if something succeeded and we're unordered. - bool needToEnforceWC = - !response->isErrDetailsSet() - || ( !request.getOrdered() - && response->sizeErrDetails() < request.sizeWriteOps() ); - - if ( !enforcedWC && needToEnforceWC ) { - dassert( !errors.writeError.get() ); // emptied above - - // Might have gotten a write concern validity error earlier, these are - // enforced even if the wc isn't applied, so we ignore. - errors.wcError.reset(); - - Status status = _safeWriter->enforceWriteConcern( conn, - nss.db().toString(), - writeConcern, - &gleResult ); - - if ( status.isOK() ) { - status = extractGLEErrors( gleResult, &errors ); - } - - if ( !status.isOK() ) { - response->clear(); - response->setOk( false ); - response->setErrCode( status.code() ); - response->setErrMessage( status.reason() ); - return; - } - } - // END Write concern retry + bool needToEnforceWC = WriteConcernOptions::Acknowledged.woCompare(writeConcern) != 0 && + WriteConcernOptions::Unacknowledged.woCompare(writeConcern) != 0; + + if ( needToEnforceWC && + ( !response->isErrDetailsSet() || + ( !request.getOrdered() && + // Not all errored. Note: implicit response->isErrDetailsSet(). + response->sizeErrDetails() < request.sizeWriteOps() ))) { + + // Might have gotten a write concern validity error earlier, these are + // enforced even if the wc isn't applied, so we ignore. + response->unsetWriteConcernError(); + + const string dbName( nss.db().toString() ); - if ( errors.wcError.get() ) { - response->setWriteConcernError( errors.wcError.release() ); + Status status( Status::OK() ); + + if ( response->isErrDetailsSet() ) { + const WriteErrorDetail* lastError = response->getErrDetails().back(); + + // If last write op was an error. + if ( lastError->getIndex() == static_cast<int>( request.sizeWriteOps() - 1 )) { + // Reset previous errors so we can apply the write concern no matter what + // as long as it is valid. + status = _safeWriter->clearErrors( conn, dbName ); } } + + if ( !status.isOK() ) { + response->clear(); + response->setOk( false ); + response->setErrCode( status.code() ); + response->setErrMessage( status.reason() ); + return; + } + + BSONObj gleResult; + status = _safeWriter->enforceWriteConcern( conn, + dbName, + writeConcern, + &gleResult ); + + GLEErrors errors; + if ( status.isOK() ) { + status = extractGLEErrors( gleResult, &errors ); + } + + if ( !status.isOK() ) { + response->clear(); + response->setOk( false ); + response->setErrCode( status.code() ); + response->setErrMessage( status.reason() ); + return; + } + + if ( errors.wcError.get() ) { + response->setWriteConcernError( errors.wcError.release() ); + } } response->setOk( true ); diff --git a/src/mongo/s/write_ops/batch_downconvert.h b/src/mongo/s/write_ops/batch_downconvert.h index cd5440ae993..521a48143eb 100644 --- a/src/mongo/s/write_ops/batch_downconvert.h +++ b/src/mongo/s/write_ops/batch_downconvert.h @@ -72,6 +72,12 @@ namespace mongo { const StringData& dbName, const BSONObj& writeConcern, BSONObj* gleResponse ) = 0; + + /** + * Clears the error information on this connection. + */ + virtual Status clearErrors( DBClientBase* conn, + const StringData& dbName ) = 0; }; /** diff --git a/src/mongo/s/write_ops/batch_downconvert_test.cpp b/src/mongo/s/write_ops/batch_downconvert_test.cpp index d103cf9caab..b253c0d94aa 100644 --- a/src/mongo/s/write_ops/batch_downconvert_test.cpp +++ b/src/mongo/s/write_ops/batch_downconvert_test.cpp @@ -221,7 +221,8 @@ namespace { public: MockSafeWriter( const vector<BSONObj>& gleResponses ) : - _gleResponses( gleResponses.begin(), gleResponses.end() ) { + _gleResponses( gleResponses.begin(), gleResponses.end() ), + _isClearCalled( false ) { } virtual ~MockSafeWriter() { @@ -241,19 +242,37 @@ namespace { const StringData& dbName, const BSONObj& writeConcern, BSONObj* gleResponse ) { + if (_gleResponses.empty()) { + return Status(ErrorCodes::IllegalOperation, "No more gle responses"); + } + BSONObj response = _gleResponses.front(); + *gleResponse = response.getOwned(); _gleResponses.pop_front(); - *gleResponse = response; return Status::OK(); } + Status clearErrors( DBClientBase* conn, + const StringData& dbName ) { + _isClearCalled = true; + return Status::OK(); + } + + bool isClearCalled() const { + return _isClearCalled; + } + deque<BSONObj> _gleResponses; + + private: + bool _isClearCalled; }; TEST(WriteBatchDownconvert, Basic) { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 1.0, err: null}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); MockSafeWriter mockWriter( gleResponses ); BatchSafeWriter batchWriter( &mockWriter ); @@ -270,12 +289,14 @@ namespace { ASSERT_EQUALS( response.getN(), 1 ); ASSERT( !response.isErrDetailsSet() ); ASSERT( !response.isWriteConcernErrorSet() ); + ASSERT( !mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, GLENotOk) { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 0.0, errmsg: 'message', code: 1234}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); MockSafeWriter mockWriter( gleResponses ); BatchSafeWriter batchWriter( &mockWriter ); @@ -292,12 +313,14 @@ namespace { ASSERT( !response.getOk() ); ASSERT_EQUALS( response.getErrCode(), 1234 ); ASSERT_EQUALS( response.getErrMessage(), "message" ); + ASSERT( !mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, BasicUpsert) { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 1.0, err: null, n: 1, upserted : 'abcde'}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); MockSafeWriter mockWriter( gleResponses ); BatchSafeWriter batchWriter( &mockWriter ); @@ -318,6 +341,7 @@ namespace { "abcde" ); ASSERT( !response.isErrDetailsSet() ); ASSERT( !response.isWriteConcernErrorSet() ); + ASSERT( !mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, WriteError) { @@ -327,6 +351,7 @@ namespace { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 1.0, err: 'message', code: 12345}" ) ); gleResponses.push_back( fromjson( "{ok: 1.0, err: null}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); MockSafeWriter mockWriter( gleResponses ); BatchSafeWriter batchWriter( &mockWriter ); @@ -348,6 +373,7 @@ namespace { ASSERT_EQUALS( response.getErrDetailsAt(0)->getErrMessage(), "message" ); ASSERT_EQUALS( response.getErrDetailsAt(0)->getErrCode(), 12345 ); ASSERT( !response.isWriteConcernErrorSet() ); + ASSERT( !mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, WriteErrorAndReplError) { @@ -356,6 +382,7 @@ namespace { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 1.0, err: 'message', code: 12345}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); gleResponses.push_back( fromjson( "{ok: 1.0, err: 'norepl', wnote: 'message'}" ) ); MockSafeWriter mockWriter( gleResponses ); @@ -370,7 +397,6 @@ namespace { BatchedCommandResponse response; batchWriter.safeWriteBatch( NULL, cmdRequest, &response ); - ASSERT( response.getOk() ); ASSERT_EQUALS( response.getN(), 1 ); ASSERT_EQUALS( response.sizeErrDetails(), 1u ); @@ -381,6 +407,7 @@ namespace { ASSERT_EQUALS( response.getWriteConcernError()->getErrMessage(), "message" ); ASSERT_EQUALS( response.getWriteConcernError()->getErrCode(), ErrorCodes::WriteConcernFailed ); + ASSERT( !mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, FinalWriteErrorAndReplError) { @@ -417,6 +444,7 @@ namespace { ASSERT_EQUALS( response.getWriteConcernError()->getErrMessage(), "message" ); ASSERT_EQUALS( response.getWriteConcernError()->getErrCode(), ErrorCodes::WriteConcernFailed ); + ASSERT( mockWriter.isClearCalled() ); } TEST(WriteBatchDownconvert, ReportOpTime) { @@ -427,6 +455,7 @@ namespace { vector<BSONObj> gleResponses; gleResponses.push_back( fromjson( "{ok: 1.0, err: null, lastOp: Timestamp(10, 0)}" ) ); gleResponses.push_back( fromjson( "{ok: 1.0, err: null, lastOp: Timestamp(20, 0)}" ) ); + gleResponses.push_back( fromjson( "{ok: 1.0}" ) ); MockSafeWriter mockWriter( gleResponses ); BatchSafeWriter batchWriter( &mockWriter ); @@ -445,6 +474,7 @@ namespace { ASSERT( !response.isErrDetailsSet() ); ASSERT( !response.isWriteConcernErrorSet() ); ASSERT_EQUALS( response.getLastOp().toStringPretty(), OpTime(20, 0).toStringPretty() ); + ASSERT( !mockWriter.isClearCalled() ); } // diff --git a/src/mongo/s/write_ops/batch_upconvert.cpp b/src/mongo/s/write_ops/batch_upconvert.cpp index d371fde5848..59ff4a272cb 100644 --- a/src/mongo/s/write_ops/batch_upconvert.cpp +++ b/src/mongo/s/write_ops/batch_upconvert.cpp @@ -175,8 +175,15 @@ namespace mongo { } else if ( response.isErrDetailsSet() ) { // The last error in the batch is always reported - this matches expected COE - // semantics for insert batches and works for single writes - lastBatchError = response.getErrDetails().back(); + // semantics for insert batches. For updates and deletes, error is only reported + // if the error was on the last item. + + const bool lastOpErrored = response.getErrDetails().back()->getIndex() == + static_cast<int>(request.sizeWriteOps() - 1); + if ( request.getBatchType() == BatchedCommandRequest::BatchType_Insert || + lastOpErrored ) { + lastBatchError = response.getErrDetails().back(); + } } else { // We don't care about write concern errors, these happen in legacy mode in GLE. diff --git a/src/mongo/s/write_ops/dbclient_safe_writer.cpp b/src/mongo/s/write_ops/dbclient_safe_writer.cpp index c5370948cb5..143414fcf5d 100644 --- a/src/mongo/s/write_ops/dbclient_safe_writer.cpp +++ b/src/mongo/s/write_ops/dbclient_safe_writer.cpp @@ -96,9 +96,6 @@ namespace mongo { BSONObj* gleResponse ) { try { - BSONObj resetResponse; // ignored, always ok - conn->runCommand( dbName.toString(), BSON( "resetError" << 1 ), resetResponse ); - BSONObjBuilder gleCmdB; gleCmdB.append( "getLastError", true ); gleCmdB.appendElements( writeConcern ); @@ -112,4 +109,17 @@ namespace mongo { return Status::OK(); } + Status DBClientSafeWriter::clearErrors( DBClientBase* conn, + const StringData& dbName ) { + try { + BSONObj resetResponse; // ignored, always ok. + conn->runCommand( dbName.toString(), BSON( "resetError" << 1 ), resetResponse ); + } + catch ( const DBException& ex ) { + return ex.toStatus(); + } + + return Status::OK(); + } + } diff --git a/src/mongo/s/write_ops/dbclient_safe_writer.h b/src/mongo/s/write_ops/dbclient_safe_writer.h index c4656281651..f1f07f4d0d3 100644 --- a/src/mongo/s/write_ops/dbclient_safe_writer.h +++ b/src/mongo/s/write_ops/dbclient_safe_writer.h @@ -57,6 +57,8 @@ namespace mongo { const BSONObj& writeConcern, BSONObj* gleResponse ); + Status clearErrors( DBClientBase* conn, + const StringData& dbName ); }; } diff --git a/src/mongo/shell/bulk_api.js b/src/mongo/shell/bulk_api.js index f4b2a8c3ca7..5af814b81d9 100644 --- a/src/mongo/shell/bulk_api.js +++ b/src/mongo/shell/bulk_api.js @@ -46,13 +46,6 @@ var _bulk_api_module = (function() { return db.runCommand( cmd ); }; - var enforceWriteConcern = function(db, options) { - // Reset previous errors so we can apply the write concern no matter what - // as long as it is valid. - db.runCommand({ resetError: 1 }); - return executeGetLastError(db, options); - }; - /** * Wraps the result for write commands and presents a convenient api for accessing * single results & errors (returns the last one if there are multiple). @@ -888,12 +881,27 @@ var _bulk_api_module = (function() { } } - // The write concern may have not been enforced if we did it earlier and a write - // error occurs, so we apply the actual write concern at the end. - if (batchResult.writeErrors.length == 0 || - !ordered && (batchResult.writeErrors.length < batch.operations.length)) { - result = enforceWriteConcern(collection.getDB(), writeConcern); - extractedError = extractGLEErrors(result); + var needToEnforceWC = writeConcern != null && + bsonWoCompare(writeConcern, { w: 1 }) != 0 && + bsonWoCompare(writeConcern, { w: 0 }) != 0; + + if (needToEnforceWC && + (batchResult.writeErrors.length == 0 || + (!ordered && + // not all errored. + batchResult.writeErrors.length < batch.operations.length))) { + + // if last write errored + if( batchResult.writeErrors.length > 0 && + batchResult.writeErrors[batchResult.writeErrors.length - 1].index == + (batch.operations.length - 1)) { + // Reset previous errors so we can apply the write concern no matter what + // as long as it is valid. + collection.getDB().runCommand({ resetError: 1 }); + } + + result = executeGetLastError(collection.getDB(), writeConcern); + extractedError = extractGLEErrors(result); } if (extractedError != null && extractedError.wcError != null) { |