diff options
Diffstat (limited to 'src/mongo/s/d_migrate.cpp')
| -rw-r--r-- | src/mongo/s/d_migrate.cpp | 1914 |
1 files changed, 1914 insertions, 0 deletions
diff --git a/src/mongo/s/d_migrate.cpp b/src/mongo/s/d_migrate.cpp new file mode 100644 index 00000000000..138081fe376 --- /dev/null +++ b/src/mongo/s/d_migrate.cpp @@ -0,0 +1,1914 @@ +// d_migrate.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/>. +*/ + + +/** + these are commands that live in mongod + mostly around shard management and checking + */ + +#include "pch.h" +#include <map> +#include <string> +#include <algorithm> + +#include <boost/thread/thread.hpp> + +#include "mongo/db/dbhelpers.h" +#include "../db/commands.h" +#include "../db/jsobj.h" +#include "../db/cmdline.h" +#include "../db/queryoptimizer.h" +#include "../db/btree.h" +#include "../db/repl_block.h" +#include "../db/dur.h" +#include "../db/clientcursor.h" +#include "../db/pagefault.h" +#include "../db/repl.h" + +#include "../client/connpool.h" +#include "../client/distlock.h" +#include "mongo/client/dbclientcursor.h" + +#include "../util/queue.h" +#include "../util/startup_test.h" +#include "../util/processinfo.h" +#include "../util/ramlog.h" + +#include "shard.h" +#include "d_logic.h" +#include "config.h" +#include "chunk.h" + +using namespace std; + +namespace mongo { + + BSONObj findShardKeyIndexPattern_locked( const string& ns , const BSONObj& shardKeyPattern ) { + verify( Lock::isLocked() ); + NamespaceDetails* nsd = nsdetails( ns.c_str() ); + verify( nsd ); + const IndexDetails* idx = nsd->findIndexByPrefix( shardKeyPattern , true ); /* require single key */ + verify( idx ); + return idx->keyPattern().getOwned(); + } + + + BSONObj findShardKeyIndexPattern_unlocked( const string& ns , const BSONObj& shardKeyPattern ) { + Client::ReadContext context( ns ); + return findShardKeyIndexPattern_locked( ns , shardKeyPattern ).getOwned(); + } + + Tee* migrateLog = new RamLog( "migrate" ); + + class MoveTimingHelper { + public: + MoveTimingHelper( const string& where , const string& ns , BSONObj min , BSONObj max , int total , string& cmdErrmsg ) + : _where( where ) , _ns( ns ) , _next( 0 ) , _total( total ) , _cmdErrmsg( cmdErrmsg ) { + _nextNote = 0; + _b.append( "min" , min ); + _b.append( "max" , max ); + } + + ~MoveTimingHelper() { + // even if logChange doesn't throw, bson does + // sigh + try { + if ( _next != _total ) { + note( "aborted" ); + } + if ( _cmdErrmsg.size() ) { + note( _cmdErrmsg ); + warning() << "got error doing chunk migrate: " << _cmdErrmsg << endl; + } + + configServer.logChange( (string)"moveChunk." + _where , _ns, _b.obj() ); + } + catch ( const std::exception& e ) { + warning() << "couldn't record timing for moveChunk '" << _where << "': " << e.what() << migrateLog; + } + } + + void done( int step ) { + verify( step == ++_next ); + verify( step <= _total ); + + stringstream ss; + ss << "step" << step << " of " << _total; + string s = ss.str(); + + CurOp * op = cc().curop(); + if ( op ) + op->setMessage( s.c_str() ); + else + warning() << "op is null in MoveTimingHelper::done" << migrateLog; + + _b.appendNumber( s , _t.millis() ); + _t.reset(); + +#if 0 + // debugging for memory leak? + ProcessInfo pi; + ss << " v:" << pi.getVirtualMemorySize() + << " r:" << pi.getResidentSize(); + log() << ss.str() << migrateLog; +#endif + } + + + void note( const string& s ) { + string field = "note"; + if ( _nextNote > 0 ) { + StringBuilder buf; + buf << "note" << _nextNote; + field = buf.str(); + } + _nextNote++; + + _b.append( field , s ); + } + + private: + Timer _t; + + string _where; + string _ns; + + int _next; + int _total; // expected # of steps + int _nextNote; + + string _cmdErrmsg; + + BSONObjBuilder _b; + + }; + + struct OldDataCleanup { + static AtomicUInt _numThreads; // how many threads are doing async cleanup + + bool secondaryThrottle; + string ns; + BSONObj min; + BSONObj max; + BSONObj shardKeyPattern; + set<CursorId> initial; + + OldDataCleanup(){ + _numThreads++; + } + OldDataCleanup( const OldDataCleanup& other ) { + secondaryThrottle = other.secondaryThrottle; + ns = other.ns; + min = other.min.getOwned(); + max = other.max.getOwned(); + shardKeyPattern = other.shardKeyPattern.getOwned(); + initial = other.initial; + _numThreads++; + } + ~OldDataCleanup(){ + _numThreads--; + } + + string toString() const { + return str::stream() << ns << " from " << min << " -> " << max; + } + + void doRemove() { + ShardForceVersionOkModeBlock sf; + { + RemoveSaver rs("moveChunk",ns,"post-cleanup"); + long long numDeleted = + Helpers::removeRange( ns , + min , + max , + findShardKeyIndexPattern_unlocked( ns , shardKeyPattern ) , + false , /*maxInclusive*/ + secondaryThrottle , + cmdLine.moveParanoia ? &rs : 0 , /*callback*/ + true ); /*fromMigrate*/ + log() << "moveChunk deleted: " << numDeleted << migrateLog; + } + + + ReplTime lastOpApplied = cc().getLastOp().asDate(); + Timer t; + for ( int i=0; i<3600; i++ ) { + if ( opReplicatedEnough( lastOpApplied , ( getSlaveCount() / 2 ) + 1 ) ) { + LOG(t.seconds() < 30 ? 1 : 0) << "moveChunk repl sync took " << t.seconds() << " seconds" << migrateLog; + return; + } + sleepsecs(1); + } + + warning() << "moveChunk repl sync timed out after " << t.seconds() << " seconds" << migrateLog; + } + + }; + + AtomicUInt OldDataCleanup::_numThreads = 0; + + static const char * const cleanUpThreadName = "cleanupOldData"; + + class ChunkCommandHelper : public Command { + public: + ChunkCommandHelper( const char * name ) + : Command( name ) { + } + + virtual void help( stringstream& help ) const { + help << "internal - should not be called directly"; + } + virtual bool slaveOk() const { return false; } + virtual bool adminOnly() const { return true; } + virtual LockType locktype() const { return NONE; } + + }; + + bool isInRange( const BSONObj& obj , const BSONObj& min , const BSONObj& max ) { + BSONObj k = obj.extractFields( min, true ); + + return k.woCompare( min ) >= 0 && k.woCompare( max ) < 0; + } + + + class MigrateFromStatus { + public: + + MigrateFromStatus() : _m("MigrateFromStatus") , _workLock("MigrateFromStatus::workLock") { + _active = false; + _inCriticalSection = false; + _memoryUsed = 0; + } + + void start( string ns , + const BSONObj& min , + const BSONObj& max , + const BSONObj& shardKeyPattern ) { + scoped_lock ll(_workLock); + scoped_lock l(_m); // reads and writes _active + + verify( ! _active ); + + verify( ! min.isEmpty() ); + verify( ! max.isEmpty() ); + verify( ns.size() ); + + _ns = ns; + _min = min; + _max = max; + _shardKeyPattern = shardKeyPattern; + + verify( _cloneLocs.size() == 0 ); + verify( _deleted.size() == 0 ); + verify( _reload.size() == 0 ); + verify( _memoryUsed == 0 ); + + _active = true; + } + + void done() { + Lock::DBRead lk( _ns ); + + { + scoped_spinlock lk( _trackerLocks ); + _deleted.clear(); + _reload.clear(); + _cloneLocs.clear(); + } + _memoryUsed = 0; + + scoped_lock l(_m); + _active = false; + _inCriticalSection = false; + } + + void logOp( const char * opstr , const char * ns , const BSONObj& obj , BSONObj * patt ) { + if ( ! _getActive() ) + return; + + if ( _ns != ns ) + return; + + // no need to log if this is not an insertion, an update, or an actual deletion + // note: opstr 'db' isn't a deletion but a mention that a database exists (for replication + // machinery mostly) + char op = opstr[0]; + if ( op == 'n' || op =='c' || ( op == 'd' && opstr[1] == 'b' ) ) + return; + + BSONElement ide; + if ( patt ) + ide = patt->getField( "_id" ); + else + ide = obj["_id"]; + + if ( ide.eoo() ) { + warning() << "logOpForSharding got mod with no _id, ignoring obj: " << obj << migrateLog; + return; + } + + BSONObj it; + + switch ( opstr[0] ) { + + case 'd': { + + if ( getThreadName() == cleanUpThreadName ) { + // we don't want to xfer things we're cleaning + // as then they'll be deleted on TO + // which is bad + return; + } + + // can't filter deletes :( + _deleted.push_back( ide.wrap() ); + _memoryUsed += ide.size() + 5; + return; + } + + case 'i': + it = obj; + break; + + case 'u': + if ( ! Helpers::findById( cc() , _ns.c_str() , ide.wrap() , it ) ) { + warning() << "logOpForSharding couldn't find: " << ide << " even though should have" << migrateLog; + return; + } + break; + + } + + if ( ! isInRange( it , _min , _max ) ) + return; + + _reload.push_back( ide.wrap() ); + _memoryUsed += ide.size() + 5; + } + + void xfer( list<BSONObj> * l , BSONObjBuilder& b , const char * name , long long& size , bool explode ) { + const long long maxSize = 1024 * 1024; + + if ( l->size() == 0 || size > maxSize ) + return; + + BSONArrayBuilder arr(b.subarrayStart(name)); + + list<BSONObj>::iterator i = l->begin(); + + while ( i != l->end() && size < maxSize ) { + BSONObj t = *i; + if ( explode ) { + BSONObj it; + if ( Helpers::findById( cc() , _ns.c_str() , t, it ) ) { + arr.append( it ); + size += it.objsize(); + } + } + else { + arr.append( t ); + } + i = l->erase( i ); + size += t.objsize(); + } + + arr.done(); + } + + /** + * called from the dest of a migrate + * transfers mods from src to dest + */ + bool transferMods( string& errmsg , BSONObjBuilder& b ) { + if ( ! _getActive() ) { + errmsg = "no active migration!"; + return false; + } + + long long size = 0; + + { + Client::ReadContext cx( _ns ); + + xfer( &_deleted , b , "deleted" , size , false ); + xfer( &_reload , b , "reload" , size , true ); + } + + b.append( "size" , size ); + + return true; + } + + /** + * Get the disklocs that belong to the chunk migrated and sort them in _cloneLocs (to avoid seeking disk later) + * + * @param maxChunkSize number of bytes beyond which a chunk's base data (no indices) is considered too large to move + * @param errmsg filled with textual description of error if this call return false + * @return false if approximate chunk size is too big to move or true otherwise + */ + bool storeCurrentLocs( long long maxChunkSize , string& errmsg , BSONObjBuilder& result ) { + Client::ReadContext ctx( _ns ); + NamespaceDetails *d = nsdetails( _ns.c_str() ); + if ( ! d ) { + errmsg = "ns not found, should be impossible"; + return false; + } + + const IndexDetails *idx = d->findIndexByPrefix( _shardKeyPattern , + true ); /* require single key */ + + if ( idx == NULL ) { + errmsg = (string)"can't find index in storeCurrentLocs" + causedBy( errmsg ); + return false; + } + // Assume both min and max non-empty, append MinKey's to make them fit chosen index + BSONObj min = Helpers::modifiedRangeBound( _min , idx->keyPattern() , -1 ); + BSONObj max = Helpers::modifiedRangeBound( _max , idx->keyPattern() , -1 ); + + BtreeCursor* btreeCursor = BtreeCursor::make( d , *idx , min , max , false , 1 ); + auto_ptr<ClientCursor> cc( + new ClientCursor( QueryOption_NoCursorTimeout , + shared_ptr<Cursor>( btreeCursor ) , _ns ) ); + + // use the average object size to estimate how many objects a full chunk would carry + // do that while traversing the chunk's range using the sharding index, below + // there's a fair amount of slack before we determine a chunk is too large because object sizes will vary + unsigned long long maxRecsWhenFull; + long long avgRecSize; + const long long totalRecs = d->stats.nrecords; + if ( totalRecs > 0 ) { + avgRecSize = d->stats.datasize / totalRecs; + maxRecsWhenFull = maxChunkSize / avgRecSize; + maxRecsWhenFull = std::min( (unsigned long long)(Chunk::MaxObjectPerChunk + 1) , 130 * maxRecsWhenFull / 100 /* slack */ ); + } + else { + avgRecSize = 0; + maxRecsWhenFull = Chunk::MaxObjectPerChunk + 1; + } + + // do a full traversal of the chunk and don't stop even if we think it is a large chunk + // we want the number of records to better report, in that case + bool isLargeChunk = false; + unsigned long long recCount = 0;; + while ( cc->ok() ) { + DiskLoc dl = cc->currLoc(); + if ( ! isLargeChunk ) { + scoped_spinlock lk( _trackerLocks ); + _cloneLocs.insert( dl ); + } + cc->advance(); + + // we can afford to yield here because any change to the base data that we might miss is already being + // queued and will be migrated in the 'transferMods' stage + if ( ! cc->yieldSometimes( ClientCursor::DontNeed ) ) { + cc.release(); + break; + } + + if ( ++recCount > maxRecsWhenFull ) { + isLargeChunk = true; + } + } + + if ( isLargeChunk ) { + warning() << "can't move chunk of size (approximately) " << recCount * avgRecSize + << " because maximum size allowed to move is " << maxChunkSize + << " ns: " << _ns << " " << _min << " -> " << _max + << migrateLog; + result.appendBool( "chunkTooBig" , true ); + result.appendNumber( "estimatedChunkSize" , (long long)(recCount * avgRecSize) ); + errmsg = "chunk too big to move"; + return false; + } + + { + scoped_spinlock lk( _trackerLocks ); + log() << "moveChunk number of documents: " << _cloneLocs.size() << migrateLog; + } + return true; + } + + bool clone( string& errmsg , BSONObjBuilder& result ) { + if ( ! _getActive() ) { + errmsg = "not active"; + return false; + } + + ElapsedTracker tracker (128, 10); // same as ClientCursor::_yieldSometimesTracker + + int allocSize; + { + Client::ReadContext ctx( _ns ); + NamespaceDetails *d = nsdetails( _ns.c_str() ); + verify( d ); + scoped_spinlock lk( _trackerLocks ); + allocSize = std::min(BSONObjMaxUserSize, (int)((12 + d->averageObjectSize()) * _cloneLocs.size())); + } + BSONArrayBuilder a (allocSize); + + while ( 1 ) { + bool filledBuffer = false; + + auto_ptr<LockMongoFilesShared> fileLock; + Record* recordToTouch = 0; + + { + Client::ReadContext ctx( _ns ); + scoped_spinlock lk( _trackerLocks ); + set<DiskLoc>::iterator i = _cloneLocs.begin(); + for ( ; i!=_cloneLocs.end(); ++i ) { + if (tracker.intervalHasElapsed()) // should I yield? + break; + + DiskLoc dl = *i; + + Record* r = dl.rec(); + if ( ! r->likelyInPhysicalMemory() ) { + fileLock.reset( new LockMongoFilesShared() ); + recordToTouch = r; + break; + } + + BSONObj o = dl.obj(); + + // use the builder size instead of accumulating 'o's size so that we take into consideration + // the overhead of BSONArray indices + if ( a.len() + o.objsize() + 1024 > BSONObjMaxUserSize ) { + filledBuffer = true; // break out of outer while loop + break; + } + + a.append( o ); + } + + _cloneLocs.erase( _cloneLocs.begin() , i ); + + if ( _cloneLocs.empty() || filledBuffer ) + break; + } + + if ( recordToTouch ) { + // its safe to touch here bceause we have a LockMongoFilesShared + // we can't do where we get the lock because we would have to unlock the main readlock and tne _trackerLocks + // simpler to handle this out there + recordToTouch->touch(); + recordToTouch = 0; + } + + } + + result.appendArray( "objects" , a.arr() ); + return true; + } + + void aboutToDelete( const Database* db , const DiskLoc& dl ) { + verify(db); + Lock::assertWriteLocked(db->name); + + if ( ! _getActive() ) + return; + + if ( ! db->ownsNS( _ns ) ) + return; + + + // not needed right now + // but trying to prevent a future bug + scoped_spinlock lk( _trackerLocks ); + + _cloneLocs.erase( dl ); + } + + long long mbUsed() const { return _memoryUsed / ( 1024 * 1024 ); } + + bool getInCriticalSection() const { scoped_lock l(_m); return _inCriticalSection; } + void setInCriticalSection( bool b ) { scoped_lock l(_m); _inCriticalSection = b; } + + bool isActive() const { return _getActive(); } + + void doRemove( OldDataCleanup& cleanup ) { + int it = 0; + while ( true ) { + if ( it > 20 && it % 10 == 0 ) log() << "doRemote iteration " << it << " for: " << cleanup << endl; + { + scoped_lock ll(_workLock); + if ( ! _active ) { + cleanup.doRemove(); + return; + } + } + sleepmillis( 1000 ); + } + } + + private: + mutable mongo::mutex _m; // protect _inCriticalSection and _active + bool _inCriticalSection; + bool _active; + + string _ns; + BSONObj _min; + BSONObj _max; + BSONObj _shardKeyPattern; + + // we need the lock in case there is a malicious _migrateClone for example + // even though it shouldn't be needed under normal operation + SpinLock _trackerLocks; + + // disk locs yet to be transferred from here to the other side + // no locking needed because built initially by 1 thread in a read lock + // emptied by 1 thread in a read lock + // updates applied by 1 thread in a write lock + set<DiskLoc> _cloneLocs; + + list<BSONObj> _reload; // objects that were modified that must be recloned + list<BSONObj> _deleted; // objects deleted during clone that should be deleted later + long long _memoryUsed; // bytes in _reload + _deleted + + mutable mongo::mutex _workLock; // this is used to make sure only 1 thread is doing serious work + // for now, this means migrate or removing old chunk data + + bool _getActive() const { scoped_lock l(_m); return _active; } + void _setActive( bool b ) { scoped_lock l(_m); _active = b; } + + } migrateFromStatus; + + struct MigrateStatusHolder { + MigrateStatusHolder( string ns , + const BSONObj& min , + const BSONObj& max , + const BSONObj& shardKeyPattern ) { + migrateFromStatus.start( ns , min , max , shardKeyPattern ); + } + ~MigrateStatusHolder() { + migrateFromStatus.done(); + } + }; + + void _cleanupOldData( OldDataCleanup cleanup ) { + Client::initThread( cleanUpThreadName ); + if (!noauth) { + cc().getAuthenticationInfo()->authorize("local", internalSecurity.user); + } + log() << " (start) waiting to cleanup " << cleanup << " # cursors:" << cleanup.initial.size() << migrateLog; + + int loops = 0; + Timer t; + while ( t.seconds() < 900 ) { // 15 minutes + verify( !Lock::isLocked() ); + sleepmillis( 20 ); + + set<CursorId> now; + ClientCursor::find( cleanup.ns , now ); + + set<CursorId> left; + for ( set<CursorId>::iterator i=cleanup.initial.begin(); i!=cleanup.initial.end(); ++i ) { + CursorId id = *i; + if ( now.count(id) ) + left.insert( id ); + } + + if ( left.size() == 0 ) + break; + cleanup.initial = left; + + if ( ( loops++ % 200 ) == 0 ) { + log() << " (looping " << loops << ") waiting to cleanup " << cleanup.ns << " from " << cleanup.min << " -> " << cleanup.max << " # cursors:" << cleanup.initial.size() << migrateLog; + + stringstream ss; + for ( set<CursorId>::iterator i=cleanup.initial.begin(); i!=cleanup.initial.end(); ++i ) { + CursorId id = *i; + ss << id << " "; + } + log() << " cursors: " << ss.str() << migrateLog; + } + } + + migrateFromStatus.doRemove( cleanup ); + + cc().shutdown(); + } + + void cleanupOldData( OldDataCleanup cleanup ) { + try { + _cleanupOldData( cleanup ); + } + catch ( std::exception& e ) { + log() << " error cleaning old data:" << e.what() << migrateLog; + } + catch ( ... ) { + log() << " unknown error cleaning old data" << migrateLog; + } + } + + void logOpForSharding( const char * opstr , const char * ns , const BSONObj& obj , BSONObj * patt ) { + migrateFromStatus.logOp( opstr , ns , obj , patt ); + } + + void aboutToDeleteForSharding( const Database* db , const DiskLoc& dl ) { + migrateFromStatus.aboutToDelete( db , dl ); + } + + class TransferModsCommand : public ChunkCommandHelper { + public: + TransferModsCommand() : ChunkCommandHelper( "_transferMods" ) {} + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + return migrateFromStatus.transferMods( errmsg, result ); + } + } transferModsCommand; + + + class InitialCloneCommand : public ChunkCommandHelper { + public: + InitialCloneCommand() : ChunkCommandHelper( "_migrateClone" ) {} + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + return migrateFromStatus.clone( errmsg, result ); + } + } initialCloneCommand; + + + /** + * this is the main entry for moveChunk + * called to initial a move + * usually by a mongos + * this is called on the "from" side + */ + class MoveChunkCommand : public Command { + public: + MoveChunkCommand() : Command( "moveChunk" ) {} + virtual void help( stringstream& help ) const { + help << "should not be calling this directly"; + } + + virtual bool slaveOk() const { return false; } + virtual bool adminOnly() const { return true; } + virtual LockType locktype() const { return NONE; } + + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + // 1. parse options + // 2. make sure my view is complete and lock + // 3. start migrate + // in a read lock, get all DiskLoc and sort so we can do as little seeking as possible + // tell to start transferring + // 4. pause till migrate caught up + // 5. LOCK + // a) update my config, essentially locking + // b) finish migrate + // c) update config server + // d) logChange to config server + // 6. wait for all current cursors to expire + // 7. remove data locally + + // ------------------------------- + + // 1. + string ns = cmdObj.firstElement().str(); + string to = cmdObj["to"].str(); + string from = cmdObj["from"].str(); // my public address, a tad redundant, but safe + + // fromShard and toShard needed so that 2.2 mongos can interact with either 2.0 or 2.2 mongod + if( cmdObj["fromShard"].type() == String ){ + from = cmdObj["fromShard"].String(); + } + + if( cmdObj["toShard"].type() == String ){ + to = cmdObj["toShard"].String(); + } + + // if we do a w=2 after very write + bool secondaryThrottle = cmdObj["secondaryThrottle"].trueValue(); + if ( secondaryThrottle && ! anyReplEnabled() ) { + secondaryThrottle = false; + warning() << "secondaryThrottle selected but no replication" << endl; + } + + BSONObj min = cmdObj["min"].Obj(); + BSONObj max = cmdObj["max"].Obj(); + BSONElement shardId = cmdObj["shardId"]; + BSONElement maxSizeElem = cmdObj["maxChunkSizeBytes"]; + + if ( ns.empty() ) { + errmsg = "need to specify namespace in command"; + return false; + } + + if ( to.empty() ) { + errmsg = "need to specify shard to move chunk to"; + return false; + } + if ( from.empty() ) { + errmsg = "need to specify shard to move chunk from"; + return false; + } + + if ( min.isEmpty() ) { + errmsg = "need to specify a min"; + return false; + } + + if ( max.isEmpty() ) { + errmsg = "need to specify a max"; + return false; + } + + if ( shardId.eoo() ) { + errmsg = "need shardId"; + return false; + } + + if ( maxSizeElem.eoo() || ! maxSizeElem.isNumber() ) { + errmsg = "need to specify maxChunkSizeBytes"; + return false; + } + const long long maxChunkSize = maxSizeElem.numberLong(); // in bytes + + if ( ! shardingState.enabled() ) { + if ( cmdObj["configdb"].type() != String ) { + errmsg = "sharding not enabled"; + return false; + } + string configdb = cmdObj["configdb"].String(); + shardingState.enable( configdb ); + configServer.init( configdb ); + } + + MoveTimingHelper timing( "from" , ns , min , max , 6 /* steps */ , errmsg ); + + // Make sure we're as up-to-date as possible with shard information + // This catches the case where we had to previously changed a shard's host by + // removing/adding a shard with the same name + Shard::reloadShardInfo(); + + // So 2.2 mongod can interact with 2.0 mongos, mongod needs to handle either a conn + // string or a shard in the to/from fields. The Shard constructor handles this, + // eventually we should break the compatibility. + + Shard fromShard( from ); + Shard toShard( to ); + + log() << "received moveChunk request: " << cmdObj << migrateLog; + + timing.done(1); + + // 2. + + if ( migrateFromStatus.isActive() ) { + errmsg = "migration already in progress"; + return false; + } + + DistributedLock lockSetup( ConnectionString( shardingState.getConfigServer() , ConnectionString::SYNC ) , ns ); + dist_lock_try dlk; + + try{ + dlk = dist_lock_try( &lockSetup , (string)"migrate-" + min.toString() ); + } + catch( LockException& e ){ + errmsg = str::stream() << "error locking distributed lock for migration " << "migrate-" << min.toString() << causedBy( e ); + return false; + } + + if ( ! dlk.got() ) { + errmsg = str::stream() << "the collection metadata could not be locked with lock " << "migrate-" << min.toString(); + result.append( "who" , dlk.other() ); + return false; + } + + BSONObj chunkInfo = BSON("min" << min << "max" << max << "from" << fromShard.getName() << "to" << toShard.getName() ); + configServer.logChange( "moveChunk.start" , ns , chunkInfo ); + + ShardChunkVersion maxVersion; + string myOldShard; + { + scoped_ptr<ScopedDbConnection> conn( + ScopedDbConnection::getInternalScopedDbConnection( + shardingState.getConfigServer() ) ); + + BSONObj x; + BSONObj currChunk; + try{ + x = conn->get()->findOne( ShardNS::chunk, + Query( BSON( "ns" << ns ) ) + .sort( BSON( "lastmod" << -1 ) ) ); + currChunk = conn->get()->findOne( ShardNS::chunk , shardId.wrap( "_id" ) ); + } + catch( DBException& e ){ + errmsg = str::stream() << "aborted moveChunk because could not get chunk data from config server " << shardingState.getConfigServer() << causedBy( e ); + warning() << errmsg << endl; + return false; + } + + maxVersion = ShardChunkVersion::fromBSON( x, "lastmod" ); + verify( currChunk["shard"].type() ); + verify( currChunk["min"].type() ); + verify( currChunk["max"].type() ); + myOldShard = currChunk["shard"].String(); + conn->done(); + + BSONObj currMin = currChunk["min"].Obj(); + BSONObj currMax = currChunk["max"].Obj(); + if ( currMin.woCompare( min ) || currMax.woCompare( max ) ) { + errmsg = "boundaries are outdated (likely a split occurred)"; + result.append( "currMin" , currMin ); + result.append( "currMax" , currMax ); + result.append( "requestedMin" , min ); + result.append( "requestedMax" , max ); + + warning() << "aborted moveChunk because" << errmsg << ": " << min << "->" << max + << " is now " << currMin << "->" << currMax << migrateLog; + return false; + } + + if ( myOldShard != fromShard.getName() ) { + errmsg = "location is outdated (likely balance or migrate occurred)"; + result.append( "from" , fromShard.getName() ); + result.append( "official" , myOldShard ); + + warning() << "aborted moveChunk because " << errmsg << ": chunk is at " << myOldShard + << " and not at " << fromShard.getName() << migrateLog; + return false; + } + + if ( maxVersion < shardingState.getVersion( ns ) ) { + errmsg = "official version less than mine?"; + maxVersion.addToBSON( result, "officialVersion" ); + shardingState.getVersion( ns ).addToBSON( result, "myVersion" ); + + warning() << "aborted moveChunk because " << errmsg << ": official " << maxVersion + << " mine: " << shardingState.getVersion(ns) << migrateLog; + return false; + } + + // since this could be the first call that enable sharding we also make sure to have the chunk manager up to date + shardingState.gotShardName( myOldShard ); + + // Using the maxVersion we just found will enforce a check - if we use zero version, + // it's possible this shard will be *at* zero version from a previous migrate and + // no refresh will be done + // TODO: Make this less fragile + ShardChunkVersion shardVersion = maxVersion; + shardingState.trySetVersion( ns , shardVersion /* will return updated */ ); + + log() << "moveChunk request accepted at version " << shardVersion << migrateLog; + } + + timing.done(2); + + // 3. + + ShardChunkManagerPtr chunkManager = shardingState.getShardChunkManager( ns ); + verify( chunkManager != NULL ); + BSONObj shardKeyPattern = chunkManager->getKey(); + if ( shardKeyPattern.isEmpty() ){ + errmsg = "no shard key found"; + return false; + } + + MigrateStatusHolder statusHolder( ns , min , max , shardKeyPattern ); + { + // this gets a read lock, so we know we have a checkpoint for mods + if ( ! migrateFromStatus.storeCurrentLocs( maxChunkSize , errmsg , result ) ) + return false; + + scoped_ptr<ScopedDbConnection> connTo( + ScopedDbConnection::getScopedDbConnection( toShard.getConnString() ) ); + BSONObj res; + bool ok; + try{ + ok = connTo->get()->runCommand( "admin" , + BSON( "_recvChunkStart" << ns << + "from" << fromShard.getConnString() << + "min" << min << + "max" << max << + "shardKeyPattern" << shardKeyPattern << + "configServer" << configServer.modelServer() << + "secondaryThrottle" << secondaryThrottle + ) , + res ); + } + catch( DBException& e ){ + errmsg = str::stream() << "moveChunk could not contact to: shard " << to << " to start transfer" << causedBy( e ); + warning() << errmsg << endl; + return false; + } + + connTo->done(); + + if ( ! ok ) { + errmsg = "moveChunk failed to engage TO-shard in the data transfer: "; + verify( res["errmsg"].type() ); + errmsg += res["errmsg"].String(); + result.append( "cause" , res ); + return false; + } + + } + timing.done( 3 ); + + // 4. + for ( int i=0; i<86400; i++ ) { // don't want a single chunk move to take more than a day + verify( !Lock::isLocked() ); + sleepsecs( 1 ); + scoped_ptr<ScopedDbConnection> conn( + ScopedDbConnection::getScopedDbConnection( toShard.getConnString() ) ); + BSONObj res; + bool ok; + try { + ok = conn->get()->runCommand( "admin" , BSON( "_recvChunkStatus" << 1 ) , res ); + res = res.getOwned(); + } + catch( DBException& e ){ + errmsg = str::stream() << "moveChunk could not contact to: shard " << to << " to monitor transfer" << causedBy( e ); + warning() << errmsg << endl; + return false; + } + + conn->done(); + + log(0) << "moveChunk data transfer progress: " << res << " my mem used: " << migrateFromStatus.mbUsed() << migrateLog; + + if ( ! ok || res["state"].String() == "fail" ) { + warning() << "moveChunk error transferring data caused migration abort: " << res << migrateLog; + errmsg = "data transfer error"; + result.append( "cause" , res ); + return false; + } + + if ( res["state"].String() == "steady" ) + break; + + if ( migrateFromStatus.mbUsed() > (500 * 1024 * 1024) ) { + // this is too much memory for us to use for this + // so we're going to abort the migrate + scoped_ptr<ScopedDbConnection> conn( + ScopedDbConnection::getScopedDbConnection( toShard.getConnString() ) ); + + BSONObj res; + conn->get()->runCommand( "admin" , BSON( "_recvChunkAbort" << 1 ) , res ); + res = res.getOwned(); + conn->done(); + error() << "aborting migrate because too much memory used res: " << res << migrateLog; + errmsg = "aborting migrate because too much memory used"; + result.appendBool( "split" , true ); + return false; + } + + killCurrentOp.checkForInterrupt(); + } + timing.done(4); + + // 5. + { + // 5.a + // we're under the collection lock here, so no other migrate can change maxVersion or ShardChunkManager state + migrateFromStatus.setInCriticalSection( true ); + ShardChunkVersion currVersion = maxVersion; + ShardChunkVersion myVersion = currVersion; + myVersion.incMajor(); + + { + Lock::DBWrite lk( ns ); + verify( myVersion > shardingState.getVersion( ns ) ); + + // bump the chunks manager's version up and "forget" about the chunk being moved + // this is not the commit point but in practice the state in this shard won't until the commit it done + shardingState.donateChunk( ns , min , max , myVersion ); + } + + log() << "moveChunk setting version to: " << myVersion << migrateLog; + + // 5.b + // we're under the collection lock here, too, so we can undo the chunk donation because no other state change + // could be ongoing + { + BSONObj res; + scoped_ptr<ScopedDbConnection> connTo( + ScopedDbConnection::getScopedDbConnection( toShard.getConnString() ) ); + + bool ok; + + try{ + ok = connTo->get()->runCommand( "admin" , + BSON( "_recvChunkCommit" << 1 ) , + res ); + } + catch( DBException& e ){ + errmsg = str::stream() << "moveChunk could not contact to: shard " << toShard.getConnString() << " to commit transfer" << causedBy( e ); + warning() << errmsg << endl; + return false; + } + + connTo->done(); + + if ( ! ok ) { + { + Lock::DBWrite lk( ns ); + + // revert the chunk manager back to the state before "forgetting" about the chunk + shardingState.undoDonateChunk( ns , min , max , currVersion ); + } + + log() << "moveChunk migrate commit not accepted by TO-shard: " << res + << " resetting shard version to: " << currVersion << migrateLog; + + errmsg = "_recvChunkCommit failed!"; + result.append( "cause" , res ); + return false; + } + + log() << "moveChunk migrate commit accepted by TO-shard: " << res << migrateLog; + } + + // 5.c + + // version at which the next highest lastmod will be set + // if the chunk being moved is the last in the shard, nextVersion is that chunk's lastmod + // otherwise the highest version is from the chunk being bumped on the FROM-shard + ShardChunkVersion nextVersion; + + // we want to go only once to the configDB but perhaps change two chunks, the one being migrated and another + // local one (so to bump version for the entire shard) + // we use the 'applyOps' mechanism to group the two updates and make them safer + // TODO pull config update code to a module + + BSONObjBuilder cmdBuilder; + + BSONArrayBuilder updates( cmdBuilder.subarrayStart( "applyOps" ) ); + { + // update for the chunk being moved + BSONObjBuilder op; + op.append( "op" , "u" ); + op.appendBool( "b" , false /* no upserting */ ); + op.append( "ns" , ShardNS::chunk ); + + BSONObjBuilder n( op.subobjStart( "o" ) ); + n.append( "_id" , Chunk::genID( ns , min ) ); + myVersion.addToBSON( n, "lastmod" ); + n.append( "ns" , ns ); + n.append( "min" , min ); + n.append( "max" , max ); + n.append( "shard" , toShard.getName() ); + n.done(); + + BSONObjBuilder q( op.subobjStart( "o2" ) ); + q.append( "_id" , Chunk::genID( ns , min ) ); + q.done(); + + updates.append( op.obj() ); + } + + nextVersion = myVersion; + + // if we have chunks left on the FROM shard, update the version of one of them as well + // we can figure that out by grabbing the chunkManager installed on 5.a + // TODO expose that manager when installing it + + ShardChunkManagerPtr chunkManager = shardingState.getShardChunkManager( ns ); + if( chunkManager->getNumChunks() > 0 ) { + + // get another chunk on that shard + BSONObj lookupKey; + BSONObj bumpMin, bumpMax; + do { + chunkManager->getNextChunk( lookupKey , &bumpMin , &bumpMax ); + lookupKey = bumpMin; + } + while( bumpMin == min ); + + BSONObjBuilder op; + op.append( "op" , "u" ); + op.appendBool( "b" , false ); + op.append( "ns" , ShardNS::chunk ); + + nextVersion.incMinor(); // same as used on donateChunk + BSONObjBuilder n( op.subobjStart( "o" ) ); + n.append( "_id" , Chunk::genID( ns , bumpMin ) ); + nextVersion.addToBSON( n, "lastmod" ); + n.append( "ns" , ns ); + n.append( "min" , bumpMin ); + n.append( "max" , bumpMax ); + n.append( "shard" , fromShard.getName() ); + n.done(); + + BSONObjBuilder q( op.subobjStart( "o2" ) ); + q.append( "_id" , Chunk::genID( ns , bumpMin ) ); + q.done(); + + updates.append( op.obj() ); + + log() << "moveChunk updating self version to: " << nextVersion << " through " + << bumpMin << " -> " << bumpMax << " for collection '" << ns << "'" << migrateLog; + + } + else { + + log() << "moveChunk moved last chunk out for collection '" << ns << "'" << migrateLog; + } + + updates.done(); + + BSONArrayBuilder preCond( cmdBuilder.subarrayStart( "preCondition" ) ); + { + BSONObjBuilder b; + b.append( "ns" , ShardNS::chunk ); + b.append( "q" , BSON( "query" << BSON( "ns" << ns ) << "orderby" << BSON( "lastmod" << -1 ) ) ); + { + BSONObjBuilder bb( b.subobjStart( "res" ) ); + // TODO: For backwards compatibility, we can't yet require an epoch here + bb.appendTimestamp( "lastmod", maxVersion.toLong() ); + bb.done(); + } + preCond.append( b.obj() ); + } + + preCond.done(); + + BSONObj cmd = cmdBuilder.obj(); + LOG(7) << "moveChunk update: " << cmd << migrateLog; + + bool ok = false; + BSONObj cmdResult; + try { + scoped_ptr<ScopedDbConnection> conn( + ScopedDbConnection::getInternalScopedDbConnection( + shardingState.getConfigServer() ) ); + ok = conn->get()->runCommand( "config" , cmd , cmdResult ); + conn->done(); + } + catch ( DBException& e ) { + warning() << e << migrateLog; + ok = false; + BSONObjBuilder b; + e.getInfo().append( b ); + cmdResult = b.obj(); + } + + if ( ! ok ) { + + // this could be a blip in the connectivity + // wait out a few seconds and check if the commit request made it + // + // if the commit made it to the config, we'll see the chunk in the new shard and there's no action + // if the commit did not make it, currently the only way to fix this state is to bounce the mongod so + // that the old state (before migrating) be brought in + + warning() << "moveChunk commit outcome ongoing: " << cmd << " for command :" << cmdResult << migrateLog; + sleepsecs( 10 ); + + try { + scoped_ptr<ScopedDbConnection> conn( + ScopedDbConnection::getInternalScopedDbConnection( + shardingState.getConfigServer() ) ); + + // look for the chunk in this shard whose version got bumped + // we assume that if that mod made it to the config, the applyOps was successful + BSONObj doc = conn->get()->findOne( ShardNS::chunk, + Query(BSON( "ns" << ns )) + .sort( BSON("lastmod" << -1))); + ShardChunkVersion checkVersion = ShardChunkVersion::fromBSON( doc["lastmod"] ); + + if ( checkVersion.isEquivalentTo( nextVersion ) ) { + log() << "moveChunk commit confirmed" << migrateLog; + + } + else { + error() << "moveChunk commit failed: version is at" + << checkVersion << " instead of " << nextVersion << migrateLog; + error() << "TERMINATING" << migrateLog; + dbexit( EXIT_SHARDING_ERROR ); + } + + conn->done(); + + } + catch ( ... ) { + error() << "moveChunk failed to get confirmation of commit" << migrateLog; + error() << "TERMINATING" << migrateLog; + dbexit( EXIT_SHARDING_ERROR ); + } + } + + migrateFromStatus.setInCriticalSection( false ); + + // 5.d + configServer.logChange( "moveChunk.commit" , ns , chunkInfo ); + } + + migrateFromStatus.done(); + timing.done(5); + + { + // 6. + OldDataCleanup c; + c.secondaryThrottle = secondaryThrottle; + c.ns = ns; + c.min = min.getOwned(); + c.max = max.getOwned(); + c.shardKeyPattern = shardKeyPattern.getOwned(); + ClientCursor::find( ns , c.initial ); + if ( c.initial.size() ) { + log() << "forking for cleaning up chunk data" << migrateLog; + boost::thread t( boost::bind( &cleanupOldData , c ) ); + } + else { + log() << "doing delete inline" << migrateLog; + // 7. + c.doRemove(); + } + + + } + timing.done(6); + + return true; + + } + + } moveChunkCmd; + + bool ShardingState::inCriticalMigrateSection() { + return migrateFromStatus.getInCriticalSection(); + } + + /* ----- + below this are the "to" side commands + + command to initiate + worker thread + does initial clone + pulls initial change set + keeps pulling + keeps state + command to get state + commend to "commit" + */ + + class MigrateStatus { + public: + + MigrateStatus() : m_active("MigrateStatus") { active = false; } + + void prepare() { + scoped_lock l(m_active); // reading and writing 'active' + + verify( ! active ); + state = READY; + errmsg = ""; + + numCloned = 0; + clonedBytes = 0; + numCatchup = 0; + numSteady = 0; + + active = true; + } + + void go() { + try { + _go(); + } + catch ( std::exception& e ) { + state = FAIL; + errmsg = e.what(); + error() << "migrate failed: " << e.what() << migrateLog; + } + catch ( ... ) { + state = FAIL; + errmsg = "UNKNOWN ERROR"; + error() << "migrate failed with unknown exception" << migrateLog; + } + setActive( false ); + } + + void _go() { + verify( getActive() ); + verify( state == READY ); + verify( ! min.isEmpty() ); + verify( ! max.isEmpty() ); + + slaveCount = ( getSlaveCount() / 2 ) + 1; + + string errmsg; + MoveTimingHelper timing( "to" , ns , min , max , 5 /* steps */ , errmsg ); + + scoped_ptr<ScopedDbConnection> connPtr( + ScopedDbConnection::getScopedDbConnection( from ) ); + ScopedDbConnection& conn = *connPtr; + conn->getLastError(); // just test connection + + { + // 0. copy system.namespaces entry if collection doesn't already exist + Client::WriteContext ctx( ns ); + // Only copy if ns doesn't already exist + if ( ! nsdetails( ns.c_str() ) ) { + string system_namespaces = NamespaceString( ns ).db + ".system.namespaces"; + BSONObj entry = conn->findOne( system_namespaces, BSON( "name" << ns ) ); + if ( entry["options"].isABSONObj() ) { + string errmsg; + if ( ! userCreateNS( ns.c_str(), entry["options"].Obj(), errmsg, true, 0 ) ) + warning() << "failed to create collection with options: " << errmsg + << endl; + } + } + } + + { + // 1. copy indexes + + vector<BSONObj> all; + { + auto_ptr<DBClientCursor> indexes = conn->getIndexes( ns ); + + while ( indexes->more() ) { + all.push_back( indexes->next().getOwned() ); + } + } + + for ( unsigned i=0; i<all.size(); i++ ) { + BSONObj idx = all[i]; + Client::WriteContext ct( ns ); + string system_indexes = cc().database()->name + ".system.indexes"; + theDataFileMgr.insertAndLog( system_indexes.c_str() , idx, true /* flag fromMigrate in oplog */ ); + } + + timing.done(1); + } + + { + // 2. delete any data already in range + RemoveSaver rs( "moveChunk" , ns , "preCleanup" ); + long long num = Helpers::removeRange( ns , + min , + max , + findShardKeyIndexPattern_unlocked( ns , shardKeyPattern ) , + false , /*maxInclusive*/ + secondaryThrottle , /* secondaryThrottle */ + cmdLine.moveParanoia ? &rs : 0 , /*callback*/ + true ); /* flag fromMigrate in oplog */ + if ( num ) + warning() << "moveChunkCmd deleted data already in chunk # objects: " << num << migrateLog; + + timing.done(2); + } + + + { + // 3. initial bulk clone + state = CLONE; + + while ( true ) { + BSONObj res; + if ( ! conn->runCommand( "admin" , BSON( "_migrateClone" << 1 ) , res ) ) { // gets array of objects to copy, in disk order + state = FAIL; + errmsg = "_migrateClone failed: "; + errmsg += res.toString(); + error() << errmsg << migrateLog; + conn.done(); + return; + } + + BSONObj arr = res["objects"].Obj(); + int thisTime = 0; + + BSONObjIterator i( arr ); + while( i.more() ) { + BSONObj o = i.next().Obj(); + { + PageFaultRetryableSection pgrs; + while ( 1 ) { + try { + Lock::DBWrite lk( ns ); + Helpers::upsert( ns, o, true ); + break; + } + catch ( PageFaultException& e ) { + e.touch(); + } + } + } + thisTime++; + numCloned++; + clonedBytes += o.objsize(); + + if ( secondaryThrottle ) { + if ( ! waitForReplication( cc().getLastOp(), 2, 60 /* seconds to wait */ ) ) { + warning() << "secondaryThrottle on, but doc insert timed out after 60 seconds, continuing" << endl; + } + } + } + + if ( thisTime == 0 ) + break; + } + + timing.done(3); + } + + // if running on a replicated system, we'll need to flush the docs we cloned to the secondaries + ReplTime lastOpApplied = cc().getLastOp().asDate(); + + { + // 4. do bulk of mods + state = CATCHUP; + while ( true ) { + BSONObj res; + if ( ! conn->runCommand( "admin" , BSON( "_transferMods" << 1 ) , res ) ) { + state = FAIL; + errmsg = "_transferMods failed: "; + errmsg += res.toString(); + error() << "_transferMods failed: " << res << migrateLog; + conn.done(); + return; + } + if ( res["size"].number() == 0 ) + break; + + apply( res , &lastOpApplied ); + + const int maxIterations = 3600*50; + int i; + for ( i=0;i<maxIterations; i++) { + if ( state == ABORT ) { + timing.note( "aborted" ); + return; + } + + if ( opReplicatedEnough( lastOpApplied ) ) + break; + + if ( i > 100 ) { + warning() << "secondaries having hard time keeping up with migrate" << migrateLog; + } + + sleepmillis( 20 ); + } + + if ( i == maxIterations ) { + errmsg = "secondary can't keep up with migrate"; + error() << errmsg << migrateLog; + conn.done(); + state = FAIL; + return; + } + } + + timing.done(4); + } + + { + // pause to wait for replication + // this will prevent us from going into critical section until we're ready + Timer t; + while ( t.minutes() < 600 ) { + if ( flushPendingWrites( lastOpApplied ) ) + break; + sleepsecs(1); + } + } + + { + // 5. wait for commit + + state = STEADY; + while ( state == STEADY || state == COMMIT_START ) { + BSONObj res; + if ( ! conn->runCommand( "admin" , BSON( "_transferMods" << 1 ) , res ) ) { + log() << "_transferMods failed in STEADY state: " << res << migrateLog; + errmsg = res.toString(); + state = FAIL; + conn.done(); + return; + } + + if ( res["size"].number() > 0 && apply( res , &lastOpApplied ) ) + continue; + + if ( state == ABORT ) { + timing.note( "aborted" ); + return; + } + + if ( state == COMMIT_START ) { + if ( flushPendingWrites( lastOpApplied ) ) + break; + } + + sleepmillis( 10 ); + } + + if ( state == FAIL ) { + errmsg = "timed out waiting for commit"; + return; + } + + timing.done(5); + } + + state = DONE; + conn.done(); + } + + void status( BSONObjBuilder& b ) { + b.appendBool( "active" , getActive() ); + + b.append( "ns" , ns ); + b.append( "from" , from ); + b.append( "min" , min ); + b.append( "max" , max ); + b.append( "shardKeyPattern" , shardKeyPattern ); + + b.append( "state" , stateString() ); + if ( state == FAIL ) + b.append( "errmsg" , errmsg ); + { + BSONObjBuilder bb( b.subobjStart( "counts" ) ); + bb.append( "cloned" , numCloned ); + bb.append( "clonedBytes" , clonedBytes ); + bb.append( "catchup" , numCatchup ); + bb.append( "steady" , numSteady ); + bb.done(); + } + + + } + + bool apply( const BSONObj& xfer , ReplTime* lastOpApplied ) { + ReplTime dummy; + if ( lastOpApplied == NULL ) { + lastOpApplied = &dummy; + } + + bool didAnything = false; + + if ( xfer["deleted"].isABSONObj() ) { + RemoveSaver rs( "moveChunk" , ns , "removedDuring" ); + + BSONObjIterator i( xfer["deleted"].Obj() ); + while ( i.more() ) { + Client::WriteContext cx(ns); + + BSONObj id = i.next().Obj(); + + // do not apply deletes if they do not belong to the chunk being migrated + BSONObj fullObj; + if ( Helpers::findById( cc() , ns.c_str() , id, fullObj ) ) { + if ( ! isInRange( fullObj , min , max ) ) { + log() << "not applying out of range deletion: " << fullObj << migrateLog; + + continue; + } + } + + Helpers::removeRange( ns , + id , + id, + findShardKeyIndexPattern_locked( ns , shardKeyPattern ), + true , /*maxInclusive*/ + false , /* secondaryThrottle */ + cmdLine.moveParanoia ? &rs : 0 , /*callback*/ + true ); /*fromMigrate*/ + + *lastOpApplied = cx.ctx().getClient()->getLastOp().asDate(); + didAnything = true; + } + } + + if ( xfer["reload"].isABSONObj() ) { + BSONObjIterator i( xfer["reload"].Obj() ); + while ( i.more() ) { + Client::WriteContext cx(ns); + + BSONObj it = i.next().Obj(); + + Helpers::upsert( ns , it , true ); + + *lastOpApplied = cx.ctx().getClient()->getLastOp().asDate(); + didAnything = true; + } + } + + return didAnything; + } + + bool opReplicatedEnough( const ReplTime& lastOpApplied ) { + // if replication is on, try to force enough secondaries to catch up + // TODO opReplicatedEnough should eventually honor priorities and geo-awareness + // for now, we try to replicate to a sensible number of secondaries + return mongo::opReplicatedEnough( lastOpApplied , slaveCount ); + } + + bool flushPendingWrites( const ReplTime& lastOpApplied ) { + if ( ! opReplicatedEnough( lastOpApplied ) ) { + OpTime op( lastOpApplied ); + OCCASIONALLY warning() << "migrate commit waiting for " << slaveCount + << " slaves for '" << ns << "' " << min << " -> " << max + << " waiting for: " << op + << migrateLog; + return false; + } + + log() << "migrate commit succeeded flushing to secondaries for '" << ns << "' " << min << " -> " << max << migrateLog; + + { + Lock::GlobalRead lk; + + // if durability is on, force a write to journal + if ( getDur().commitNow() ) { + log() << "migrate commit flushed to journal for '" << ns << "' " << min << " -> " << max << migrateLog; + } + } + + return true; + } + + string stateString() { + switch ( state ) { + case READY: return "ready"; + case CLONE: return "clone"; + case CATCHUP: return "catchup"; + case STEADY: return "steady"; + case COMMIT_START: return "commitStart"; + case DONE: return "done"; + case FAIL: return "fail"; + case ABORT: return "abort"; + } + verify(0); + return ""; + } + + bool startCommit() { + if ( state != STEADY ) + return false; + state = COMMIT_START; + + Timer t; + // we wait for the commit to succeed before giving up + while ( t.minutes() <= 5 ) { + sleepmillis(1); + if ( state == DONE ) + return true; + } + state = FAIL; + log() << "startCommit never finished!" << migrateLog; + return false; + } + + void abort() { + state = ABORT; + errmsg = "aborted"; + } + + bool getActive() const { scoped_lock l(m_active); return active; } + void setActive( bool b ) { scoped_lock l(m_active); active = b; } + + mutable mongo::mutex m_active; + bool active; + + string ns; + string from; + + BSONObj min; + BSONObj max; + BSONObj shardKeyPattern; + + long long numCloned; + long long clonedBytes; + long long numCatchup; + long long numSteady; + bool secondaryThrottle; + + int slaveCount; + + enum State { READY , CLONE , CATCHUP , STEADY , COMMIT_START , DONE , FAIL , ABORT } state; + string errmsg; + + } migrateStatus; + + void migrateThread() { + Client::initThread( "migrateThread" ); + if (!noauth) { + ShardedConnectionInfo::addHook(); + cc().getAuthenticationInfo()->authorize("local", internalSecurity.user); + } + migrateStatus.go(); + cc().shutdown(); + } + + class RecvChunkStartCommand : public ChunkCommandHelper { + public: + RecvChunkStartCommand() : ChunkCommandHelper( "_recvChunkStart" ) {} + + virtual LockType locktype() const { return WRITE; } // this is so don't have to do locking internally + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + + if ( migrateStatus.getActive() ) { + errmsg = "migrate already in progress"; + return false; + } + + if ( OldDataCleanup::_numThreads > 0 ) { + errmsg = + str::stream() + << "still waiting for a previous migrates data to get cleaned, can't accept new chunks, num threads: " + << OldDataCleanup::_numThreads; + return false; + } + + if ( ! configServer.ok() ) + configServer.init( cmdObj["configServer"].String() ); + + migrateStatus.prepare(); + + migrateStatus.ns = cmdObj.firstElement().String(); + migrateStatus.from = cmdObj["from"].String(); + migrateStatus.min = cmdObj["min"].Obj().getOwned(); + migrateStatus.max = cmdObj["max"].Obj().getOwned(); + migrateStatus.shardKeyPattern = cmdObj["shardKeyPattern"].Obj().getOwned(); + migrateStatus.secondaryThrottle = cmdObj["secondaryThrottle"].trueValue(); + + if ( migrateStatus.secondaryThrottle && ! anyReplEnabled() ) { + warning() << "secondaryThrottle asked for, but not replication" << endl; + migrateStatus.secondaryThrottle = false; + } + + boost::thread m( migrateThread ); + + result.appendBool( "started" , true ); + return true; + } + + } recvChunkStartCmd; + + class RecvChunkStatusCommand : public ChunkCommandHelper { + public: + RecvChunkStatusCommand() : ChunkCommandHelper( "_recvChunkStatus" ) {} + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + migrateStatus.status( result ); + return 1; + } + + } recvChunkStatusCommand; + + class RecvChunkCommitCommand : public ChunkCommandHelper { + public: + RecvChunkCommitCommand() : ChunkCommandHelper( "_recvChunkCommit" ) {} + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + bool ok = migrateStatus.startCommit(); + migrateStatus.status( result ); + return ok; + } + + } recvChunkCommitCommand; + + class RecvChunkAbortCommand : public ChunkCommandHelper { + public: + RecvChunkAbortCommand() : ChunkCommandHelper( "_recvChunkAbort" ) {} + + bool run(const string& , BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) { + migrateStatus.abort(); + migrateStatus.status( result ); + return true; + } + + } recvChunkAboortCommand; + + + class IsInRangeTest : public StartupTest { + public: + void run() { + BSONObj min = BSON( "x" << 1 ); + BSONObj max = BSON( "x" << 5 ); + + verify( ! isInRange( BSON( "x" << 0 ) , min , max ) ); + verify( isInRange( BSON( "x" << 1 ) , min , max ) ); + verify( isInRange( BSON( "x" << 3 ) , min , max ) ); + verify( isInRange( BSON( "x" << 4 ) , min , max ) ); + verify( ! isInRange( BSON( "x" << 5 ) , min , max ) ); + verify( ! isInRange( BSON( "x" << 6 ) , min , max ) ); + + LOG(1) << "isInRangeTest passed" << migrateLog; + } + } isInRangeTest; +} |
