diff options
Diffstat (limited to 'src/mongo/db/d_concurrency.cpp')
| -rw-r--r-- | src/mongo/db/d_concurrency.cpp | 816 |
1 files changed, 816 insertions, 0 deletions
diff --git a/src/mongo/db/d_concurrency.cpp b/src/mongo/db/d_concurrency.cpp new file mode 100644 index 00000000000..8d4186315cc --- /dev/null +++ b/src/mongo/db/d_concurrency.cpp @@ -0,0 +1,816 @@ +// @file d_concurrency.cpp + +/** +* Copyright (C) 2008 10gen Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "pch.h" +#include "d_concurrency.h" +#include "../util/concurrency/qlock.h" +#include "../util/concurrency/threadlocal.h" +#include "../util/concurrency/rwlock.h" +#include "../util/concurrency/mapsf.h" +#include "../util/assert_util.h" +#include "../util/stacktrace.h" +#include "client.h" +#include "curop.h" +#include "namespacestring.h" +#include "d_globals.h" +#include "server.h" +#include "dur.h" +#include "lockstat.h" + +// oplog locking +// no top level read locks +// system.profile writing +// oplog now +// yielding +// commitIfNeeded + +#define MONGOD_CONCURRENCY_LEVEL_GLOBAL 0 +#define MONGOD_CONCURRENCY_LEVEL_DB 1 + +#ifndef MONGOD_CONCURRENCY_LEVEL +#define MONGOD_CONCURRENCY_LEVEL MONGOD_CONCURRENCY_LEVEL_DB +#endif + +namespace mongo { + + static const bool DB_LEVEL_LOCKING_ENABLED = ( ( MONGOD_CONCURRENCY_LEVEL ) >= MONGOD_CONCURRENCY_LEVEL_DB ); + + inline LockState& lockState() { + return cc().lockState(); + } + + char threadState() { + return lockState().threadState(); + } + + class DBTryLockTimeoutException : public std::exception { + public: + DBTryLockTimeoutException() {} + virtual ~DBTryLockTimeoutException() throw() { } + }; + + namespace dur { + void assertNothingSpooled(); + void releasingWriteLock(); + } + + // e.g. externalobjsortmutex uses hlmutex as it can be locked for very long times + // todo : report HLMutex status in db.currentOp() output + // perhaps move this elsewhere as this could be used in mongos and this file is for mongod + HLMutex::HLMutex(const char *name) : SimpleMutex(name) { } + + /* dbname->lock + Currently these are never deleted - will linger if db was closed. (that should be fine.) + We don't put the lock inside the Database object as those can come and go with open and + closes and that would just add complexity. + Note there is no path concept for where the database is; if somehow you had two db's open + in different directories with the same name, it will be ok but they are sharing a lock + then. + */ + static mapsf<string,WrapperForRWLock*> dblocks; + + /* we don't want to touch dblocks too much as a mutex is involved. thus party for that, + this is here... + */ + WrapperForRWLock *nestableLocks[] = { + 0, + new WrapperForRWLock("local"), + new WrapperForRWLock("admin") + }; + + LockStat* Lock::nestableLockStat( Nestable db ) { + return &nestableLocks[db]->stats; + } + + static void locked_W(); + static void unlocking_w(); + static void unlocking_W(); + + class WrapperForQLock { + QLock q; + public: + LockStat stats; + + void lock_r() { + verify( threadState() == 0 ); + lockState().lockedStart( 'r' ); + q.lock_r(); + } + + void lock_w() { + verify( threadState() == 0 ); + getDur().commitIfNeeded(); + lockState().lockedStart( 'w' ); + q.lock_w(); + } + + void lock_R() { + LockState& ls = lockState(); + massert(16103, str::stream() << "can't lock_R, threadState=" << (int) ls.threadState(), ls.threadState() == 0); + ls.lockedStart( 'R' ); + q.lock_R(); + } + + void lock_W() { + LockState& ls = lockState(); + if( ls.threadState() ) { + log() << "can't lock_W, threadState=" << (int) ls.threadState() << endl; + fassert(16114,false); + } + getDur().commitIfNeeded(); // check before locking - will use an R lock for the commit if need to do one, which is better than W + ls.lockedStart( 'W' ); + { + q.lock_W(); + } + locked_W(); + } + + // how to count try's that fail is an interesting question. we should get rid of try(). + bool lock_R_try(int millis) { + verify( threadState() == 0 ); + bool got = q.lock_R_try(millis); + if( got ) + lockState().lockedStart( 'R' ); + return got; + } + + bool lock_W_try(int millis) { + verify( threadState() == 0 ); + bool got = q.lock_W_try(millis); + if( got ) { + lockState().lockedStart( 'W' ); + locked_W(); + } + return got; + } + + void unlock_r() { + wassert( threadState() == 'r' ); + lockState().unlocked(); + q.unlock_r(); + } + + void unlock_w() { + unlocking_w(); + wassert( threadState() == 'w' ); + lockState().unlocked(); + q.unlock_w(); + } + + void unlock_R() { _unlock_R(); } + + void unlock_W() { + wassert( threadState() == 'W' ); + unlocking_W(); + lockState().unlocked(); + q.unlock_W(); + } + + // todo timing stats? : + void W_to_R() { q.W_to_R(); } + void R_to_W() { q.R_to_W(); } + bool w_to_X() { return q.w_to_X(); } + void X_to_w() { q.X_to_w(); } + + private: + void _unlock_R() { + wassert( threadState() == 'R' ); + lockState().unlocked(); + q.unlock_R(); + } + }; + + static WrapperForQLock& qlk = *new WrapperForQLock(); + LockStat* Lock::globalLockStat() { + return &qlk.stats; + } + + void reportLockStats(BSONObjBuilder& result) { + BSONObjBuilder b; + b.append(".", qlk.stats.report()); + b.append("admin", nestableLocks[Lock::admin]->stats.report()); + b.append("local", nestableLocks[Lock::local]->stats.report()); + { + mapsf<string,WrapperForRWLock*>::ref r(dblocks); + for( map<string,WrapperForRWLock*>::const_iterator i = r.r.begin(); i != r.r.end(); i++ ) { + b.append(i->first, i->second->stats.report()); + } + } + result.append("locks", b.obj()); + } + + int Lock::isLocked() { + return threadState(); + } + int Lock::isReadLocked() { + return threadState() == 'R' || threadState() == 'r'; + } + int Lock::somethingWriteLocked() { + return threadState() == 'W' || threadState() == 'w'; + } + bool Lock::isRW() { + return threadState() == 'W' || threadState() == 'R'; + } + bool Lock::isW() { + return threadState() == 'W'; + } + bool Lock::isR() { + return threadState() == 'R'; + } + bool Lock::nested() { + // note this doesn't tell us much actually, it tells us if we are nesting locks but + // they could be the a global lock twice or a global and a specific or two specifics + // (such as including local) + return lockState().recursiveCount() > 1; + } + + bool Lock::isWriteLocked(const StringData& ns) { + LockState &ls = lockState(); + if( ls.threadState() == 'W' ) + return true; + if( ls.threadState() != 'w' ) + return false; + return ls.isLocked( ns ); + } + bool Lock::atLeastReadLocked(const StringData& ns) + { + LockState &ls = lockState(); + if( ls.threadState() == 'R' || ls.threadState() == 'W' ) + return true; // global + if( ls.threadState() == 0 ) + return false; + return ls.isLocked( ns ); + } + void Lock::assertAtLeastReadLocked(const StringData& ns) { + if( !atLeastReadLocked(ns) ) { + LockState &ls = lockState(); + log() << "error expected " << ns << " to be locked " << endl; + ls.dump(); + msgasserted(16104, str::stream() << "expected to be read locked for " << ns); + } + } + void Lock::assertWriteLocked(const StringData& ns) { + if( !Lock::isWriteLocked(ns) ) { + lockState().dump(); + msgasserted(16105, str::stream() << "expected to be write locked for " << ns); + } + } + bool Lock::dbLevelLockingEnabled() { + return DB_LEVEL_LOCKING_ENABLED; + } + + RWLockRecursive &Lock::ParallelBatchWriterMode::_batchLock = *(new RWLockRecursive("special")); + void Lock::ParallelBatchWriterMode::iAmABatchParticipant() { + lockState()._batchWriter = true; + } + + Lock::ParallelBatchWriterSupport::ParallelBatchWriterSupport() { + relock(); + } + + void Lock::ParallelBatchWriterSupport::tempRelease() { + _lk.reset( 0 ); + } + + void Lock::ParallelBatchWriterSupport::relock() { + LockState& ls = lockState(); + if ( ! ls._batchWriter ) { + AcquiringParallelWriter a(ls); + _lk.reset( new RWLockRecursive::Shared(ParallelBatchWriterMode::_batchLock) ); + } + } + + + Lock::ScopedLock::ScopedLock( char type ) + : _type(type), _stat(0) { + LockState& ls = lockState(); + ls.enterScopedLock( this ); + } + Lock::ScopedLock::~ScopedLock() { + LockState& ls = lockState(); + int prevCount = ls.recursiveCount(); + Lock::ScopedLock* what = ls.leaveScopedLock(); + fassert( 16171 , prevCount != 1 || what == this ); + } + + long long Lock::ScopedLock::acquireFinished( LockStat* stat ) { + long long acquisitionTime = _timer.micros(); + _timer.reset(); + _stat = stat; + cc().curop()->lockStat().recordAcquireTimeMicros( _type , acquisitionTime ); + return acquisitionTime; + } + + void Lock::ScopedLock::tempRelease() { + long long micros = _timer.micros(); + _tempRelease(); + _pbws_lk.tempRelease(); + _recordTime( micros ); // might as well do after we unlock + } + + void Lock::ScopedLock::_recordTime( long long micros ) { + if ( _stat ) + _stat->recordLockTimeMicros( _type , micros ); + cc().curop()->lockStat().recordLockTimeMicros( _type , micros ); + } + + void Lock::ScopedLock::recordTime() { + _recordTime(_timer.micros()); + } + + void Lock::ScopedLock::resetTime() { + _timer.reset(); + } + + void Lock::ScopedLock::relock() { + _pbws_lk.relock(); + _relock(); + resetTime(); + } + + + + Lock::TempRelease::TempRelease() : cant( Lock::nested() ) + { + if( cant ) + return; + + LockState& ls = lockState(); + + fassert( 16116, ls.recursiveCount() == 1 ); + fassert( 16117, ls.threadState() != 0 ); + + scopedLk = ls.leaveScopedLock(); + fassert( 16118, scopedLk ); + scopedLk->tempRelease(); + } + Lock::TempRelease::~TempRelease() + { + if( cant ) + return; + + LockState& ls = lockState(); + + fassert( 16119, scopedLk ); + fassert( 16120 , ls.threadState() == 0 ); + + ls.enterScopedLock( scopedLk ); + scopedLk->relock(); + } + + void Lock::GlobalWrite::_tempRelease() { + fassert(16121, !noop); + char ts = threadState(); + fassert(16122, ts != 'R'); // indicates downgraded; not allowed with temprelease + fassert(16123, ts == 'W'); + qlk.unlock_W(); + } + void Lock::GlobalWrite::_relock() { + fassert(16125, !noop); + char ts = threadState(); + fassert(16126, ts == 0); + Acquiring a(this,lockState()); + qlk.lock_W(); + } + + void Lock::GlobalRead::_tempRelease() { + fassert(16127, !noop); + char ts = threadState(); + fassert(16128, ts == 'R'); + qlk.unlock_R(); + } + void Lock::GlobalRead::_relock() { + fassert(16129, !noop); + char ts = threadState(); + fassert(16130, ts == 0); + Acquiring a(this,lockState()); + qlk.lock_R(); + } + + void Lock::DBWrite::_tempRelease() { + unlockDB(); + } + void Lock::DBWrite::_relock() { + lockDB(_what); + } + void Lock::DBRead::_tempRelease() { + unlockDB(); + } + void Lock::DBRead::_relock() { + lockDB(_what); + } + + Lock::GlobalWrite::GlobalWrite(bool sg, int timeoutms) + : ScopedLock('W') { + char ts = threadState(); + noop = false; + if( ts == 'W' ) { + noop = true; + return; + } + dassert( ts == 0 ); + + Acquiring a(this,lockState()); + + if ( timeoutms != -1 ) { + bool success = qlk.lock_W_try( timeoutms ); + if ( !success ) throw DBTryLockTimeoutException(); + } + else { + qlk.lock_W(); + } + } + Lock::GlobalWrite::~GlobalWrite() { + if( noop ) { + return; + } + recordTime(); // for lock stats + if( threadState() == 'R' ) { // we downgraded + qlk.unlock_R(); + } + else { + qlk.unlock_W(); + } + } + void Lock::GlobalWrite::downgrade() { + verify( !noop ); + verify( threadState() == 'W' ); + qlk.W_to_R(); + lockState().changeLockState( 'R' ); + } + + // you will deadlock if 2 threads doing this + void Lock::GlobalWrite::upgrade() { + verify( !noop ); + verify( threadState() == 'R' ); + qlk.R_to_W(); + lockState().changeLockState( 'W' ); + } + + Lock::GlobalRead::GlobalRead( int timeoutms ) + : ScopedLock( 'R' ) { + LockState& ls = lockState(); + char ts = ls.threadState(); + noop = false; + if( ts == 'R' || ts == 'W' ) { + noop = true; + return; + } + + Acquiring a(this,ls); + + if ( timeoutms != -1 ) { + bool success = qlk.lock_R_try( timeoutms ); + if ( !success ) throw DBTryLockTimeoutException(); + } + else { + qlk.lock_R(); // we are unlocked in the qlock/top sense. lock_R will assert if we are in an in compatible state + } + } + Lock::GlobalRead::~GlobalRead() { + if( !noop ) { + recordTime(); // for lock stats + qlk.unlock_R(); + } + } + + void Lock::DBWrite::lockNestable(Nestable db) { + _nested = true; + LockState& ls = lockState(); + if( ls.nestableCount() ) { + if( db != ls.whichNestable() ) { + error() << "can't lock local and admin db at the same time " << (int) db << ' ' << (int) ls.whichNestable() << endl; + fassert(16131,false); + } + verify( ls.nestableCount() > 0 ); + } + else { + fassert(16132,_weLocked==0); + ls.lockedNestable(db, 1); + _weLocked = nestableLocks[db]; + _weLocked->lock(); + } + } + void Lock::DBRead::lockNestable(Nestable db) { + _nested = true; + LockState& ls = lockState(); + if( ls.nestableCount() ) { + // we are nested in our locking of local. previous lock could be read OR write lock on local. + } + else { + ls.lockedNestable(db,-1); + fassert(16133,_weLocked==0); + _weLocked = nestableLocks[db]; + _weLocked->lock_shared(); + } + } + + void Lock::DBWrite::lockOther(const string& db) { + fassert( 16252, !db.empty() ); + LockState& ls = lockState(); + + // we do checks first, as on assert destructor won't be called so don't want to be half finished with our work. + if( ls.otherCount() ) { + // nested. if/when we do temprelease with DBWrite we will need to increment here + // (so we can not release or assert if nested). + massert(16106, str::stream() << "internal error tried to lock two databases at the same time. old:" << ls.otherName() << " new:" << db , db == ls.otherName() ); + return; + } + + // first lock for this db. check consistent order with local db lock so we never deadlock. local always comes last + massert(16098, str::stream() << "can't dblock:" << db << " when local or admin is already locked", ls.nestableCount() == 0); + + if( db != ls.otherName() ) + { + mapsf<string,WrapperForRWLock*>::ref r(dblocks); + WrapperForRWLock*& lock = r[db]; + if( lock == 0 ) + lock = new WrapperForRWLock(db.c_str()); + ls.lockedOther( db , 1 , lock ); + } + else { + DEV OCCASIONALLY { dassert( dblocks.get(db) == ls.otherLock() ); } + ls.lockedOther(1); + } + + fassert(16134,_weLocked==0); + ls.otherLock()->lock(); + _weLocked = ls.otherLock(); + } + + static Lock::Nestable n(const char *db) { + if( str::equals(db, "local") ) + return Lock::local; + if( str::equals(db, "admin") ) + return Lock::admin; + return Lock::notnestable; + } + + void Lock::DBWrite::lockDB(const string& ns) { + fassert( 16253, !ns.empty() ); + LockState& ls = lockState(); + + Acquiring a(this,ls); + _locked_W=false; + _locked_w=false; + _weLocked=0; + + + massert( 16186 , "can't get a DBWrite while having a read lock" , ! ls.hasAnyReadLock() ); + if( ls.isW() ) + return; + + if (DB_LEVEL_LOCKING_ENABLED) { + char db[MaxDatabaseNameLen]; + nsToDatabase(ns.data(), db); + Nestable nested = n(db); + if( nested == admin ) { + // we can't nestedly lock both admin and local as implemented. so lock_W. + qlk.lock_W(); + _locked_W = true; + return; + } + if( !nested ) + lockOther(db); + lockTop(ls); + if( nested ) + lockNestable(nested); + } + else { + qlk.lock_W(); + _locked_w = true; + } + } + + void Lock::DBRead::lockDB(const string& ns) { + fassert( 16254, !ns.empty() ); + LockState& ls = lockState(); + + Acquiring a(this,ls); + _locked_r=false; + _weLocked=0; + + if ( ls.isRW() ) + return; + if (DB_LEVEL_LOCKING_ENABLED) { + char db[MaxDatabaseNameLen]; + nsToDatabase(ns.data(), db); + Nestable nested = n(db); + if( !nested ) + lockOther(db); + lockTop(ls); + if( nested ) + lockNestable(nested); + } + else { + qlk.lock_R(); + _locked_r = true; + } + } + + Lock::DBWrite::DBWrite( const StringData& ns ) + : ScopedLock( 'w' ), _what(ns.data()), _nested(false) { + lockDB( _what ); + } + + Lock::DBRead::DBRead( const StringData& ns ) + : ScopedLock( 'r' ), _what(ns.data()), _nested(false) { + lockDB( _what ); + } + + Lock::DBWrite::~DBWrite() { + unlockDB(); + } + Lock::DBRead::~DBRead() { + unlockDB(); + } + + void Lock::DBWrite::unlockDB() { + if( _weLocked ) { + recordTime(); // for lock stats + + if ( _nested ) + lockState().unlockedNestable(); + else + lockState().unlockedOther(); + + _weLocked->unlock(); + } + + if( _locked_w ) { + if (DB_LEVEL_LOCKING_ENABLED) { + qlk.unlock_w(); + } else { + qlk.unlock_W(); + } + } + if( _locked_W ) { + qlk.unlock_W(); + } + _weLocked = 0; + _locked_W = _locked_w = false; + } + void Lock::DBRead::unlockDB() { + if( _weLocked ) { + recordTime(); // for lock stats + + if( _nested ) + lockState().unlockedNestable(); + else + lockState().unlockedOther(); + + _weLocked->unlock_shared(); + } + + if( _locked_r ) { + if (DB_LEVEL_LOCKING_ENABLED) { + qlk.unlock_r(); + } else { + qlk.unlock_R(); + } + } + _weLocked = 0; + _locked_r = false; + } + + void Lock::DBWrite::lockTop(LockState& ls) { + switch( ls.threadState() ) { + case 'w': + break; + default: + verify(false); + case 0 : + qlk.lock_w(); + _locked_w = true; + } + } + void Lock::DBRead::lockTop(LockState& ls) { + switch( ls.threadState() ) { + case 'r': + case 'w': + break; + default: + verify(false); + case 0 : + qlk.lock_r(); + _locked_r = true; + } + } + + void Lock::DBRead::lockOther(const string& db) { + fassert( 16255, !db.empty() ); + LockState& ls = lockState(); + + // we do checks first, as on assert destructor won't be called so don't want to be half finished with our work. + if( ls.otherCount() ) { + // nested. prev could be read or write. if/when we do temprelease with DBRead/DBWrite we will need to increment/decrement here + // (so we can not release or assert if nested). temprelease we should avoid if we can though, it's a bit of an anti-pattern. + massert(16099, str::stream() << "internal error tried to lock two databases at the same time. old:" << ls.otherName() << " new:" << db, db == ls.otherName() ); + return; + } + + // first lock for this db. check consistent order with local db lock so we never deadlock. local always comes last + massert(16100, str::stream() << "can't dblock:" << db << " when local or admin is already locked", ls.nestableCount() == 0); + + if( db != ls.otherName() ) + { + mapsf<string,WrapperForRWLock*>::ref r(dblocks); + WrapperForRWLock*& lock = r[db]; + if( lock == 0 ) + lock = new WrapperForRWLock(db.c_str()); + ls.lockedOther( db , -1 , lock ); + } + else { + DEV OCCASIONALLY { dassert( dblocks.get(db) == ls.otherLock() ); } + ls.lockedOther(-1); + } + fassert(16135,_weLocked==0); + ls.otherLock()->lock_shared(); + _weLocked = ls.otherLock(); + } + + Lock::DBWrite::UpgradeToExclusive::UpgradeToExclusive() { + fassert( 16187, lockState().threadState() == 'w' ); + + // We're about to temporarily drop w, so stop the lock time stopwatch + lockState().recordLockTime(); + + _gotUpgrade = qlk.w_to_X(); + if ( _gotUpgrade ) { + lockState().changeLockState('W'); + lockState().resetLockTime(); + } + } + + Lock::DBWrite::UpgradeToExclusive::~UpgradeToExclusive() { + if ( _gotUpgrade ) { + fassert( 16188, lockState().threadState() == 'W' ); + lockState().recordLockTime(); + qlk.X_to_w(); + lockState().changeLockState('w'); + } + else { + fassert( 16189, lockState().threadState() == 'w' ); + } + + // Start recording lock time again + lockState().resetLockTime(); + } + + writelocktry::writelocktry( int tryms ) : + _got( false ), + _dbwlock( NULL ) + { + try { + _dbwlock.reset(new Lock::GlobalWrite( false, tryms )); + } + catch ( DBTryLockTimeoutException & ) { + return; + } + _got = true; + } + writelocktry::~writelocktry() { + } + + // note: the 'already' concept here might be a bad idea as a temprelease wouldn't notice it is nested then + readlocktry::readlocktry( int tryms ) : + _got( false ), + _dbrlock( NULL ) + { + try { + _dbrlock.reset(new Lock::GlobalRead( tryms )); + } + catch ( DBTryLockTimeoutException & ) { + return; + } + _got = true; + } + readlocktry::~readlocktry() { + } + + void locked_W() { + } + void unlocking_w() { + // we can't commit early in this case; so a bit more to do here. + dur::releasingWriteLock(); + } + void unlocking_W() { + dur::releasingWriteLock(); + } + +} |
