/** * Copyright (C) 2017 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/platform/basic.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/query.h" #include "mongo/client/remote_command_targeter_mock.h" #include "mongo/db/catalog/catalog_raii.h" #include "mongo/db/client.h" #include "mongo/db/dbdirectclient.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/keypattern.h" #include "mongo/db/repl/replication_coordinator_mock.h" #include "mongo/db/s/collection_sharding_state.h" #include "mongo/db/s/sharding_state.h" #include "mongo/s/balancer_configuration.h" #include "mongo/s/chunk_version.h" #include "mongo/s/client/shard_registry.h" #include "mongo/s/sharding_mongod_test_fixture.h" #include "mongo/unittest/unittest.h" namespace mongo { namespace { using unittest::assertGet; using Deletion = CollectionRangeDeleter::Deletion; const NamespaceString kNss = NamespaceString("foo", "bar"); const std::string kPattern = "_id"; const BSONObj kKeyPattern = BSON(kPattern << 1); const std::string kShardName{"a"}; const HostAndPort dummyHost("dummy", 123); const NamespaceString kAdminSysVer = NamespaceString("admin", "system.version"); class CollectionRangeDeleterTest : public ShardingMongodTestFixture { protected: void setUp() override { _epoch = OID::gen(); serverGlobalParams.clusterRole = ClusterRole::ShardServer; ShardingMongodTestFixture::setUp(); replicationCoordinator()->alwaysAllowWrites(true); ASSERT_OK(initializeGlobalShardingStateForMongodForTest(ConnectionString(dummyHost))); // RemoteCommandTargeterMock::get(shardRegistry()->getConfigShard()->getTargeter()) // ->setConnectionStringReturnValue(kConfigConnStr); configTargeter()->setFindHostReturnValue(dummyHost); DBDirectClient(operationContext()).createCollection(kNss.ns()); { AutoGetCollection autoColl(operationContext(), kNss, MODE_IX); auto collectionShardingState = CollectionShardingState::get(operationContext(), kNss); const KeyPattern skPattern(kKeyPattern); auto cm = ChunkManager::makeNew( kNss, UUID::gen(), kKeyPattern, nullptr, false, epoch(), {ChunkType(kNss, ChunkRange{skPattern.globalMin(), skPattern.globalMax()}, ChunkVersion(1, 0, epoch()), ShardId("otherShard"))}); collectionShardingState->refreshMetadata( operationContext(), stdx::make_unique(cm, ShardId("thisShard"))); } } void tearDown() override { { AutoGetCollection autoColl(operationContext(), kNss, MODE_IX); auto collectionShardingState = CollectionShardingState::get(operationContext(), kNss); collectionShardingState->refreshMetadata(operationContext(), nullptr); } ShardingMongodTestFixture::tearDown(); } boost::optional next(CollectionRangeDeleter& rangeDeleter, int maxToDelete) { return CollectionRangeDeleter::cleanUpNextRange( operationContext(), kNss, epoch(), maxToDelete, &rangeDeleter); } std::shared_ptr configTargeter() const { return RemoteCommandTargeterMock::get(shardRegistry()->getConfigShard()->getTargeter()); } OID const& epoch() const { return _epoch; } std::unique_ptr makeBalancerConfiguration() override { return stdx::make_unique(); } private: OID _epoch; }; // Tests the case that there is nothing in the database. TEST_F(CollectionRangeDeleterTest, EmptyDatabase) { CollectionRangeDeleter rangeDeleter; ASSERT_FALSE(next(rangeDeleter, 1)); } // Tests the case that there is data, but it is not in a range to clean. TEST_F(CollectionRangeDeleterTest, NoDataInGivenRangeToClean) { CollectionRangeDeleter rangeDeleter; const BSONObj insertedDoc = BSON(kPattern << 25); DBDirectClient dbclient(operationContext()); dbclient.insert(kNss.toString(), insertedDoc); ASSERT_BSONOBJ_EQ(insertedDoc, dbclient.findOne(kNss.toString(), QUERY(kPattern << 25))); std::list ranges; ranges.emplace_back(Deletion{ChunkRange{BSON(kPattern << 0), BSON(kPattern << 10)}, Date_t{}}); auto when = rangeDeleter.add(std::move(ranges)); ASSERT(when && *when == Date_t{}); ASSERT_EQ(1u, rangeDeleter.size()); ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_EQ(0u, rangeDeleter.size()); ASSERT_BSONOBJ_EQ(insertedDoc, dbclient.findOne(kNss.toString(), QUERY(kPattern << 25))); ASSERT_FALSE(next(rangeDeleter, 1)); } // Tests the case that there is a single document within a range to clean. TEST_F(CollectionRangeDeleterTest, OneDocumentInOneRangeToClean) { CollectionRangeDeleter rangeDeleter; const BSONObj insertedDoc = BSON(kPattern << 5); DBDirectClient dbclient(operationContext()); dbclient.insert(kNss.toString(), BSON(kPattern << 5)); ASSERT_BSONOBJ_EQ(insertedDoc, dbclient.findOne(kNss.toString(), QUERY(kPattern << 5))); std::list ranges; auto deletion = Deletion{ChunkRange(BSON(kPattern << 0), BSON(kPattern << 10)), Date_t{}}; ranges.emplace_back(std::move(deletion)); auto when = rangeDeleter.add(std::move(ranges)); ASSERT(when && *when == Date_t{}); ASSERT_TRUE(ranges.empty()); // spliced elements out of it auto optNotifn = rangeDeleter.overlaps(ChunkRange(BSON(kPattern << 0), BSON(kPattern << 10))); ASSERT(optNotifn); auto notifn = *optNotifn; ASSERT(!notifn.ready()); // actually delete one ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT(!notifn.ready()); ASSERT_EQ(rangeDeleter.size(), 1u); // range empty, pop range, notify ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_TRUE(rangeDeleter.isEmpty()); ASSERT(notifn.ready() && notifn.waitStatus(operationContext()).isOK()); ASSERT_TRUE(dbclient.findOne(kNss.toString(), QUERY(kPattern << 5)).isEmpty()); ASSERT_FALSE(next(rangeDeleter, 1)); ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); } // Tests the case that there are multiple documents within a range to clean. TEST_F(CollectionRangeDeleterTest, MultipleDocumentsInOneRangeToClean) { CollectionRangeDeleter rangeDeleter; DBDirectClient dbclient(operationContext()); dbclient.insert(kNss.toString(), BSON(kPattern << 1)); dbclient.insert(kNss.toString(), BSON(kPattern << 2)); dbclient.insert(kNss.toString(), BSON(kPattern << 3)); ASSERT_EQUALS(3ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); std::list ranges; auto deletion = Deletion{ChunkRange(BSON(kPattern << 0), BSON(kPattern << 10)), Date_t{}}; ranges.emplace_back(std::move(deletion)); auto when = rangeDeleter.add(std::move(ranges)); ASSERT(when && *when == Date_t{}); ASSERT_TRUE(next(rangeDeleter, 100)); ASSERT_TRUE(next(rangeDeleter, 100)); ASSERT_EQUALS(0ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); ASSERT_FALSE(next(rangeDeleter, 100)); ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); } // Tests the case that there are multiple documents within a range to clean, and the range deleter // has a max deletion rate of one document per run. TEST_F(CollectionRangeDeleterTest, MultipleCleanupNextRangeCalls) { CollectionRangeDeleter rangeDeleter; DBDirectClient dbclient(operationContext()); dbclient.insert(kNss.toString(), BSON(kPattern << 1)); dbclient.insert(kNss.toString(), BSON(kPattern << 2)); dbclient.insert(kNss.toString(), BSON(kPattern << 3)); ASSERT_EQUALS(3ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); std::list ranges; auto deletion = Deletion{ChunkRange(BSON(kPattern << 0), BSON(kPattern << 10)), Date_t{}}; ranges.emplace_back(std::move(deletion)); auto when = rangeDeleter.add(std::move(ranges)); ASSERT(when && *when == Date_t{}); ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_EQUALS(2ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_EQUALS(1ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_TRUE(next(rangeDeleter, 1)); ASSERT_EQUALS(0ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 5))); ASSERT_FALSE(next(rangeDeleter, 1)); ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); } // Tests the case that there are two ranges to clean, each containing multiple documents. TEST_F(CollectionRangeDeleterTest, MultipleDocumentsInMultipleRangesToClean) { CollectionRangeDeleter rangeDeleter; DBDirectClient dbclient(operationContext()); dbclient.insert(kNss.toString(), BSON(kPattern << 1)); dbclient.insert(kNss.toString(), BSON(kPattern << 2)); dbclient.insert(kNss.toString(), BSON(kPattern << 3)); dbclient.insert(kNss.toString(), BSON(kPattern << 4)); dbclient.insert(kNss.toString(), BSON(kPattern << 5)); dbclient.insert(kNss.toString(), BSON(kPattern << 6)); ASSERT_EQUALS(6ULL, dbclient.count(kNss.toString(), BSON(kPattern << LT << 10))); std::list ranges; auto later = Date_t::now(); ranges.emplace_back(Deletion{ChunkRange{BSON(kPattern << 0), BSON(kPattern << 3)}, later}); auto when = rangeDeleter.add(std::move(ranges)); ASSERT(when && *when == later); ASSERT_TRUE(ranges.empty()); // not guaranteed by std, but failure would indicate a problem. std::list ranges2; ranges2.emplace_back(Deletion{ChunkRange{BSON(kPattern << 4), BSON(kPattern << 7)}, later}); when = rangeDeleter.add(std::move(ranges2)); ASSERT(!when); std::list ranges3; ranges3.emplace_back(Deletion{ChunkRange{BSON(kPattern << 3), BSON(kPattern << 4)}, Date_t{}}); when = rangeDeleter.add(std::move(ranges3)); ASSERT(when); auto optNotifn1 = rangeDeleter.overlaps(ChunkRange{BSON(kPattern << 0), BSON(kPattern << 3)}); ASSERT_TRUE(optNotifn1); auto& notifn1 = *optNotifn1; ASSERT_FALSE(notifn1.ready()); auto optNotifn2 = rangeDeleter.overlaps(ChunkRange{BSON(kPattern << 4), BSON(kPattern << 7)}); ASSERT_TRUE(optNotifn2); auto& notifn2 = *optNotifn2; ASSERT_FALSE(notifn2.ready()); auto optNotifn3 = rangeDeleter.overlaps(ChunkRange{BSON(kPattern << 3), BSON(kPattern << 4)}); ASSERT_TRUE(optNotifn3); auto& notifn3 = *optNotifn3; ASSERT_FALSE(notifn3.ready()); // test op== on notifications ASSERT_TRUE(notifn1 == *optNotifn1); ASSERT_FALSE(notifn1 == *optNotifn2); ASSERT_TRUE(notifn1 != *optNotifn2); ASSERT_FALSE(notifn1 != *optNotifn1); // no op log entry yet ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); ASSERT_EQUALS(6ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 7))); // catch range3, [3..4) only auto next1 = next(rangeDeleter, 100); ASSERT_TRUE(next1); ASSERT_EQUALS(*next1, Date_t{}); // no op log entry for immediate deletions ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); // 3 gone ASSERT_EQUALS(5ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 7))); ASSERT_EQUALS(2ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 4))); ASSERT_FALSE(notifn1.ready()); // no trigger yet ASSERT_FALSE(notifn2.ready()); // no trigger yet ASSERT_FALSE(notifn3.ready()); // no trigger yet // this will find the [3..4) range empty, so pop the range and notify auto next2 = next(rangeDeleter, 100); ASSERT_TRUE(next2); ASSERT_EQUALS(*next2, Date_t{}); // still no op log entry, because not delayed ASSERT_EQUALS(0ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); // deleted 1, 5 left ASSERT_EQUALS(2ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 4))); ASSERT_EQUALS(5ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 10))); ASSERT_FALSE(notifn1.ready()); // no trigger yet ASSERT_FALSE(notifn2.ready()); // no trigger yet ASSERT_TRUE(notifn3.ready()); // triggered. ASSERT_OK(notifn3.waitStatus(operationContext())); // This will find the regular queue empty, but the [0..3) range in the delayed queue. // However, the time to delete them is now, so the range is moved to the regular queue. auto next3 = next(rangeDeleter, 100); ASSERT_TRUE(next3); ASSERT_EQUALS(*next3, Date_t{}); ASSERT_FALSE(notifn1.ready()); // no trigger yet ASSERT_FALSE(notifn2.ready()); // no trigger yet // deleted 3, 3 left ASSERT_EQUALS(3ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 10))); ASSERT_EQUALS(1ULL, dbclient.count(kAdminSysVer.ns(), BSON(kPattern << "startRangeDeletion"))); // clang-format off ASSERT_BSONOBJ_EQ( BSON("_id" << "startRangeDeletion" << "ns" << kNss.ns() << "epoch" << epoch() << "min" << BSON("_id" << 0) << "max" << BSON("_id" << 3)), dbclient.findOne(kAdminSysVer.ns(), QUERY("_id" << "startRangeDeletion"))); // clang-format on // this will find the [0..3) range empty, so pop the range and notify auto next4 = next(rangeDeleter, 100); ASSERT_TRUE(next4); ASSERT_EQUALS(*next4, Date_t{}); ASSERT_TRUE(notifn1.ready()); ASSERT_OK(notifn1.waitStatus(operationContext())); ASSERT_FALSE(notifn2.ready()); // op log entry unchanged // clang-format off ASSERT_BSONOBJ_EQ( BSON("_id" << "startRangeDeletion" << "ns" << kNss.ns() << "epoch" << epoch() << "min" << BSON("_id" << 0) << "max" << BSON("_id" << 3)), dbclient.findOne(kAdminSysVer.ns(), QUERY("_id" << "startRangeDeletion"))); // clang-format on // still 3 left ASSERT_EQUALS(3ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 10))); // delete the remaining documents auto next5 = next(rangeDeleter, 100); ASSERT_TRUE(next5); ASSERT_EQUALS(*next5, Date_t{}); ASSERT_FALSE(notifn2.ready()); // Another delayed range, so logged // clang-format off ASSERT_BSONOBJ_EQ( BSON("_id" << "startRangeDeletion" << "ns" << kNss.ns() << "epoch" << epoch() << "min" << BSON("_id" << 4) << "max" << BSON("_id" << 7)), dbclient.findOne(kAdminSysVer.ns(), QUERY("_id" << "startRangeDeletion"))); // clang-format on // all docs gone ASSERT_EQUALS(0ULL, dbclient.count(kNss.ns(), BSON(kPattern << LT << 10))); // discover there are no more, pop range 2 auto next6 = next(rangeDeleter, 100); ASSERT_TRUE(next6); ASSERT_EQUALS(*next6, Date_t{}); ASSERT_TRUE(notifn2.ready()); ASSERT_OK(notifn2.waitStatus(operationContext())); // discover there are no more ranges ASSERT_FALSE(next(rangeDeleter, 1)); } } // namespace } // namespace mongo