#include #include #include #include #include #include #include #include #include "common/utils/Coroutine.h" #include "common/utils/FaultInjection.h" #include "fbs/core/user/User.h" #include "fbs/meta/Common.h" #include "gtest/gtest.h" #include "meta/store/DirEntry.h" #include "meta/store/Inode.h" #include "meta/store/MetaStore.h" #include "meta/store/ops/BatchOperation.h" #include "tests/GtestHelpers.h" #include "tests/meta/MetaTestBase.h" namespace hf3fs::meta::server { namespace { template class TestRename : public MetaTestBase {}; using KVTypes = ::testing::Types; TYPED_TEST_SUITE(TestRename, KVTypes); TYPED_TEST(TestRename, Basic) { folly::coro::blockingWait([&]() -> CoTask { auto cluster = this->createMockCluster(); auto &meta = cluster.meta().getOperator(); FAULT_INJECTION_SET(10, 5); // create a, rename a -> b, stat a -> InodeId::root(), stat b -> Inode CO_ASSERT_OK(co_await meta.create({SUPER_USER, "a", {}, O_RDONLY, p644})); CO_ASSERT_OK(co_await meta.rename({SUPER_USER, "a", "b"})); auto a = co_await meta.stat({SUPER_USER, "a", AtFlags(AT_SYMLINK_FOLLOW)}); CO_ASSERT_ERROR(a, MetaCode::kNotFound); auto b = co_await meta.stat({SUPER_USER, "b", AtFlags(AT_SYMLINK_FOLLOW)}); CO_ASSERT_OK(b); // create file c, d, rename c -> d, c will replace d InodeId cId; auto c = co_await meta.create({SUPER_USER, "c", {}, O_RDONLY, p644}); auto d = co_await meta.create({SUPER_USER, "d", {}, O_RDONLY, p644}); CO_ASSERT_OK(c); CO_ASSERT_OK(d); cId = c->stat.id; // check rename return inode auto renameResult = co_await meta.rename({SUPER_USER, "c", "d"}); CO_ASSERT_OK(renameResult); CO_ASSERT_TRUE(renameResult->stat.has_value()); CO_ASSERT_EQ(renameResult->stat->id, cId); auto statResult = co_await meta.stat({SUPER_USER, "d", AtFlags(AT_SYMLINK_FOLLOW)}); CO_ASSERT_OK(statResult); CO_ASSERT_EQ(statResult->stat, renameResult->stat); auto c1 = co_await meta.stat({SUPER_USER, "c", AtFlags(AT_SYMLINK_FOLLOW)}); CO_ASSERT_ERROR(c1, MetaCode::kNotFound); auto d1 = co_await meta.stat({SUPER_USER, "d", AtFlags(AT_SYMLINK_FOLLOW)}); CO_ASSERT_OK(d1); CO_ASSERT_EQ(d1->stat.id, cId); // create directory e, f/g, rename e -> f, should return not empty auto f = co_await meta.mkdirs({SUPER_USER, "e", p755, false}); auto g = co_await meta.mkdirs({SUPER_USER, "f/g", p755, true}); CO_ASSERT_OK(f); CO_ASSERT_OK(g); CO_ASSERT_ERROR(co_await meta.rename({SUPER_USER, "e", "f"}), MetaCode::kNotEmpty); // remove f/g FAULT_INJECTION_SET(0, 0); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "f/g", AtFlags(), false})); // rename e -> f again FAULT_INJECTION_SET(10, 5); CO_ASSERT_OK(co_await meta.rename({SUPER_USER, "e", "f"})); }()); } TYPED_TEST(TestRename, Directory) { folly::coro::blockingWait([&]() -> CoTask { auto cluster = this->createMockCluster(); auto &meta = cluster.meta().getOperator(); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir1", p777, false})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir2", p777, false})); auto stat1 = (co_await meta.stat({SUPER_USER, "dir1", AtFlags()}))->stat; CO_ASSERT_EQ(stat1.asDirectory().name, "dir1"); CO_ASSERT_OK(co_await meta.rename({SUPER_USER, "dir1", "dir2"})); auto stat2 = (co_await meta.stat({SUPER_USER, "dir2", AtFlags()}))->stat; CO_ASSERT_EQ(stat2.asDirectory().name, "dir2"); CO_ASSERT_EQ(stat2.id, stat1.id); }()); } TYPED_TEST(TestRename, Trash) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(true); config.mock_meta().gc().set_gc_directory_delay(0_s); config.mock_meta().gc().set_gc_file_delay(0_s); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "trash", p777, false})); CO_ASSERT_OK(co_await meta.create({SUPER_USER, "dir/a", {}, O_RDONLY, p644})); CO_ASSERT_OK(co_await meta.create({SUPER_USER, "dir/b", {}, O_RDONLY, p644})); CO_ASSERT_OK(co_await meta.rename({SUPER_USER, "dir/a", "trash/trash_name", true})); CO_ASSERT_ERROR(co_await meta.rename({SUPER_USER, "dir/b", "trash/trash_name", true}), MetaCode::kExists); }()); } TYPED_TEST(TestRename, GC) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(true); config.mock_meta().gc().set_gc_directory_delay(0_s); config.mock_meta().gc().set_gc_file_delay(0_s); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); // rename directory to trash, should be owner and rwx permission CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "trash", p777, true})); auto ua = flat::UserInfo(flat::Uid(1), flat::Gid(1)); auto ub = flat::UserInfo(flat::Uid(2), flat::Gid(2)); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "ua", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "ua/data-500", flat::Permission(0500), true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "ua/data-777", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "trash/ua", p777, true})); CO_ASSERT_ERROR(co_await meta.rename({ua, "ua/data-777", "trash/data-777"}), MetaCode::kNoPermission); CO_ASSERT_OK(co_await meta.rename({ua, "ua/data-777", "trash/data-777", true})); CO_ASSERT_ERROR(co_await meta.rename({ua, "ua/data-500", "trash/data-500", true}), MetaCode::kNoPermission); CO_ASSERT_OK(co_await meta.rename({ua, "trash/ua", "trash/ua-dest"})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "ua/dir_has_root", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "ua/dir_has_root/root", p700, true})); CO_ASSERT_ERROR(co_await meta.rename({ua, "ua/dir_has_root", "trash/dir_has_root", true}), MetaCode::kNoPermission); // rename directory to another directory which is under GC, should fail CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir1/subdir", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir2", p777, false})); auto stat = (co_await meta.stat({SUPER_USER, "dir1", AtFlags()}))->stat; CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "dir1", AtFlags(), true})); CO_ASSERT_ERROR(co_await meta.rename({SUPER_USER, "dir2", PathAt(stat.id, "dir")}), MetaCode::kNotFound); CO_ASSERT_ERROR(co_await meta.rename({SUPER_USER, "dir2", PathAt(InodeId::gcRoot(), "orphan")}), MetaCode::kNoPermission); // rename replace a file, the file should be removed by GC. InodeId oldDstId; // create src and dst auto createSrc = co_await meta.create({SUPER_USER, "a", {}, O_RDONLY, p644}); auto createDst = co_await meta.create({SUPER_USER, "b", {}, O_RDONLY, p644}); CO_ASSERT_OK(createSrc); CO_ASSERT_OK(createDst); oldDstId = createDst->stat.id; // do rename to replace old dst CO_ASSERT_OK(co_await meta.rename({SUPER_USER, "a", "b"})); // wait GC tasks std::this_thread::sleep_for(std::chrono::seconds(1)); // GC should remove old dst CO_ASSERT_INODE_NOT_EXISTS(oldDstId); }()); } TYPED_TEST(TestRename, ConflictSet) { folly::coro::blockingWait([&]() -> CoTask { auto cluster = this->createMockCluster(); auto &meta = cluster.meta().getOperator(); auto &store = cluster.meta().getStore(); for (int i = 0; i < 100; i++) { std::string src = std::to_string(i) + ".src"; std::string dst = std::to_string(i) + ".dst"; InodeId srcId; // create src first auto result = co_await meta.mkdirs({SUPER_USER, src, p755, false}); CO_ASSERT_OK(result); srcId = result->stat.id; CHECK_CONFLICT_SET( [&](auto &txn) -> CoTask { auto req = RenameReq(SUPER_USER, Path(src), Path(dst)); auto removeResult = co_await store.rename(req)->run(txn); CO_ASSERT_OK(removeResult); }, (std::vector{ MetaTestHelper::getDirEntryKey(InodeId::root(), src), // src dirEntry MetaTestHelper::getDirEntryKey(InodeId::root(), dst), // dst dirEntry MetaTestHelper::getInodeKey(InodeId::root()), // dst parent Inode MetaTestHelper::getInodeKey(srcId), // src Inode }), (std::vector{ MetaTestHelper::getDirEntryKey(InodeId::root(), src), // src dirEntry MetaTestHelper::getDirEntryKey(InodeId::root(), dst), // dst dirEntry MetaTestHelper::getInodeKey(srcId), // src Inode }), false); } }()); } TYPED_TEST(TestRename, Concurrent) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.set_num_meta(1); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); auto &store = cluster.meta().getStore(); auto rename = [&](Path src, Path dst) -> std::function(IReadWriteTransaction &)> { return [=, &store](auto &txn) -> CoTask { // rename src to dst auto req = RenameReq(SUPER_USER, Path(src), Path(dst)); auto renameResult = co_await store.rename(req)->run(txn); CO_ASSERT_OK(renameResult); }; }; auto create = [&](InodeId parent, Path path, bool directory) -> std::function(IReadWriteTransaction &)> { EXPECT_FALSE(path.has_parent_path()); return [=, &store](auto &txn) -> CoTask { // create or mkdir at dst if (directory) { auto req = MkdirsReq(SUPER_USER, PathAt(parent, path), p755, true); auto mkdirResult = co_await store.mkdirs(req)->run(txn); CO_ASSERT_OK(mkdirResult); } else { auto req = CreateReq(SUPER_USER, PathAt(parent, path), {}, O_RDONLY, p644); CO_ASSERT_OK(co_await BatchedOp::create(store, txn, req)); } }; }; // rename src -> dst and concurrent create dst should conflict for (int i = 0; i < 100; i++) { std::string src = std::to_string(i) + ".src-1"; std::string dst = std::to_string(i) + ".dst-1"; // create src first CO_ASSERT_OK(co_await meta.create({SUPER_USER, src, {}, O_RDONLY, p644})); if (i % 2 == 0) { CO_ASSERT_CONFLICT(rename(src, dst), create(InodeId::root(), dst, i % 2 == 0)); } else { CO_ASSERT_CONFLICT(create(InodeId::root(), dst, i % 2 == 0), rename(src, dst)); } } // rename to same destination should conflict for (int i = 0; i < 100; i++) { std::string src1 = std::to_string(i) + ".src-2.1"; std::string src2 = std::to_string(i) + ".src-2.2"; std::string dst = std::to_string(i) + ".dst-2"; for (auto path : {src1, src2}) { CO_ASSERT_OK(co_await meta.create({SUPER_USER, path, {}, O_RDONLY, p644})); } auto rename1 = rename(src1, dst); // rename src1 to dst auto rename2 = rename(src2, dst); // rename src2 to dst if (i % 2 == 0) { CO_ASSERT_CONFLICT(rename1, rename2); } else { CO_ASSERT_CONFLICT(rename2, rename1); } } // rename src to different dst should be conflict for (int i = 0; i < 100; i++) { std::string src = std::to_string(i) + ".src-3"; std::string dst1 = std::to_string(i) + ".dst-3.1"; std::string dst2 = std::to_string(i) + ".dst-3.2"; // create src1, src2 first CO_ASSERT_OK(co_await meta.create({SUPER_USER, Path(src), {}, O_RDONLY, p644})); auto rename1 = rename(src, dst1); // rename src to dst1 auto rename2 = rename(src, dst2); // rename src to dst2 if (i % 2 == 0) { CO_ASSERT_CONFLICT(rename1, rename2); } else { CO_ASSERT_CONFLICT(rename2, rename1); } } // rename different srcs to different dsts shouldn't be conflict for (int i = 0; i < 100; i++) { std::string src1 = std::to_string(i) + ".src-4.1"; std::string src2 = std::to_string(i) + ".src-4.2"; std::string dst1 = std::to_string(i) + ".dst-4.1"; std::string dst2 = std::to_string(i) + ".dst-4.2"; for (auto path : {src1, src2}) { CO_ASSERT_OK(co_await meta.create({SUPER_USER, path, {}, O_RDONLY, p644})); } auto rename1 = rename(src1, dst1); // rename src1 to dst1 auto rename2 = rename(src2, dst2); // rename src2 to dst2 CO_ASSERT_NO_CONFLICT(rename1, rename2); } // rename replace a empty directory and create under directory should be conflict. for (int i = 0; i < 100; i++) { std::string src = std::to_string(i) + ".src-5"; std::string dst = std::to_string(i) + ".dst-5"; InodeId oldDstId; // create src and dst first auto result = co_await meta.mkdirs({SUPER_USER, src, p755, false}); CO_ASSERT_OK(result); result = co_await meta.mkdirs({SUPER_USER, dst, p755, false}); CO_ASSERT_OK(result); oldDstId = result->stat.id; if (i % 2 == 0) { CO_ASSERT_CONFLICT(rename(src, dst), create(oldDstId, "child", (i % 2 == 0))); } else { CO_ASSERT_CONFLICT(create(oldDstId, "child", (i % 2 == 0)), rename(src, dst)); } } // mkdir /a /b/d, rename /a -> /b/d/e and rename /b/d -> /a/c should conflict // mkdir /a, /b/d/e CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "a", p755, true})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "b/d", p755, true})); if (folly::Random::oneIn(2)) { CO_ASSERT_CONFLICT(rename("/a", "/b/d/e"), rename("/b/d", "/a/c")); } else { CO_ASSERT_CONFLICT(rename("/b/d", "/a/c"), rename("/a", "/b/d/e")); } }()); } } // namespace } // namespace hf3fs::meta::server