// repair_database.cpp /** * Copyright (C) 2014 MongoDB 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 . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/db/repair_database.h" #include #include "mongo/db/background.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database.h" #include "mongo/db/catalog/database_holder.h" #include "mongo/db/catalog/index_create.h" #include "mongo/db/client.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/util/file.h" #include "mongo/util/file_allocator.h" #include "mongo/util/mmap.h" namespace mongo { typedef boost::filesystem::path Path; // TODO SERVER-4328 bool inDBRepair = false; struct doingRepair { doingRepair() { verify( ! inDBRepair ); inDBRepair = true; } ~doingRepair() { inDBRepair = false; } }; // inheritable class to implement an operation that may be applied to all // files in a database using _applyOpToDataFiles() class FileOp { public: virtual ~FileOp() {} // Return true if file exists and operation successful virtual bool apply( const boost::filesystem::path &p ) = 0; virtual const char * op() const = 0; }; void _applyOpToDataFiles(const string& database, FileOp &fo, bool afterAllocator = false, const string& path = storageGlobalParams.dbpath); void _deleteDataFiles(const std::string& database) { if (storageGlobalParams.directoryperdb) { FileAllocator::get()->waitUntilFinished(); MONGO_ASSERT_ON_EXCEPTION_WITH_MSG( boost::filesystem::remove_all( boost::filesystem::path(storageGlobalParams.dbpath) / database), "delete data files with a directoryperdb"); return; } class : public FileOp { virtual bool apply( const boost::filesystem::path &p ) { return boost::filesystem::remove( p ); } virtual const char * op() const { return "remove"; } } deleter; _applyOpToDataFiles( database, deleter, true ); } void boostRenameWrapper( const Path &from, const Path &to ) { try { boost::filesystem::rename( from, to ); } catch ( const boost::filesystem::filesystem_error & ) { // boost rename doesn't work across partitions boost::filesystem::copy_file( from, to); boost::filesystem::remove( from ); } } // back up original database files to 'temp' dir void _renameForBackup( const std::string& database, const Path &reservedPath ) { Path newPath( reservedPath ); if (storageGlobalParams.directoryperdb) newPath /= database; class Renamer : public FileOp { public: Renamer( const Path &newPath ) : newPath_( newPath ) {} private: const boost::filesystem::path &newPath_; virtual bool apply( const Path &p ) { if ( !boost::filesystem::exists( p ) ) return false; boostRenameWrapper( p, newPath_ / ( p.leaf().string() + ".bak" ) ); return true; } virtual const char * op() const { return "renaming"; } } renamer( newPath ); _applyOpToDataFiles( database, renamer, true ); } intmax_t dbSize( const string& database ) { class SizeAccumulator : public FileOp { public: SizeAccumulator() : totalSize_( 0 ) {} intmax_t size() const { return totalSize_; } private: virtual bool apply( const boost::filesystem::path &p ) { if ( !boost::filesystem::exists( p ) ) return false; totalSize_ += boost::filesystem::file_size( p ); return true; } virtual const char *op() const { return "checking size"; } intmax_t totalSize_; }; SizeAccumulator sa; _applyOpToDataFiles( database, sa ); return sa.size(); } // move temp files to standard data dir void _replaceWithRecovered( const string& database, const char *reservedPathString ) { Path newPath(storageGlobalParams.dbpath); if (storageGlobalParams.directoryperdb) newPath /= database; class Replacer : public FileOp { public: Replacer( const Path &newPath ) : newPath_( newPath ) {} private: const boost::filesystem::path &newPath_; virtual bool apply( const Path &p ) { if ( !boost::filesystem::exists( p ) ) return false; boostRenameWrapper( p, newPath_ / p.leaf() ); return true; } virtual const char * op() const { return "renaming"; } } replacer( newPath ); _applyOpToDataFiles( database, replacer, true, reservedPathString ); } // generate a directory name for storing temp data files Path uniqueReservedPath( const char *prefix ) { Path repairPath = Path(storageGlobalParams.repairpath); Path reservedPath; int i = 0; bool exists = false; do { stringstream ss; ss << prefix << "_repairDatabase_" << i++; reservedPath = repairPath / ss.str(); MONGO_ASSERT_ON_EXCEPTION( exists = boost::filesystem::exists( reservedPath ) ); } while ( exists ); return reservedPath; } void _applyOpToDataFiles( const string& database, FileOp &fo, bool afterAllocator, const string& path ) { if ( afterAllocator ) FileAllocator::get()->waitUntilFinished(); string c = database; c += '.'; boost::filesystem::path p(path); if (storageGlobalParams.directoryperdb) p /= database; boost::filesystem::path q; q = p / (c+"ns"); bool ok = false; MONGO_ASSERT_ON_EXCEPTION( ok = fo.apply( q ) ); if ( ok ) { LOG(2) << fo.op() << " file " << q.string() << endl; } int i = 0; int extra = 10; // should not be necessary, this is defensive in case there are missing files while ( 1 ) { verify( i <= DiskLoc::MaxFiles ); stringstream ss; ss << c << i; q = p / ss.str(); MONGO_ASSERT_ON_EXCEPTION( ok = fo.apply(q) ); if ( ok ) { if ( extra != 10 ) { LOG(1) << fo.op() << " file " << q.string() << endl; log() << " _applyOpToDataFiles() warning: extra == " << extra << endl; } } else if ( --extra <= 0 ) break; i++; } } class RepairFileDeleter { public: RepairFileDeleter( OperationContext* txn, const string& dbName, const string& pathString, const Path& path ) : _txn(txn), _dbName( dbName ), _pathString( pathString ), _path( path ), _success( false ) { } ~RepairFileDeleter() { if ( _success ) return; log() << "cleaning up failed repair " << "db: " << _dbName << " path: " << _pathString; try { _txn->recoveryUnit()->syncDataAndTruncateJournal(); MongoFile::flushAll(true); // need both in case journaling is disabled { Client::Context tempContext( _dbName, _pathString ); Database::closeDatabase( _dbName, _pathString ); } MONGO_ASSERT_ON_EXCEPTION( boost::filesystem::remove_all( _path ) ); } catch ( DBException& e ) { error() << "RepairFileDeleter failed to cleanup: " << e; error() << "aborting"; fassertFailed( 17402 ); } } void success() { _success = true; } private: OperationContext* _txn; string _dbName; string _pathString; Path _path; bool _success; }; Status repairDatabase( OperationContext* txn, string dbName, bool preserveClonedFilesOnFailure, bool backupOriginalFiles ) { scoped_ptr repairFileDeleter; doingRepair dr; dbName = nsToDatabase( dbName ); log() << "repairDatabase " << dbName << endl; BackgroundOperation::assertNoBgOpInProgForDb(dbName); txn->recoveryUnit()->syncDataAndTruncateJournal(); // Must be done before and after repair intmax_t totalSize = dbSize( dbName ); intmax_t freeSize = File::freeSpace(storageGlobalParams.repairpath); if ( freeSize > -1 && freeSize < totalSize ) { return Status( ErrorCodes::OutOfDiskSpace, str::stream() << "Cannot repair database " << dbName << " having size: " << totalSize << " (bytes) because free disk space is: " << freeSize << " (bytes)" ); } txn->checkForInterrupt(); Path reservedPath = uniqueReservedPath( ( preserveClonedFilesOnFailure || backupOriginalFiles ) ? "backup" : "_tmp" ); MONGO_ASSERT_ON_EXCEPTION( boost::filesystem::create_directory( reservedPath ) ); string reservedPathString = reservedPath.string(); if ( !preserveClonedFilesOnFailure ) repairFileDeleter.reset( new RepairFileDeleter( txn, dbName, reservedPathString, reservedPath ) ); { Database* originalDatabase = dbHolder().get( dbName, storageGlobalParams.dbpath ); if ( originalDatabase == NULL ) return Status( ErrorCodes::NamespaceNotFound, "database does not exist to repair" ); Database* tempDatabase = NULL; { bool justCreated = false; tempDatabase = dbHolderW().getOrCreate( dbName, reservedPathString, justCreated ); invariant( justCreated ); } map namespacesToCopy; { string ns = dbName + ".system.namespaces"; Client::Context ctx( ns ); Collection* coll = originalDatabase->getCollection( ns ); if ( coll ) { scoped_ptr it( coll->getIterator( DiskLoc(), false, CollectionScanParams::FORWARD ) ); while ( !it->isEOF() ) { DiskLoc loc = it->getNext(); BSONObj obj = coll->docFor( loc ); string ns = obj["name"].String(); NamespaceString nss( ns ); if ( nss.isSystem() ) { if ( nss.isSystemDotIndexes() ) continue; if ( nss.coll() == "system.namespaces" ) continue; } if ( !nss.isNormal() ) continue; CollectionOptions options; if ( obj["options"].isABSONObj() ) { Status status = options.parse( obj["options"].Obj() ); if ( !status.isOK() ) return status; } namespacesToCopy[ns] = options; } } } for ( map::const_iterator i = namespacesToCopy.begin(); i != namespacesToCopy.end(); ++i ) { string ns = i->first; CollectionOptions options = i->second; Collection* tempCollection = NULL; { Client::Context tempContext( ns, tempDatabase ); tempCollection = tempDatabase->createCollection( txn, ns, options, true, false ); } Client::Context readContext( ns, originalDatabase ); Collection* originalCollection = originalDatabase->getCollection( txn, ns ); invariant( originalCollection ); // data MultiIndexBlock indexBlock(txn, tempCollection ); { vector indexes; IndexCatalog::IndexIterator ii = originalCollection->getIndexCatalog()->getIndexIterator( false ); while ( ii.more() ) { IndexDescriptor* desc = ii.next(); indexes.push_back( desc->infoObj() ); } Client::Context tempContext( ns, tempDatabase ); Status status = indexBlock.init( indexes ); if ( !status.isOK() ) return status; } scoped_ptr iterator( originalCollection->getIterator( DiskLoc(), false, CollectionScanParams::FORWARD ) ); while ( !iterator->isEOF() ) { DiskLoc loc = iterator->getNext(); invariant( !loc.isNull() ); BSONObj doc = originalCollection->docFor( loc ); Client::Context tempContext( ns, tempDatabase ); StatusWith result = tempCollection->insertDocument( txn, doc, indexBlock ); if ( !result.isOK() ) return result.getStatus(); txn->recoveryUnit()->commitIfNeeded(); txn->checkForInterrupt(false); } { Client::Context tempContext( ns, tempDatabase ); Status status = indexBlock.commit(); if ( !status.isOK() ) return status; } } txn->recoveryUnit()->syncDataAndTruncateJournal(); MongoFile::flushAll(true); // need both in case journaling is disabled txn->checkForInterrupt(false); Client::Context tempContext( dbName, reservedPathString ); Database::closeDatabase( dbName, reservedPathString ); } // at this point if we abort, we don't want to delete new files // as they might be the only copies if ( repairFileDeleter.get() ) repairFileDeleter->success(); Client::Context ctx( dbName ); Database::closeDatabase(dbName, storageGlobalParams.dbpath); if ( backupOriginalFiles ) { _renameForBackup( dbName, reservedPath ); } else { // first make new directory before deleting data Path newDir = Path(storageGlobalParams.dbpath) / dbName; MONGO_ASSERT_ON_EXCEPTION(boost::filesystem::create_directory(newDir)); // this deletes old files _deleteDataFiles( dbName ); if ( !boost::filesystem::exists(newDir) ) { // we deleted because of directoryperdb // re-create MONGO_ASSERT_ON_EXCEPTION(boost::filesystem::create_directory(newDir)); } } _replaceWithRecovered( dbName, reservedPathString.c_str() ); if ( !backupOriginalFiles ) MONGO_ASSERT_ON_EXCEPTION( boost::filesystem::remove_all( reservedPath ) ); return Status::OK(); } }