#include #include #include #include #include #include #include #include #include #include #include #include #include "common/kv/ITransaction.h" #include "common/utils/Coroutine.h" #include "common/utils/Duration.h" #include "common/utils/FaultInjection.h" #include "common/utils/UtcTime.h" #include "fbs/core/user/User.h" #include "fbs/meta/Common.h" #include "fbs/meta/Service.h" #include "gtest/gtest.h" #include "meta/store/DirEntry.h" #include "meta/store/Inode.h" #include "meta/store/MetaStore.h" #include "meta/store/Utils.h" #include "tests/GtestHelpers.h" #include "tests/meta/MetaTestBase.h" namespace hf3fs::meta::server { template class TestRemove : public MetaTestBase {}; using KVTypes = ::testing::Types; TYPED_TEST_SUITE(TestRemove, KVTypes); TYPED_TEST(TestRemove, GC) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); config.mock_meta().gc().set_gc_directory_delay(0_s); config.mock_meta().gc().set_gc_file_delay(0_s); config.mock_meta().gc().set_scan_interval(10_ms); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); auto &gcManager = cluster.meta().getGcManager(); int fileCnt = 512; // create a directory, create some files, subdirectories, symlinks under it, then remove it recursively auto mkdirResult = co_await meta.mkdirs({SUPER_USER, "directory", p755, false}); CO_ASSERT_OK(mkdirResult); auto &directory = mkdirResult->stat; for (int i = 0; i < fileCnt; i++) { CO_ASSERT_OK(co_await meta.create({SUPER_USER, PathAt(directory.id, std::to_string(i) + ".file"), {}, 0, p644})); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, PathAt(directory.id, std::to_string(i) + ".dir"), p755, false})); CO_ASSERT_OK(co_await meta.symlink({SUPER_USER, PathAt(directory.id, std::to_string(i) + ".symlink"), "target"})); } GET_INODE_CNTS(inodes); GET_DIRENTRY_CNTS(entries); fmt::print("inodes {} dirEntries {}\n", inodes, entries); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "directory", AtFlags(), true})); GET_INODE_CNTS(numInodes); config.mock_meta().gc().set_enable(true); std::this_thread::sleep_for(std::chrono::seconds(4)); READ_WRITE_TRANSACTION_NO_COMMIT({ for (auto gcDir : gcManager.currGcDirectories()) { auto empty = co_await DirEntryList::checkEmpty(*txn, gcDir->dirId()); CO_ASSERT_OK(empty); CO_ASSERT_TRUE(empty.value()); } // all inodes except root, gcRoot, gcDirectory should be removed CO_ASSERT_INODE_CNTS(numInodes - 1 - fileCnt * 3); }); }()); } TYPED_TEST(TestRemove, Remove) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); FAULT_INJECTION_SET(10, 3); CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, "not-exists", AtFlags(), false}), MetaCode::kNotFound); CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, "/", AtFlags(), false}), StatusCode::kInvalidArg); CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, "not-exists/", AtFlags(), false}), StatusCode::kInvalidArg); CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "a/b", p755, true})); // remove a not empty directory CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, "a", AtFlags(), false}), MetaCode::kNotEmpty); // remove a not empty directory recursively CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "a", AtFlags(), true})); // stat removed directory CO_ASSERT_ERROR(co_await meta.stat({SUPER_USER, "a", AtFlags(AT_SYMLINK_NOFOLLOW)}), MetaCode::kNotFound); auto result = co_await meta.create({SUPER_USER, "file", {}, O_RDONLY, p644}); CO_ASSERT_OK(result); auto &inode = result->stat; CO_ASSERT_OK(co_await meta.hardLink({SUPER_USER, "file", "file-hardlink", AtFlags(AT_SYMLINK_NOFOLLOW)})); auto statResult = co_await meta.stat({SUPER_USER, "file-hardlink", AtFlags(AT_SYMLINK_NOFOLLOW)}); CO_ASSERT_OK(statResult); CO_ASSERT_EQ(inode.nlink, 1); CO_ASSERT_EQ(statResult->stat.nlink, 2); CO_ASSERT_EQ(inode.id, statResult->stat.id); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "file", AtFlags(), false})); statResult = co_await meta.stat({SUPER_USER, "file-hardlink", AtFlags(AT_SYMLINK_NOFOLLOW)}); CO_ASSERT_OK(statResult); CO_ASSERT_EQ(statResult->stat.nlink, 1); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, "file-hardlink", AtFlags(), false})); CO_ASSERT_ERROR(co_await meta.stat({SUPER_USER, "file-hardlink", AtFlags(AT_SYMLINK_NOFOLLOW)}), MetaCode::kNotFound); }()); } TYPED_TEST(TestRemove, RemoveRecursivePerm) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); 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, "shared", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "shared/ua/subdir", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "shared/ua2/subdir", flat::Permission(0222), true})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "shared/ua_ub/subdir", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ub, "shared/ua_ub/subdir/dir", p700, false})); CO_ASSERT_OK(co_await meta.mkdirs({ua, "shared/ua_ub2/subdir", p777, true})); CO_ASSERT_OK(co_await meta.mkdirs({ub, "shared/ua_ub2/subdir/dir", flat::Permission(0777 & S_ISVTX), false})); FAULT_INJECTION_SET(10, 3); CO_ASSERT_ERROR(co_await meta.remove({ua, "shared/ua2", AtFlags(), true}), MetaCode::kNoPermission); CO_ASSERT_ERROR(co_await meta.remove({ub, "shared/ua", AtFlags(), true}), MetaCode::kNoPermission); CO_ASSERT_OK(co_await meta.remove({ua, "shared/ua", AtFlags(), true})); CO_ASSERT_ERROR(co_await meta.remove({ua, "shared/ua_ub", AtFlags(), true}), MetaCode::kNoPermission); CO_ASSERT_ERROR(co_await meta.remove({ua, "shared/ua_ub2", AtFlags(), true}), MetaCode::kNoPermission); }()); } TYPED_TEST(TestRemove, RemoveRecursive) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); 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(); // remove a non-empty directory recursively should remove subdirectory too. auto mkdirResult = co_await meta.mkdirs({SUPER_USER, "dir", p755, true}); CO_ASSERT_OK(mkdirResult); auto dirId = mkdirResult->stat.id; // create a sub directory auto subDirResult = co_await meta.mkdirs({SUPER_USER, "dir/subdir", p755, true}); CO_ASSERT_OK(subDirResult); auto subDirId = subDirResult->stat.id; // create a file under directory auto createResult = co_await meta.create({SUPER_USER, PathAt(subDirId, "file"), {}, O_EXCL, p644}); CO_ASSERT_OK(createResult); auto fileId = createResult->stat.id; for (auto path : {"dir/.", "dir/..", "dir/subdir", "dir/subdir/file"}) { fmt::print("path {}\n", path); CO_ASSERT_OK(co_await meta.stat({SUPER_USER, PathAt(path), AtFlags(AT_SYMLINK_FOLLOW)})); } // remove directory auto removeResult = co_await meta.remove({SUPER_USER, "dir", AtFlags(), true}); CO_ASSERT_OK(removeResult); READ_ONLY_TRANSACTION({ // all inodes present for (auto inodeId : {dirId, subDirId, fileId}) { CO_ASSERT_OK((co_await Inode::snapshotLoad(*txn, inodeId)).then(checkMetaFound)); } // dir entry should present for (auto [parent, name] : {std::pair(dirId, "subdir"), std::pair(subDirId, "file")}) { fmt::print("parent {}, name {}\n", parent, name); CO_ASSERT_OK((co_await DirEntry::snapshotLoad(*txn, parent, name)).then(checkMetaFound)); } }); // lookup at deleted directory for (auto [parent, name] : {std::pair{dirId, "subdir"}, {subDirId, "file"}}) { fmt::print("parent {}, name {}\n", parent, name); CO_ASSERT_OK(co_await meta.stat({SUPER_USER, PathAt(parent, name), AtFlags(AT_SYMLINK_FOLLOW)})); } for (auto path : {"dir/.", "dir/..", "dir/subdir", "dir/subdir/file"}) { fmt::print("path {}\n", path); CO_ASSERT_ERROR(co_await meta.stat({SUPER_USER, PathAt(path), AtFlags(AT_SYMLINK_FOLLOW)}), MetaCode::kNotFound); } CO_ASSERT_ERROR(co_await meta.create({SUPER_USER, PathAt(dirId, "file2"), {}, O_EXCL, p644}), MetaCode::kNotFound); auto file2 = co_await meta.create({SUPER_USER, PathAt(subDirId, "file2"), {}, O_EXCL, p644}); CO_ASSERT_OK(file2); config.mock_meta().gc().set_enable(true); std::this_thread::sleep_for(std::chrono::seconds(2)); READ_ONLY_TRANSACTION({ // all inodes removed for (auto inodeId : {dirId, subDirId, fileId, file2->stat.id}) { CO_ASSERT_ERROR((co_await Inode::snapshotLoad(*txn, inodeId)).then(checkMetaFound), MetaCode::kNotFound); } }); // after GC, directory has deleted, create under directory should get kNotFound CO_ASSERT_ERROR(co_await meta.create({SUPER_USER, PathAt(dirId, "another-file"), {}, O_EXCL, p644}), MetaCode::kNotFound); CO_ASSERT_ERROR(co_await meta.create({SUPER_USER, PathAt(subDirId, "another-file"), {}, O_EXCL, p644}), MetaCode::kNotFound); // lookup at deleted directory should get kNotFound for (auto pair : {std::pair{dirId, "."}, {dirId, ".."}, {dirId, "subdir"}, {subDirId, "file"}}) { CO_ASSERT_ERROR(co_await meta.stat({SUPER_USER, PathAt(pair.first, pair.second), AtFlags(AT_SYMLINK_FOLLOW)}), MetaCode::kNotFound); } }()); } TYPED_TEST(TestRemove, RemoveDirectoryByInode) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); FAULT_INJECTION_SET(10, 3); // remove a non-empty directory recursively should remove subdirectory too. auto mkdirResult = co_await meta.mkdirs({SUPER_USER, "dir", p755, true}); CO_ASSERT_OK(mkdirResult); auto dirId = mkdirResult->stat.id; // create a sub directory CO_ASSERT_OK(co_await meta.mkdirs({SUPER_USER, "dir/subdir", p755, true})); auto createResult = co_await meta.create({SUPER_USER, PathAt(dirId, "file"), {}, O_EXCL, p644}); CO_ASSERT_OK(createResult); auto fileId = createResult->stat.id; CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, fileId, AtFlags(), false}), MetaCode::kNotDirectory); // remove directory CO_ASSERT_ERROR(co_await meta.remove({SUPER_USER, dirId, AtFlags(), false}), MetaCode::kNotEmpty); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, dirId, AtFlags(), true})); }()); } TYPED_TEST(TestRemove, Symlink) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); CO_ASSERT_OK(co_await meta.create({SUPER_USER, PathAt("file"), {}, O_EXCL, p644})); CO_ASSERT_OK(co_await meta.symlink({SUPER_USER, PathAt("symlink1"), "file"})); CO_ASSERT_OK(co_await meta.stat({SUPER_USER, PathAt("symlink1"), AtFlags(AT_SYMLINK_FOLLOW)})); CO_ASSERT_OK( co_await meta.hardLink({SUPER_USER, PathAt("symlink1"), PathAt("symlink2"), AtFlags(AT_SYMLINK_NOFOLLOW)})); CO_ASSERT_OK(co_await meta.stat({SUPER_USER, PathAt("symlink2"), AtFlags(AT_SYMLINK_FOLLOW)})); CO_ASSERT_OK(co_await meta.remove({SUPER_USER, PathAt("symlink1"), AtFlags(AT_SYMLINK_NOFOLLOW), false})); CO_ASSERT_OK(co_await meta.stat({SUPER_USER, PathAt("symlink2"), AtFlags(AT_SYMLINK_FOLLOW)})); }()); } TYPED_TEST(TestRemove, ConflictSet) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); auto &store = cluster.meta().getStore(); // remove file for (int i = 0; i < 100; i++) { std::string path = std::to_string(i) + ".file"; auto result = co_await meta.create({SUPER_USER, path, {}, 0, p644}); CO_ASSERT_OK(result); auto inodeId = result->stat.id; CHECK_CONFLICT_SET( [&](auto &txn) -> CoTask { auto req = RemoveReq(SUPER_USER, Path(path), AtFlags(0), false); auto removeResult = co_await store.remove(req)->run(txn); CO_ASSERT_OK(removeResult); }, (std::vector{ MetaTestHelper::getDirEntryKey(InodeId::root(), path), MetaTestHelper::getInodeKey(inodeId), }), (std::vector{ MetaTestHelper::getInodeKey(inodeId), }), false); } // remove directory for (int i = 0; i < 1; i++) { std::string path = std::to_string(i) + ".directory"; auto result = co_await meta.mkdirs({SUPER_USER, path, p755, false}); CO_ASSERT_OK(result); auto inodeId = result->stat.id; CHECK_CONFLICT_SET( [&](auto &txn) -> CoTask { auto req = RemoveReq(SUPER_USER, Path(path), AtFlags(0), false); auto removeResult = co_await store.remove(req)->run(txn); CO_ASSERT_OK(removeResult); }, (std::vector{ MetaTestHelper::getDirEntryKey(InodeId::root(), path), }), (std::vector{ MetaTestHelper::getInodeKey(inodeId), // src Inode }), false); } }()); } TYPED_TEST(TestRemove, Idempotent) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); config.mock_meta().set_idempotent_record_expire(5_s); config.mock_meta().set_idempotent_record_clean(1_s); config.set_num_meta(1); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); auto remove1 = RemoveReq({SUPER_USER, PathAt("file"), AtFlags(AT_SYMLINK_NOFOLLOW), false}); auto remove2 = RemoveReq({SUPER_USER, PathAt("file"), AtFlags(AT_SYMLINK_NOFOLLOW), false}); CO_ASSERT_ERROR(co_await meta.remove(remove1), MetaCode::kNotFound); CO_ASSERT_OK(co_await meta.create({SUPER_USER, PathAt("file"), {}, O_EXCL, p644})); CO_ASSERT_ERROR(co_await meta.remove(remove1), MetaCode::kNotFound); CO_ASSERT_OK(co_await meta.remove(remove2)); CO_ASSERT_OK(co_await meta.remove(remove2)); CO_ASSERT_ERROR(co_await meta.remove(remove1), MetaCode::kNotFound); co_await folly::coro::sleep(2_s); CO_ASSERT_OK(co_await meta.remove(remove2)); co_await folly::coro::sleep(5_s); CO_ASSERT_ERROR(co_await meta.remove(remove2), MetaCode::kNotFound); // remove check inode id CO_ASSERT_OK(co_await meta.create({SUPER_USER, PathAt("file"), {}, O_EXCL, p644})); CO_ASSERT_ERROR( co_await meta.remove( {SUPER_USER, PathAt("file"), AtFlags(AT_SYMLINK_NOFOLLOW), false, false, InodeId(folly::Random::rand64())}), MetaCode::kNotFound); co_return; }()); } TYPED_TEST(TestRemove, ConcurrentCreate) { folly::coro::blockingWait([&]() -> CoTask { MockCluster::Config config; config.mock_meta().gc().set_enable(false); config.set_num_meta(1); auto cluster = this->createMockCluster(config); auto &meta = cluster.meta().getOperator(); auto &store = cluster.meta().getStore(); // rmdir and concurrent create should conflict for (int i = 0; i < 100; i++) { auto dirPath = std::to_string(i) + ".directory"; auto childPath = "child"; auto mkdir = co_await meta.mkdirs({SUPER_USER, dirPath, p755, true}); CO_ASSERT_OK(mkdir); auto parentId = mkdir->stat.id; auto remove = [&](auto &txn) -> CoTask { auto req = RemoveReq(SUPER_USER, Path(dirPath), AtFlags(0), false); auto removeResult = co_await store.remove(req)->run(txn); CO_ASSERT_OK(removeResult); }; auto create = [&](auto &txn) -> CoTask { if (i % 2 == 0) { auto req = MkdirsReq(SUPER_USER, PathAt(parentId, childPath), p755, false); auto mkdirResult = co_await store.mkdirs(req)->run(txn); CO_ASSERT_OK(mkdirResult); } else { auto req = CreateReq(SUPER_USER, PathAt(parentId, childPath), {}, O_RDONLY, p644); auto createResult = co_await BatchedOp::create(store, txn, req); CO_ASSERT_OK(createResult); } }; if (i % 2 == 0) { CO_ASSERT_CONFLICT(remove, create); } else { CO_ASSERT_CONFLICT(create, remove); } } }()); } } // namespace hf3fs::meta::server