/**
* Copyright (C) 2013 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 .
*
* 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
#include
#include "mongo/db/field_parser.h"
#include "mongo/db/range_deleter.h"
#include "mongo/db/range_deleter_mock_env.h"
#include "mongo/db/repl/repl_coordinator_global.h"
#include "mongo/db/repl/repl_coordinator_mock.h"
#include "mongo/db/repl/repl_settings.h"
#include "mongo/db/write_concern_options.h"
#include "mongo/stdx/functional.h"
#include "mongo/unittest/unittest.h"
namespace {
using std::string;
using mongo::BSONObj;
using mongo::CursorId;
using mongo::DeletedRange;
using mongo::FieldParser;
using mongo::KeyRange;
using mongo::Notification;
using mongo::RangeDeleter;
using mongo::RangeDeleterMockEnv;
using mongo::RangeDeleterOptions;
using mongo::OperationContext;
OperationContext* const noTxn = NULL; // MockEnv doesn't need txn XXX SERVER-13931
// Capped sleep interval is 640 mSec, Nyquist frequency is 1280 mSec => round up to 2 sec.
const int MAX_IMMEDIATE_DELETE_WAIT_SECS = 2;
const mongo::repl::ReplSettings replSettings;
// Should not be able to queue deletes if deleter workers were not started.
TEST(QueueDelete, CantAfterStop) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
deleter.stopWorkers();
string errMsg;
ASSERT_FALSE(deleter.queueDelete(RangeDeleterOptions(KeyRange("test.user",
BSON("x" << 120),
BSON("x" << 200),
BSON("x" << 1))),
NULL /* notifier not needed */,
&errMsg));
ASSERT_FALSE(errMsg.empty());
ASSERT_FALSE(env->deleteOccured());
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should not start delete if the set of cursors that were open when the
// delete was queued is still open.
TEST(QueuedDelete, ShouldWaitCursor) {
const string ns("test.user");
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
env->addCursorId(ns, 345);
Notification notifyDone;
RangeDeleterOptions deleterOptions(KeyRange(ns,
BSON("x" << 0),
BSON("x" << 10),
BSON("x" << 1)));
deleterOptions.waitForOpenCursors = true;
ASSERT_TRUE(deleter.queueDelete(deleterOptions,
¬ifyDone,
NULL /* errMsg not needed */));
env->waitForNthGetCursor(1u);
ASSERT_EQUALS(1U, deleter.getPendingDeletes());
ASSERT_FALSE(env->deleteOccured());
// Set the open cursors to a totally different sets of cursorIDs.
env->addCursorId(ns, 200);
env->removeCursorId(ns, 345);
notifyDone.waitToBeNotified();
ASSERT_TRUE(env->deleteOccured());
const DeletedRange deletedChunk(env->getLastDelete());
ASSERT_EQUALS(ns, deletedChunk.ns);
ASSERT_TRUE(deletedChunk.min.equal(BSON("x" << 0)));
ASSERT_TRUE(deletedChunk.max.equal(BSON("x" << 10)));
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should terminate when stop is requested.
TEST(QueuedDelete, StopWhileWaitingCursor) {
const string ns("test.user");
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
env->addCursorId(ns, 345);
Notification notifyDone;
RangeDeleterOptions deleterOptions(KeyRange(ns,
BSON("x" << 0),
BSON("x" << 10),
BSON("x" << 1)));
deleterOptions.waitForOpenCursors = true;
ASSERT_TRUE(deleter.queueDelete(deleterOptions,
¬ifyDone,
NULL /* errMsg not needed */));
env->waitForNthGetCursor(1u);
deleter.stopWorkers();
ASSERT_FALSE(env->deleteOccured());
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
static void rangeDeleterDeleteNow(RangeDeleter* deleter,
OperationContext* txn,
const RangeDeleterOptions& deleterOptions,
std::string* errMsg) {
deleter->deleteNow(txn, deleterOptions, errMsg);
}
// Should not start delete if the set of cursors that were open when the
// deleteNow method is called is still open.
TEST(ImmediateDelete, ShouldWaitCursor) {
const string ns("test.user");
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
env->addCursorId(ns, 345);
string errMsg;
RangeDeleterOptions deleterOption(KeyRange(ns,
BSON("x" << 0),
BSON("x" << 10),
BSON("x" << 1)));
deleterOption.waitForOpenCursors = true;
boost::thread deleterThread = boost::thread(mongo::stdx::bind(
rangeDeleterDeleteNow,
&deleter,
noTxn,
deleterOption,
&errMsg));
env->waitForNthGetCursor(1u);
// Note: immediate deletes has no pending state, it goes directly to inProgress
// even while waiting for cursors.
ASSERT_EQUALS(1U, deleter.getDeletesInProgress());
ASSERT_FALSE(env->deleteOccured());
// Set the open cursors to a totally different sets of cursorIDs.
env->addCursorId(ns, 200);
env->removeCursorId(ns, 345);
ASSERT_TRUE(deleterThread.timed_join(
boost::posix_time::seconds(MAX_IMMEDIATE_DELETE_WAIT_SECS)));
ASSERT_TRUE(env->deleteOccured());
const DeletedRange deletedChunk(env->getLastDelete());
ASSERT_EQUALS(ns, deletedChunk.ns);
ASSERT_TRUE(deletedChunk.min.equal(BSON("x" << 0)));
ASSERT_TRUE(deletedChunk.max.equal(BSON("x" << 10)));
ASSERT_TRUE(deletedChunk.shardKeyPattern.equal(BSON("x" << 1)));
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should terminate when stop is requested.
TEST(ImmediateDelete, StopWhileWaitingCursor) {
const string ns("test.user");
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
env->addCursorId(ns, 345);
string errMsg;
RangeDeleterOptions deleterOption(KeyRange(ns,
BSON("x" << 0),
BSON("x" << 10),
BSON("x" << 1)));
deleterOption.waitForOpenCursors = true;
boost::thread deleterThread = boost::thread(mongo::stdx::bind(
rangeDeleterDeleteNow,
&deleter,
noTxn,
deleterOption,
&errMsg));
env->waitForNthGetCursor(1u);
// Note: immediate deletes has no pending state, it goes directly to inProgress
// even while waiting for cursors.
ASSERT_EQUALS(1U, deleter.getDeletesInProgress());
ASSERT_FALSE(env->deleteOccured());
deleter.stopWorkers();
ASSERT_TRUE(deleterThread.timed_join(
boost::posix_time::seconds(MAX_IMMEDIATE_DELETE_WAIT_SECS)));
ASSERT_FALSE(env->deleteOccured());
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Tests the interaction of multiple deletes queued with different states.
// Starts by adding a new delete task, waits for the worker to work on it,
// and then adds 2 more task, one of which is ready to be deleted, while the
// other one is waiting for an open cursor. The test then makes sure that the
// deletes are performed in the right order.
TEST(MixedDeletes, MultipleDeletes) {
const string blockedNS("foo.bar");
const string ns("test.user");
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
env->addCursorId(blockedNS, 345);
env->pauseDeletes();
Notification notifyDone1;
RangeDeleterOptions deleterOption1(KeyRange(ns,
BSON("x" << 10),
BSON("x" << 20),
BSON("x" << 1)));
deleterOption1.waitForOpenCursors = true;
ASSERT_TRUE(deleter.queueDelete(deleterOption1,
¬ifyDone1,
NULL /* don't care errMsg */));
env->waitForNthPausedDelete(1u);
// Make sure that the delete is already in progress before proceeding.
ASSERT_EQUALS(1U, deleter.getDeletesInProgress());
Notification notifyDone2;
RangeDeleterOptions deleterOption2(KeyRange(blockedNS,
BSON("x" << 20),
BSON("x" << 30),
BSON("x" << 1)));
deleterOption2.waitForOpenCursors = true;
ASSERT_TRUE(deleter.queueDelete(deleterOption2,
¬ifyDone2,
NULL /* don't care errMsg */));
Notification notifyDone3;
RangeDeleterOptions deleterOption3(KeyRange(ns,
BSON("x" << 30),
BSON("x" << 40),
BSON("x" << 1)));
deleterOption3.waitForOpenCursors = true;
ASSERT_TRUE(deleter.queueDelete(deleterOption3,
¬ifyDone3,
NULL /* don't care errMsg */));
// Now, the setup is:
// { x: 10 } => { x: 20 } in progress.
// { x: 20 } => { x: 30 } waiting for cursor id 345.
// { x: 30 } => { x: 40 } waiting to be picked up by worker.
// Make sure that the current state matches the setup.
ASSERT_EQUALS(3U, deleter.getTotalDeletes());
ASSERT_EQUALS(2U, deleter.getPendingDeletes());
ASSERT_EQUALS(1U, deleter.getDeletesInProgress());
// Let the first delete proceed.
env->resumeOneDelete();
notifyDone1.waitToBeNotified();
ASSERT_TRUE(env->deleteOccured());
// { x: 10 } => { x: 20 } should be the first one since it is already in
// progress before the others are queued.
DeletedRange deleted1(env->getLastDelete());
ASSERT_EQUALS(ns, deleted1.ns);
ASSERT_TRUE(deleted1.min.equal(BSON("x" << 10)));
ASSERT_TRUE(deleted1.max.equal(BSON("x" << 20)));
ASSERT_TRUE(deleted1.shardKeyPattern.equal(BSON("x" << 1)));
// Let the second delete proceed.
env->resumeOneDelete();
notifyDone3.waitToBeNotified();
DeletedRange deleted2(env->getLastDelete());
// { x: 30 } => { x: 40 } should be next since there are still
// cursors open for blockedNS.
ASSERT_EQUALS(ns, deleted2.ns);
ASSERT_TRUE(deleted2.min.equal(BSON("x" << 30)));
ASSERT_TRUE(deleted2.max.equal(BSON("x" << 40)));
ASSERT_TRUE(deleted2.shardKeyPattern.equal(BSON("x" << 1)));
env->removeCursorId(blockedNS, 345);
// Let the last delete proceed.
env->resumeOneDelete();
notifyDone2.waitToBeNotified();
DeletedRange deleted3(env->getLastDelete());
ASSERT_EQUALS(blockedNS, deleted3.ns);
ASSERT_TRUE(deleted3.min.equal(BSON("x" << 20)));
ASSERT_TRUE(deleted3.max.equal(BSON("x" << 30)));
ASSERT_TRUE(deleted3.shardKeyPattern.equal(BSON("x" << 1)));
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should not be able to delete ranges that overlaps with a black listed range.
TEST(BlackList, CantDeleteBlackListed) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.startWorkers();
const string ns("test.user");
string errMsg;
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 100), BSON("x" << 200), &errMsg));
ASSERT_TRUE(errMsg.empty());
errMsg.clear();
ASSERT_FALSE(deleter.queueDelete(RangeDeleterOptions(KeyRange(ns,
BSON("x" << 120),
BSON("x" << 140),
BSON("x" << 1))),
NULL /* notifier not needed */,
&errMsg));
ASSERT_FALSE(errMsg.empty());
errMsg.clear();
ASSERT_FALSE(deleter.deleteNow(noTxn,
RangeDeleterOptions(KeyRange(ns,
BSON("x" << 120),
BSON("x" << 140),
BSON("x" << 1))),
&errMsg));
ASSERT_FALSE(errMsg.empty());
ASSERT_FALSE(env->deleteOccured());
deleter.stopWorkers();
}
// Should not be able to black list a range that overlaps with a range that is
// already blacklisted.
TEST(BlackList, CantDoubleBlackList) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
const string ns("test.user");
string errMsg;
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 100), BSON("x" << 200), &errMsg));
ASSERT_TRUE(errMsg.empty());
errMsg.clear();
ASSERT_FALSE(deleter.addToBlackList(ns, BSON("x" << 100), BSON("x" << 200), &errMsg));
ASSERT_FALSE(errMsg.empty());
errMsg.clear();
ASSERT_FALSE(deleter.addToBlackList(ns, BSON("x" << 80), BSON("x" << 120), &errMsg));
ASSERT_FALSE(errMsg.empty());
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should not be able to black list a range that overlaps with a range that is already
// queued for deletion.
TEST(BlackList, CantBlackListQueued) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
const string ns("test.user");
deleter.startWorkers();
// Set cursors on NS so deletes cannot be processed immediately.
env->addCursorId(ns, 58);
Notification notifyDone;
deleter.queueDelete(RangeDeleterOptions(KeyRange(ns,
BSON("x" << 0),
BSON("x" << 10),
BSON("x" << 1))),
¬ifyDone,
NULL /* errMsg not needed */);
string errMsg;
ASSERT_FALSE(deleter.addToBlackList(ns, BSON("x" << 5), BSON("x" << 15), &errMsg));
ASSERT_FALSE(errMsg.empty());
env->removeCursorId(ns, 58);
notifyDone.waitToBeNotified();
// But should be able to black list again once removed from the queue.
errMsg.clear();
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 5), BSON("x" << 15), &errMsg));
ASSERT_TRUE(errMsg.empty());
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Should not be able to black list a range that overlaps the range of an
// immediate delete that is currently in progress.
TEST(BlackList, CantBlackListImmediateInProgress) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
const string ns("test.user");
env->pauseDeletes();
string delErrMsg;
boost::thread deleterThread =
boost::thread(mongo::stdx::bind(rangeDeleterDeleteNow,
&deleter,
noTxn,
RangeDeleterOptions(KeyRange(ns,
BSON("x" << 64),
BSON("x" << 70),
BSON("x" << 1))),
&delErrMsg));
env->waitForNthPausedDelete(1u);
string blErrMsg;
ASSERT_FALSE(deleter.addToBlackList(ns, BSON("x" << 10), BSON("x" << 90), &blErrMsg));
ASSERT_FALSE(blErrMsg.empty());
env->resumeOneDelete();
deleterThread.join();
ASSERT_TRUE(delErrMsg.empty());
// Can blacklist again after delete completed.
blErrMsg.clear();
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 10), BSON("x" << 90), &blErrMsg));
ASSERT_TRUE(blErrMsg.empty());
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Undo black list should only work if the range given exactly match with an
// existing black listed range.
TEST(BlackList, UndoShouldBeExact) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
const string ns("test.user");
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 1234), BSON("x" << 8952),
NULL /* errMsg not needed */));
ASSERT_FALSE(deleter.removeFromBlackList(ns, BSON("x" << 1234), BSON("x" << 9000)));
// Range should still be blacklisted
ASSERT_FALSE(deleter.deleteNow(noTxn,
RangeDeleterOptions(KeyRange(ns,
BSON("x" << 2000),
BSON("x" << 4000),
BSON("x" << 1))),
NULL /* errMsg not needed */));
deleter.stopWorkers();
}
// Should be able to delete the range again once the black list has been undone.
TEST(BlackList, UndoBlackList) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
const string ns("test.user");
string errMsg;
ASSERT_TRUE(deleter.addToBlackList(ns, BSON("x" << 500), BSON("x" << 801), &errMsg));
ASSERT_TRUE(errMsg.empty());
errMsg.clear();
ASSERT_FALSE(deleter.deleteNow(noTxn,
RangeDeleterOptions(KeyRange(ns,
BSON("x" << 600),
BSON("x" << 700),
BSON("x" << 1))),
&errMsg));
ASSERT_FALSE(errMsg.empty());
ASSERT_TRUE(deleter.removeFromBlackList(ns, BSON("x" << 500), BSON("x" << 801)));
errMsg.clear();
ASSERT_TRUE(deleter.deleteNow(noTxn,
RangeDeleterOptions(KeyRange(ns,
BSON("x" << 600),
BSON("x" << 700),
BSON("x" << 1))),
&errMsg));
ASSERT_TRUE(errMsg.empty());
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
// Black listing should only affect the specified namespace.
TEST(BlackList, NSIsolation) {
boost::scoped_ptr mock(
new mongo::repl::ReplicationCoordinatorMock(replSettings));
mongo::repl::setGlobalReplicationCoordinator(mock.get());
RangeDeleterMockEnv* env = new RangeDeleterMockEnv();
RangeDeleter deleter(env);
deleter.addToBlackList("foo.bar", BSON("x" << 100), BSON("x" << 200),
NULL /* errMsg not needed */);
ASSERT_TRUE(deleter.deleteNow(noTxn,
RangeDeleterOptions(KeyRange("test.user",
BSON("x" << 120),
BSON("x" << 140),
BSON("x" << 1))),
NULL /* errMsg not needed */));
deleter.stopWorkers();
mongo::repl::setGlobalReplicationCoordinator(NULL);
}
} // unnamed namespace