mirror of
https://github.com/deepseek-ai/3FS
synced 2025-06-26 18:16:45 +00:00
Initial commit
This commit is contained in:
11
tests/CMakeLists.txt
Normal file
11
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
add_subdirectory(lib)
|
||||
add_subdirectory(main)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(analytics)
|
||||
add_subdirectory(meta)
|
||||
add_subdirectory(scripts)
|
||||
add_subdirectory(client)
|
||||
add_subdirectory(kv)
|
||||
add_subdirectory(storage)
|
||||
add_subdirectory(mgmtd)
|
||||
add_subdirectory(migration)
|
||||
193
tests/FakeMgmtdClient.h
Normal file
193
tests/FakeMgmtdClient.h
Normal file
@@ -0,0 +1,193 @@
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/Synchronized.h>
|
||||
#include <folly/concurrency/AtomicSharedPtr.h>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "client/mgmtd/ICommonMgmtdClient.h"
|
||||
#include "client/mgmtd/RoutingInfo.h"
|
||||
#include "common/app/ClientId.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
#include "fbs/mgmtd/ChainRef.h"
|
||||
#include "fbs/mgmtd/ChainTargetInfo.h"
|
||||
#include "fbs/mgmtd/ClientSession.h"
|
||||
#include "fbs/mgmtd/MgmtdTypes.h"
|
||||
#include "fbs/mgmtd/RoutingInfo.h"
|
||||
#include "fbs/mgmtd/Rpc.h"
|
||||
#include "fbs/mgmtd/TargetInfo.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
class FakeMgmtdClient : public hf3fs::client::ICommonMgmtdClient {
|
||||
public:
|
||||
static std::shared_ptr<FakeMgmtdClient> create(
|
||||
std::vector<std::tuple<flat::ChainTableId, size_t, size_t>> tables = {},
|
||||
size_t numMetas = 1,
|
||||
size_t numStorages = 1) {
|
||||
auto mgmtd = std::make_shared<FakeMgmtdClient>();
|
||||
for (auto [table, numChains, numReplica] : tables) {
|
||||
mgmtd->addChainTable(table, numChains, numReplica);
|
||||
}
|
||||
mgmtd->addNodes(numMetas, numStorages);
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
mgmtd->addClient(Uuid::random());
|
||||
}
|
||||
return mgmtd;
|
||||
}
|
||||
|
||||
FakeMgmtdClient()
|
||||
: routingInfo_(std::make_shared<hf3fs::client::RoutingInfo>(std::make_shared<hf3fs::flat::RoutingInfo>(),
|
||||
SteadyClock::now())) {}
|
||||
|
||||
std::shared_ptr<hf3fs::client::RoutingInfo> getRoutingInfo() override { return routingInfo_.load(); }
|
||||
|
||||
CoTryTask<void> refreshRoutingInfo(bool) override { co_return Void{}; }
|
||||
|
||||
bool addRoutingInfoListener(String name, RoutingInfoListener func) override {
|
||||
listeners_[name] = func;
|
||||
return true;
|
||||
}
|
||||
bool removeRoutingInfoListener(std::string_view name) override {
|
||||
listeners_.erase(std::string(name));
|
||||
return true;
|
||||
}
|
||||
|
||||
CoTryTask<mgmtd::ListClientSessionsRsp> listClientSessions() override {
|
||||
auto guard = sessions_.lock();
|
||||
mgmtd::ListClientSessionsRsp rsp;
|
||||
rsp.bootstrapping = false;
|
||||
rsp.sessions = *guard;
|
||||
for (auto &session : rsp.sessions) {
|
||||
session.lastExtend = UtcClock::now();
|
||||
}
|
||||
co_return rsp;
|
||||
}
|
||||
|
||||
std::set<Uuid> getActiveClients() {
|
||||
std::set<Uuid> active;
|
||||
auto guard = sessions_.lock();
|
||||
for (const auto &client : *guard) {
|
||||
active.insert(Uuid::fromHexString(client.clientId).value());
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
ClientId getOneActiveClient() {
|
||||
auto guard = sessions_.lock();
|
||||
return ClientId(Uuid::fromHexString(guard->at(folly::Random::rand32(guard->size())).clientId).value());
|
||||
}
|
||||
|
||||
void clearActiveClient() { sessions_.lock()->clear(); }
|
||||
|
||||
void addClient(Uuid client) {
|
||||
flat::ClientSession session;
|
||||
session.clientId = client.toHexString();
|
||||
sessions_.lock()->push_back(session);
|
||||
}
|
||||
|
||||
CoTryTask<mgmtd::GetClientSessionRsp> getClientSession(const String &) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<std::optional<flat::ConfigInfo>> getConfig(flat::NodeType, flat::ConfigVersion) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<RHStringHashMap<flat::ConfigVersion>> getConfigVersions() override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<std::vector<flat::TagPair>> getUniversalTags(const String &universalId) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
std::shared_ptr<hf3fs::client::RoutingInfo> cloneRoutingInfo() {
|
||||
auto curr = routingInfo_.load();
|
||||
auto next = std::make_shared<hf3fs::client::RoutingInfo>(std::make_shared<hf3fs::flat::RoutingInfo>(*curr->raw()),
|
||||
SteadyClock::now());
|
||||
next->raw()->routingInfoVersion++;
|
||||
return next;
|
||||
}
|
||||
|
||||
void setRoutingInfo(std::shared_ptr<hf3fs::client::RoutingInfo> next) {
|
||||
routingInfo_.store(next);
|
||||
for (auto &[name, func] : listeners_) {
|
||||
func(routingInfo_.load());
|
||||
}
|
||||
}
|
||||
|
||||
void addChainTable(flat::ChainTableId tableId, size_t numChains, size_t numReplica = 1) {
|
||||
auto next = cloneRoutingInfo();
|
||||
SCOPE_EXIT { setRoutingInfo(next); };
|
||||
auto &routing = next->raw();
|
||||
if (!routing->chainTables.contains(tableId)) {
|
||||
routing->chainTables[tableId] = {};
|
||||
}
|
||||
auto version = flat::ChainTableVersion(routing->chainTables[tableId].size() + 1);
|
||||
auto &table = routing->chainTables[tableId][version];
|
||||
table.chainTableVersion = version;
|
||||
for (size_t index = 1; index <= numChains; index++) {
|
||||
flat::ChainInfo chain;
|
||||
chain.chainId = flat::ChainId(tableId * 100000 + index);
|
||||
chain.chainVersion = flat::ChainVersion(1);
|
||||
chain.targets = {};
|
||||
for (size_t replica = 0; replica < numReplica; replica++) {
|
||||
flat::ChainTargetInfo target;
|
||||
target.targetId = flat::TargetId(folly::Random::rand32());
|
||||
target.publicState = flat::PublicTargetState::SERVING;
|
||||
chain.targets.push_back(target);
|
||||
}
|
||||
routing->chains[chain.chainId] = chain;
|
||||
table.chains.push_back(chain.chainId);
|
||||
}
|
||||
}
|
||||
|
||||
void clearNodes() {
|
||||
auto next = cloneRoutingInfo();
|
||||
SCOPE_EXIT { setRoutingInfo(next); };
|
||||
next->raw()->nodes.clear();
|
||||
}
|
||||
|
||||
void addNodes(size_t numMetas, size_t numStorages) {
|
||||
auto next = cloneRoutingInfo();
|
||||
SCOPE_EXIT { setRoutingInfo(next); };
|
||||
auto &routing = next->raw();
|
||||
for (size_t i = 0; i < numMetas; i++) {
|
||||
auto id = metaId_++;
|
||||
flat::NodeInfo info;
|
||||
info.app.nodeId = flat::NodeId(id);
|
||||
info.app.hostname = fmt::format("host-{}", id);
|
||||
info.type = flat::NodeType::META;
|
||||
info.status = flat::NodeStatus::HEARTBEAT_CONNECTED;
|
||||
info.app.serviceGroups.emplace_back(
|
||||
std::set<String>{"MetaSerde"},
|
||||
std::vector<net::Address>{net::Address::from(fmt::format("RDMA://127.0.0.1:{}", id)).value(),
|
||||
net::Address::from(fmt::format("TCP://127.0.0.1:{}", id)).value()});
|
||||
routing->nodes[flat::NodeId(id)] = info;
|
||||
}
|
||||
for (size_t i = 0; i < numStorages; i++) {
|
||||
auto id = storageId_++;
|
||||
flat::NodeInfo info;
|
||||
info.app.nodeId = flat::NodeId(id);
|
||||
info.app.hostname = fmt::format("host-{}", id);
|
||||
info.type = flat::NodeType::STORAGE;
|
||||
info.status = flat::NodeStatus::HEARTBEAT_CONNECTED;
|
||||
info.app.serviceGroups.emplace_back(
|
||||
std::set<String>{"Storage"},
|
||||
std::vector<net::Address>{net::Address::from(fmt::format("RDMA://127.0.0.1:{}", id)).value(),
|
||||
net::Address::from(fmt::format("TCP://127.0.0.1:{}", id)).value()});
|
||||
routing->nodes[flat::NodeId(id)] = info;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
folly::Synchronized<std::vector<flat::ClientSession>, std::mutex> sessions_;
|
||||
folly::atomic_shared_ptr<client::RoutingInfo> routingInfo_;
|
||||
std::map<std::string, RoutingInfoListener> listeners_;
|
||||
size_t metaId_ = 50;
|
||||
size_t storageId_ = 10000;
|
||||
};
|
||||
|
||||
} // namespace hf3fs::tests
|
||||
51
tests/GtestHelpers.h
Normal file
51
tests/GtestHelpers.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/experimental/TestUtil.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#define ASSERT_OK(code_to_run) \
|
||||
do { \
|
||||
auto &&_status = code_to_run; \
|
||||
ASSERT_TRUE(_status) << _status.error().describe(); \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_RESULT_EQ(result, expr) \
|
||||
do { \
|
||||
auto &&_res = (expr); \
|
||||
ASSERT_OK(_res); \
|
||||
ASSERT_EQ((result), *_res); \
|
||||
} while (false)
|
||||
|
||||
#define ASSERT_ERROR(code_to_run, expected_code) \
|
||||
do { \
|
||||
auto &&_status = code_to_run; \
|
||||
ASSERT_TRUE(_status.hasError()); \
|
||||
ASSERT_EQ(_status.error().code(), expected_code) << _status.error().describe(); \
|
||||
} while (0)
|
||||
|
||||
#define CO_ASSERT_OK(code_to_run) \
|
||||
do { \
|
||||
auto &&_status = code_to_run; \
|
||||
CO_ASSERT_FALSE(_status.hasError()) << _status.error().describe(); \
|
||||
} while (0)
|
||||
|
||||
#define CO_ASSERT_ERROR(code_to_run, expected_code) \
|
||||
do { \
|
||||
auto &&_status = code_to_run; \
|
||||
CO_ASSERT_TRUE(_status.hasError()); \
|
||||
CO_ASSERT_EQ(_status.error().code(), expected_code) << _status.error().describe(); \
|
||||
} while (0)
|
||||
|
||||
#define CO_AWAIT_ASSERT_ERROR(error_code, command) \
|
||||
do { \
|
||||
auto _r = co_await (command); \
|
||||
CO_ASSERT_TRUE(_r.hasError()); \
|
||||
CO_ASSERT_EQ(_r.error().code(), error_code) << _r.error().describe(); \
|
||||
} while (0)
|
||||
|
||||
#define CO_AWAIT_ASSERT_OK(command) \
|
||||
do { \
|
||||
auto _r = co_await (command); \
|
||||
CO_ASSERT_FALSE(_r.hasError()) << _r.error().describe(); \
|
||||
} while (0)
|
||||
1
tests/analytics/CMakeLists.txt
Normal file
1
tests/analytics/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
target_add_test(test_analytics analytics meta common storage-fbs meta-fbs mgmtd-fbs)
|
||||
138
tests/analytics/TestSerdeObjectReader.cc
Normal file
138
tests/analytics/TestSerdeObjectReader.cc
Normal file
@@ -0,0 +1,138 @@
|
||||
#include <arrow/io/file.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <optional>
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "analytics/SerdeObjectReader.h"
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "fbs/core/user/User.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
template <serde::SerdeType T>
|
||||
void readAndCompareSerdeObjectDump(const T &expected) {
|
||||
{
|
||||
auto writer = SerdeObjectWriter<T>::open(fmt::format("{}.parquet", nameof::nameof_short_type<T>()));
|
||||
ASSERT_NE(writer, nullptr);
|
||||
*writer << expected << parquet::EndRowGroup;
|
||||
}
|
||||
|
||||
auto reader = SerdeObjectReader<T>::open(fmt::format("{}.parquet", nameof::nameof_short_type<T>()));
|
||||
ASSERT_NE(reader, nullptr);
|
||||
T fromFile;
|
||||
*reader >> fromFile;
|
||||
|
||||
ASSERT_TRUE(serde::equals(expected, fromFile))
|
||||
<< "Expected: " << serde::toJsonString(expected, true, true) << std::endl
|
||||
<< "FromFile: " << serde::toJsonString(fromFile, true, true);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectReader, ChunkMeta) {
|
||||
storage::ChunkMeta chunkmeta{
|
||||
.chunkId = storage::ChunkId(1, 1),
|
||||
.updateVer = storage::ChunkVer(1),
|
||||
.commitVer = storage::ChunkVer(2),
|
||||
.checksum =
|
||||
storage::ChecksumInfo{
|
||||
.type = storage::ChecksumType::CRC32C,
|
||||
.value = 123,
|
||||
},
|
||||
};
|
||||
|
||||
readAndCompareSerdeObjectDump(chunkmeta);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectReader, Inode) {
|
||||
meta::Inode inode{
|
||||
meta::InodeId{0xFF},
|
||||
meta::InodeData{
|
||||
.type =
|
||||
meta::File{
|
||||
meta::Layout{
|
||||
.tableId = meta::ChainTableId{1},
|
||||
.tableVersion = meta::ChainTableVersion{1},
|
||||
.chains = meta::Layout::ChainRange(101 /*baseIndex*/,
|
||||
meta::Layout::ChainRange::Shuffle::STD_SHUFFLE_MT19937,
|
||||
0xFFEE /*seed*/),
|
||||
},
|
||||
},
|
||||
.nlink = 10,
|
||||
.atime = UtcTime::clock::now(),
|
||||
.ctime = UtcTime::clock::now(),
|
||||
.mtime = UtcTime::clock::now(),
|
||||
},
|
||||
};
|
||||
|
||||
readAndCompareSerdeObjectDump(inode);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectReader, DirEntry) {
|
||||
meta::DirEntry entry{meta::InodeId{0x10},
|
||||
"direntry",
|
||||
meta::DirEntryData{meta::InodeId{0xFF},
|
||||
meta::InodeType::File,
|
||||
meta::Acl(flat::Uid(1), flat::Gid(1), flat::Permission(0644))}};
|
||||
|
||||
readAndCompareSerdeObjectDump(entry);
|
||||
entry.dirAcl = std::nullopt;
|
||||
readAndCompareSerdeObjectDump(entry);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectReader, IOResult) {
|
||||
storage::IOResult ioResult{
|
||||
1024U,
|
||||
storage::ChunkVer{1},
|
||||
storage::ChunkVer{2},
|
||||
storage::ChecksumInfo{storage::ChecksumType::CRC32C, 0xFFEEAABB},
|
||||
storage::ChainVer{123},
|
||||
};
|
||||
|
||||
readAndCompareSerdeObjectDump(ioResult);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectReader, UpdateReq) {
|
||||
storage::UpdateReq updateReq{
|
||||
.payload =
|
||||
{
|
||||
.offset = 123,
|
||||
.length = 456,
|
||||
.chunkSize = 789,
|
||||
.key =
|
||||
storage::GlobalKey{
|
||||
.vChainId =
|
||||
storage::VersionedChainId{
|
||||
.chainId = storage::ChainId{10001},
|
||||
.chainVer = storage::ChainVer{2000},
|
||||
},
|
||||
},
|
||||
// .rdmabuf = net::RDMARemoteBuf{},
|
||||
.updateVer = storage::ChunkVer{999},
|
||||
.updateType = storage::UpdateType::WRITE,
|
||||
.checksum =
|
||||
storage::ChecksumInfo{
|
||||
.type = storage::ChecksumType::CRC32C,
|
||||
.value = 0xFFEEAABB,
|
||||
},
|
||||
// .inlinebuf =
|
||||
// storage::UInt8Vector{
|
||||
// .data = std::vector<uint8_t>(10, 0xFF),
|
||||
// },
|
||||
},
|
||||
.tag =
|
||||
storage::MessageTag{
|
||||
ClientId::random(),
|
||||
storage::RequestId{101},
|
||||
storage::UpdateChannel{
|
||||
.id = storage::ChannelId{1},
|
||||
.seqnum = storage::ChannelSeqNum{1},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
readAndCompareSerdeObjectDump(updateReq);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
115
tests/analytics/TestSerdeObjectVisitor.cc
Normal file
115
tests/analytics/TestSerdeObjectVisitor.cc
Normal file
@@ -0,0 +1,115 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <parquet/schema.h>
|
||||
#include <string_view>
|
||||
|
||||
#include "analytics/SerdeObjectVisitor.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
class DebugObjectVisitor : public BaseObjectVisitor<DebugObjectVisitor> {
|
||||
public:
|
||||
template <typename T>
|
||||
void visit(std::string_view k, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view k, T &) { XLOGF(DBG3, "arithmetic visit({})", k); }
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k, T &) { XLOGF(DBG3, "enum visit({})", k); }
|
||||
|
||||
template <typename T>
|
||||
requires std::is_convertible_v<T, std::string_view>
|
||||
void visit(std::string_view k, T &) { XLOGF(DBG3, "string visit({})", k); }
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
visit<typename T::UnderlyingType>(k, v.toUnderType());
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMethod T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMethod visit({})", k);
|
||||
auto serialized = serde::SerdeMethod<T>::serdeToReadable(v);
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMethod T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "WithSerdeMethod visit({})", k);
|
||||
auto serialized = serde::SerdeMethod<T>::serdeTo(v);
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMemberMethod T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMemberMethod visit({})", k);
|
||||
auto serialized = v.serdeToReadable();
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMemberMethod T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "WithSerdeMemberMethod visit({})", k);
|
||||
auto serialized = v.serdeTo();
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
|
||||
}
|
||||
};
|
||||
|
||||
TEST(TestSerdeObjectVisitor, ChunkId) {
|
||||
storage::ChunkId chunkId;
|
||||
DebugObjectVisitor visitor;
|
||||
visitor.visit("chunkId", chunkId);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectVisitor, ChunkMeta) {
|
||||
storage::ChunkMeta chunkmeta;
|
||||
DebugObjectVisitor visitor;
|
||||
visitor.visit("chunkmeta", chunkmeta);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectVisitor, InodeId) {
|
||||
meta::InodeId inodeId;
|
||||
DebugObjectVisitor visitor;
|
||||
visitor.visit("inodeId", inodeId);
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectVisitor, Inode) {
|
||||
DebugObjectVisitor visitor;
|
||||
meta::Inode inode;
|
||||
visitor.visit("inode", inode);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
40
tests/analytics/TestSerdeObjectWriter.cc
Normal file
40
tests/analytics/TestSerdeObjectWriter.cc
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <arrow/io/file.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
TEST(TestSerdeObjectWriter, ChunkMeta) {
|
||||
auto writer = SerdeObjectWriter<storage::ChunkMeta>::open("TestSerdeObjectWriter.ChunkMeta.parquet");
|
||||
ASSERT_NE(writer, nullptr);
|
||||
storage::ChunkMeta chunkmeta;
|
||||
*writer << chunkmeta << parquet::EndRowGroup;
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectWriter, Inode) {
|
||||
auto writer = SerdeObjectWriter<meta::Inode>::open("TestSerdeObjectWriter.Inode.parquet");
|
||||
ASSERT_NE(writer, nullptr);
|
||||
meta::Inode inode;
|
||||
*writer << inode << parquet::EndRowGroup;
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectWriter, DirEntry) {
|
||||
auto writer = SerdeObjectWriter<meta::DirEntry>::open("TestSerdeObjectWriter.DirEntry.parquet");
|
||||
ASSERT_NE(writer, nullptr);
|
||||
meta::DirEntry entry;
|
||||
*writer << entry << parquet::EndRowGroup;
|
||||
}
|
||||
|
||||
TEST(TestSerdeObjectWriter, UpdateReq) {
|
||||
auto writer = SerdeObjectWriter<storage::UpdateReq>::open("TestSerdeObjectWriter.UpdateReq.parquet");
|
||||
ASSERT_NE(writer, nullptr);
|
||||
storage::UpdateReq updateReq;
|
||||
*writer << updateReq << parquet::EndRowGroup;
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
39
tests/analytics/TestSerdeSchemaBuilder.cc
Normal file
39
tests/analytics/TestSerdeSchemaBuilder.cc
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "analytics/SerdeSchemaBuilder.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
using namespace parquet;
|
||||
|
||||
TEST(TestSerdeSchemaBuilder, ChunkMeta) {
|
||||
SerdeSchemaBuilder<storage::ChunkMeta> builder;
|
||||
auto chunkmetaSchema = builder.getSchema();
|
||||
|
||||
std::ostringstream outstr;
|
||||
schema::PrintSchema(chunkmetaSchema.get(), outstr);
|
||||
XLOGF(INFO, "chunkmeta schema: {}", outstr.str());
|
||||
}
|
||||
|
||||
TEST(TestSerdeSchemaBuilder, Inode) {
|
||||
SerdeSchemaBuilder<meta::Inode> builder;
|
||||
auto inodeSchema = builder.getSchema();
|
||||
|
||||
std::ostringstream outstr;
|
||||
schema::PrintSchema(inodeSchema.get(), outstr);
|
||||
XLOGF(INFO, "inode schema: {}", outstr.str());
|
||||
}
|
||||
|
||||
TEST(TestSerdeSchemaBuilder, DirEntry) {
|
||||
SerdeSchemaBuilder<meta::DirEntry> builder;
|
||||
auto dirEntrySchema = builder.getSchema();
|
||||
|
||||
std::ostringstream outstr;
|
||||
schema::PrintSchema(dirEntrySchema.get(), outstr);
|
||||
XLOGF(INFO, "inode schema: {}", outstr.str());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
139
tests/analytics/TestSerdeStructVisitor.cc
Normal file
139
tests/analytics/TestSerdeStructVisitor.cc
Normal file
@@ -0,0 +1,139 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "analytics/SerdeStructVisitor.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
class DebugStructVisitor : public BaseStructVisitor<DebugStructVisitor> {
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view k) = delete;
|
||||
|
||||
template <>
|
||||
void visit<uint16_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint16_t visit({})", k);
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<uint32_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint32_t visit({})", k);
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<uint64_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint64_t visit({})", k);
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int16_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int16_t visit({})", k);
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int32_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int32_t visit({})", k);
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int64_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int64_t visit({})", k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k) { XLOGF(DBG3, "enum visit({})", k); }
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "string visit({})", k);
|
||||
}
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMethod visit({})", k);
|
||||
visit<serde::SerdeToReadableReturnType<T>>(k);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "WithSerdeMethod visit({})", k);
|
||||
visit<serde::SerdeToReturnType<T>>(k);
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMemberMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMemberMethod visit({})", k);
|
||||
visit<serde::SerdeToReadableMemberMethodReturnType<T>>(k);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMemberMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "WithSerdeMemberMethod visit({})", k);
|
||||
visit<serde::SerdeToMemberMethodReturnType<T>>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
|
||||
}
|
||||
};
|
||||
|
||||
TEST(TestSerdeStructVisitor, ChunkId) {
|
||||
DebugStructVisitor visitor;
|
||||
visitor.visit<storage::ChunkId>("chunkId");
|
||||
}
|
||||
|
||||
TEST(TestSerdeStructVisitor, ChunkMeta) {
|
||||
DebugStructVisitor visitor;
|
||||
visitor.visit<storage::ChunkMeta>("chunkmeta");
|
||||
}
|
||||
|
||||
TEST(TestSerdeStructVisitor, InodeId) {
|
||||
DebugStructVisitor visitor;
|
||||
visitor.visit<meta::InodeId>("inodeId");
|
||||
}
|
||||
|
||||
TEST(TestSerdeStructVisitor, Inode) {
|
||||
DebugStructVisitor visitor;
|
||||
visitor.visit<meta::Inode>("inode");
|
||||
}
|
||||
|
||||
TEST(TestSerdeStructVisitor, DirEntry) {
|
||||
DebugStructVisitor visitor;
|
||||
visitor.visit<meta::DirEntry>("dirEntry");
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
134
tests/analytics/TestStructuredTraceLog.cc
Normal file
134
tests/analytics/TestStructuredTraceLog.cc
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
|
||||
#include "analytics/StructuredTraceLog.h"
|
||||
#include "common/monitor/ScopedMetricsWriter.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "meta/event/Event.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
TEST(TestStructuredTraceLog, Open) {
|
||||
StructuredTraceLog<storage::StorageEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
}
|
||||
|
||||
TEST(TestStructuredTraceLog, Close) {
|
||||
StructuredTraceLog<storage::StorageEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
traceLog.close();
|
||||
}
|
||||
|
||||
TEST(TestStructuredTraceLog, MetaEventTrace) {
|
||||
StructuredTraceLog<meta::server::MetaEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
config.set_dump_interval(100_ms);
|
||||
StructuredTraceLog<meta::server::MetaEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
|
||||
for (size_t loop = 0; loop < 100'000; loop++) {
|
||||
traceLog.append(meta::server::MetaEventTrace{
|
||||
.inodeId = meta::InodeId{loop},
|
||||
.entryName = std::to_string(loop),
|
||||
.client = ClientId::zero(),
|
||||
});
|
||||
}
|
||||
|
||||
traceLog.close();
|
||||
}
|
||||
|
||||
auto createStorageEventTrace(size_t id) {
|
||||
return storage::StorageEventTrace{
|
||||
.updateReq =
|
||||
storage::UpdateReq{
|
||||
.tag =
|
||||
storage::MessageTag{
|
||||
ClientId::zero(),
|
||||
storage::RequestId{id},
|
||||
storage::UpdateChannel{
|
||||
.id = storage::ChannelId{id},
|
||||
.seqnum = storage::ChannelSeqNum{id},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
TEST(TestStructuredTraceLog, StorageEventTrace) {
|
||||
StructuredTraceLog<storage::StorageEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
config.set_dump_interval(100_ms);
|
||||
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
|
||||
for (size_t loop = 0; loop < 100'000; loop++) {
|
||||
traceLog.append(createStorageEventTrace(loop));
|
||||
}
|
||||
|
||||
traceLog.close();
|
||||
}
|
||||
|
||||
TEST(TestStructuredTraceLog, MultiThreadsAppend) {
|
||||
auto concurrency = std::min(std::thread::hardware_concurrency(), 64U);
|
||||
StructuredTraceLog<storage::StorageEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
config.set_max_num_writers(concurrency / 2);
|
||||
config.set_dump_interval(1_s);
|
||||
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
|
||||
std::vector<std::future<void>> tasks;
|
||||
|
||||
for (size_t i = 0; i < concurrency; i++) {
|
||||
tasks.push_back(std::async(std::launch::async, [&traceLog]() {
|
||||
for (size_t loop = 0; loop < 100'000; loop++) {
|
||||
traceLog.append(createStorageEventTrace(loop));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for (auto &t : tasks) t.wait();
|
||||
traceLog.close();
|
||||
}
|
||||
|
||||
TEST(TestStructuredTraceLog, MultiThreadsNewEntry) {
|
||||
auto concurrency = std::min(std::thread::hardware_concurrency(), 64U);
|
||||
StructuredTraceLog<storage::StorageEventTrace>::Config config;
|
||||
config.set_enabled(true);
|
||||
config.set_max_num_writers(concurrency);
|
||||
config.set_dump_interval(1_s);
|
||||
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
|
||||
ASSERT_TRUE(traceLog.open());
|
||||
|
||||
std::vector<std::future<void>> tasks;
|
||||
monitor::LatencyRecorder latRecorder("TestStructuredTraceLog");
|
||||
|
||||
for (size_t i = 0; i < concurrency; i++) {
|
||||
tasks.push_back(std::async(std::launch::async, [&traceLog, &latRecorder]() {
|
||||
for (size_t loop = 0; loop < 100; loop++) {
|
||||
auto entry = traceLog.newEntry(createStorageEventTrace(loop));
|
||||
entry.reset();
|
||||
}
|
||||
for (size_t loop = 0; loop < 100'000; loop++) {
|
||||
monitor::ScopedLatencyWriter latWriter(latRecorder);
|
||||
auto entry = traceLog.newEntry(createStorageEventTrace(loop));
|
||||
entry.reset();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for (auto &t : tasks) t.wait();
|
||||
traceLog.close();
|
||||
|
||||
std::vector<monitor::Sample> samples;
|
||||
latRecorder.collect(samples);
|
||||
for (const auto &s : samples) {
|
||||
XLOGF(WARN, "latency: {}", s.dist());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
2
tests/client/CMakeLists.txt
Normal file
2
tests/client/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
target_add_test(test_client hf3fs_api storage-client meta-client meta mgmtd)
|
||||
|
||||
37
tests/client/ClientWithConfig.h
Normal file
37
tests/client/ClientWithConfig.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "client/mgmtd/MgmtdClientForServer.h"
|
||||
#include "stubs/common/RealStubFactory.h"
|
||||
#include "stubs/mgmtd/MgmtdServiceStub.h"
|
||||
|
||||
namespace hf3fs::client {
|
||||
|
||||
struct MgmtdClientWithConfig {
|
||||
MgmtdClient::Config config;
|
||||
String clusterId;
|
||||
stubs::ClientContextCreator clientContextCreator;
|
||||
std::shared_ptr<MgmtdClient> rawClient;
|
||||
std::unique_ptr<MgmtdClientForServer> client;
|
||||
|
||||
MgmtdClientWithConfig(String clusterId,
|
||||
stubs::ClientContextCreator clientContextCreator,
|
||||
std::vector<net::Address> mgmtdServerAddrs) {
|
||||
config.set_mgmtd_server_addresses(mgmtdServerAddrs);
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
config.set_enable_auto_extend_client_session(false);
|
||||
|
||||
this->clusterId = std::move(clusterId);
|
||||
this->clientContextCreator = std::move(clientContextCreator);
|
||||
|
||||
auto stubFactory = std::make_unique<stubs::RealStubFactory<mgmtd::MgmtdServiceStub>>(this->clientContextCreator);
|
||||
rawClient = std::make_unique<MgmtdClient>(this->clusterId, std::move(stubFactory), config);
|
||||
client = std::make_unique<MgmtdClientForServer>(rawClient);
|
||||
}
|
||||
|
||||
MgmtdClientForServer *operator->() const { return client.get(); }
|
||||
|
||||
MgmtdClientForServer &operator*() const { return *client; }
|
||||
};
|
||||
|
||||
} // namespace hf3fs::client
|
||||
88
tests/client/ServerWithConfig.h
Normal file
88
tests/client/ServerWithConfig.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "client/mgmtd/MgmtdClientForServer.h"
|
||||
#include "common/kv/mem/MemKVEngine.h"
|
||||
#include "meta/service/MetaServer.h"
|
||||
#include "mgmtd/MgmtdServer.h"
|
||||
|
||||
namespace hf3fs {
|
||||
|
||||
template <typename ServerT>
|
||||
struct ServerWithConfig {
|
||||
typename ServerT::Config config;
|
||||
String clusterId;
|
||||
flat::NodeId nodeId;
|
||||
std::unique_ptr<ServerT> server;
|
||||
String hostname;
|
||||
uint32_t pid = 1;
|
||||
|
||||
ServerWithConfig(String clusterId, flat::NodeId nodeId) {
|
||||
this->clusterId = std::move(clusterId);
|
||||
this->nodeId = nodeId;
|
||||
|
||||
auto &baseConfig = config.base();
|
||||
for (size_t i = 0; i < baseConfig.groups_length(); ++i) {
|
||||
baseConfig.groups(i).set_network_type(net::Address::LOCAL);
|
||||
baseConfig.groups(i).listener().set_listen_port(0);
|
||||
baseConfig.groups(i).listener().set_reuse_port(true);
|
||||
}
|
||||
}
|
||||
|
||||
Result<Void> start(std::shared_ptr<kv::IKVEngine> kvEngine) {
|
||||
assert(!server);
|
||||
auto server = std::make_unique<ServerT>(config);
|
||||
RETURN_ON_ERROR(server->setup());
|
||||
|
||||
flat::AppInfo appInfo;
|
||||
appInfo.nodeId = nodeId;
|
||||
appInfo.clusterId = clusterId;
|
||||
appInfo.hostname = fmt::format("hostname.{}", nodeId.toUnderType());
|
||||
appInfo.pid = pid++;
|
||||
|
||||
std::vector<net::Address> serverAddrs;
|
||||
for (auto &group : server->groups()) {
|
||||
appInfo.serviceGroups.emplace_back(group->serviceNameList(), group->addressList());
|
||||
serverAddrs.insert(serverAddrs.end(), group->addressList().begin(), group->addressList().end());
|
||||
}
|
||||
|
||||
RETURN_ON_ERROR(server->start(appInfo, std::move(kvEngine)));
|
||||
this->server = std::move(server);
|
||||
return Void{};
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (server) server->stopAndJoin();
|
||||
server.reset();
|
||||
}
|
||||
|
||||
Result<Void> restart(std::shared_ptr<kv::IKVEngine> kvEngine) {
|
||||
stop();
|
||||
return start(std::move(kvEngine));
|
||||
}
|
||||
|
||||
std::vector<net::Address> collectAddressList(std::string_view serviceName) const {
|
||||
std::vector<net::Address> serverAddrs;
|
||||
for (const auto &group : server->groups()) {
|
||||
const auto &names = group->serviceNameList();
|
||||
const auto &addrs = group->addressList();
|
||||
if (std::find(names.begin(), names.end(), serviceName) != names.end()) {
|
||||
serverAddrs.insert(serverAddrs.end(), addrs.begin(), addrs.end());
|
||||
}
|
||||
}
|
||||
return serverAddrs;
|
||||
}
|
||||
};
|
||||
|
||||
using MgmtdServerWithConfig = ServerWithConfig<mgmtd::MgmtdServer>;
|
||||
|
||||
struct MetaServerWithConfig : ServerWithConfig<meta::server::MetaServer> {
|
||||
using Base = ServerWithConfig<meta::server::MetaServer>;
|
||||
|
||||
MetaServerWithConfig(String clusterId, flat::NodeId nodeId, std::vector<net::Address> mgmtdServerAddrs)
|
||||
: Base(std::move(clusterId), nodeId) {
|
||||
auto &mgmtdClientConfig = config.mgmtd_client();
|
||||
mgmtdClientConfig.set_mgmtd_server_addresses(mgmtdServerAddrs);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hf3fs
|
||||
1741
tests/client/TestMetaClient.cc
Normal file
1741
tests/client/TestMetaClient.cc
Normal file
File diff suppressed because it is too large
Load Diff
615
tests/client/TestMgmtdClient.cc
Normal file
615
tests/client/TestMgmtdClient.cc
Normal file
@@ -0,0 +1,615 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
|
||||
#include "client/mgmtd/MgmtdClient.h"
|
||||
#include "fbs/mgmtd/NodeConversion.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/stubs/DummyMgmtdServiceStub.h"
|
||||
|
||||
namespace hf3fs::client::tests {
|
||||
namespace {
|
||||
class MgmtdClientTest : public ::testing::Test {
|
||||
protected:
|
||||
MgmtdClientTest() {}
|
||||
};
|
||||
|
||||
#define CO_START_CLIENT(client) \
|
||||
co_await (client).start(); \
|
||||
co_await folly::coro::co_scope_exit([&]() -> CoTask<void> { co_await (client).stop(); })
|
||||
|
||||
std::vector<std::pair<String, String>> convert(const std::vector<std::pair<String, net::Address>> &v) {
|
||||
std::vector<std::pair<String, String>> ret;
|
||||
for (const auto &[method, addr] : v) {
|
||||
ret.emplace_back(method, addr.toString());
|
||||
}
|
||||
std::sort(ret.begin(), ret.end());
|
||||
return ret;
|
||||
}
|
||||
|
||||
#define CO_ASSERT_RECORDS_EQ(expectedRecords) \
|
||||
do { \
|
||||
auto _expected = convert(expectedRecords); \
|
||||
auto _actual = convert(Machine::visitRecords); \
|
||||
CO_ASSERT_EQ(_actual, _expected); \
|
||||
} while (false)
|
||||
|
||||
flat::PersistentNodeInfo makePrimary(flat::NodeId id, net::Address addr) {
|
||||
flat::PersistentNodeInfo info;
|
||||
info.nodeId = id;
|
||||
flat::ServiceGroupInfo sgi;
|
||||
sgi.services.insert("Mgmtd");
|
||||
sgi.endpoints.push_back(addr);
|
||||
info.serviceGroups.push_back(sgi);
|
||||
return info;
|
||||
}
|
||||
|
||||
struct Machine {
|
||||
Machine(flat::NodeId id, std::vector<net::Address> addrs) {
|
||||
selfInfo = makePrimary(id, addrs[0]);
|
||||
for (size_t i = 1; i < addrs.size(); ++i) {
|
||||
selfInfo.serviceGroups[0].endpoints.push_back(addrs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<mgmtd::DummyMgmtdServiceStub> createStub(net::Address addr) {
|
||||
const auto &endpoints = selfInfo.serviceGroups[0].endpoints;
|
||||
auto it = std::find(endpoints.begin(), endpoints.end(), addr);
|
||||
if (it == endpoints.end()) return nullptr;
|
||||
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
stub->set_getPrimaryMgmtdFunc([this, addr](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
|
||||
visitRecords.emplace_back("GetPrimaryMgmtd", addr);
|
||||
if (primary.hasError())
|
||||
return makeError(primary.error());
|
||||
else if (*primary)
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(**primary);
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(std::nullopt);
|
||||
})
|
||||
.set_getRoutingInfoFunc([this, addr](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
|
||||
visitRecords.emplace_back("GetRoutingInfo", addr);
|
||||
if (primary.hasError()) return makeError(primary.error());
|
||||
if (!*primary) return makeError(MgmtdCode::kNotPrimary);
|
||||
if (**primary != selfInfo) return makeError(MgmtdCode::kNotPrimary, fmt::format("{}", (*primary)->nodeId));
|
||||
if (routingInfo.hasError()) return makeError(routingInfo.error());
|
||||
if (*routingInfo) {
|
||||
return mgmtd::GetRoutingInfoRsp::create(**routingInfo);
|
||||
}
|
||||
return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
|
||||
});
|
||||
return stub;
|
||||
}
|
||||
|
||||
flat::PersistentNodeInfo selfInfo;
|
||||
Result<flat::PersistentNodeInfo *> primary = makeError(RPCCode::kConnectFailed);
|
||||
Result<flat::RoutingInfo *> routingInfo = makeError(RPCCode::kConnectFailed);
|
||||
static std::vector<std::pair<String, net::Address>> visitRecords;
|
||||
};
|
||||
|
||||
std::vector<std::pair<String, net::Address>> Machine::visitRecords;
|
||||
|
||||
TEST_F(MgmtdClientTest, testStartStopWithWrongConfig) {
|
||||
MgmtdClient::Config config;
|
||||
config.set_auto_refresh_interval(1_ms);
|
||||
config.set_auto_heartbeat_interval(1_ms);
|
||||
MgmtdClient client("", nullptr, config);
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(100));
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRepetitiveStartStop) {
|
||||
MgmtdClient::Config config;
|
||||
config.set_auto_refresh_interval(1_ms);
|
||||
config.set_auto_heartbeat_interval(1_ms);
|
||||
MgmtdClient client("", nullptr, config);
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(10));
|
||||
co_await client.stop();
|
||||
|
||||
co_await client.start();
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(10));
|
||||
co_await client.stop();
|
||||
|
||||
co_await client.start();
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(10));
|
||||
co_await client.stop();
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testGetRoutingInfoBeforeStart) {
|
||||
MgmtdClient::Config config;
|
||||
MgmtdClient client("", nullptr, config);
|
||||
auto info = client.getRoutingInfo();
|
||||
ASSERT_TRUE(!info);
|
||||
}
|
||||
|
||||
net::Address ada = net::Address::from("127.0.0.1:8080").value();
|
||||
net::Address adb = net::Address::from("192.168.0.1:9000").value();
|
||||
net::Address adc = net::Address::from("192.168.0.2:8000").value();
|
||||
net::Address add = net::Address::from("192.168.0.3:8800").value();
|
||||
net::Address ade; // invalid addr
|
||||
net::Address adf = net::Address::from("RDMA://127.0.0.1:8080").value();
|
||||
net::Address adg = net::Address::from("RDMA://192.168.0.1:9000").value();
|
||||
net::Address adh = net::Address::from("RDMA://192.168.0.2:8000").value();
|
||||
net::Address adi = net::Address::from("RDMA://192.168.0.3:8800").value();
|
||||
net::Address adj = net::Address::from("192.168.0.4:8888").value();
|
||||
|
||||
TEST_F(MgmtdClientTest, testWhenNoPrimary) {
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
|
||||
return std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({ada, adb, adc});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
|
||||
CO_ASSERT_TRUE(!client.getRoutingInfo());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testInvalidAddress) {
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({ada, ade});
|
||||
MgmtdClient client("", nullptr, config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testAddressTypeMismatch) {
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({ada, adf});
|
||||
config.set_network_type(net::Address::TCP);
|
||||
MgmtdClient client("", nullptr, config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testWhenLastIsPrimary) {
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
if (addr == adc) {
|
||||
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), adc));
|
||||
})
|
||||
.set_getRoutingInfoFunc(
|
||||
[](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
|
||||
}
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({ada, adb, adc});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
auto ri = client.getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri);
|
||||
CO_ASSERT_TRUE(!ri->raw());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testWhenPrimaryNotInConfig) {
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
if (addr == adc) {
|
||||
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
|
||||
});
|
||||
} else if (addr == add) {
|
||||
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
|
||||
})
|
||||
.set_getRoutingInfoFunc(
|
||||
[](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
|
||||
}
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({ada, adb, adc});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
auto ri = client.getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri);
|
||||
CO_ASSERT_TRUE(!ri->raw());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testWhenGetPrimaryLoop) {
|
||||
std::vector<net::Address> addresses = {ada, adb, adc};
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::vector<net::Address> addresses;
|
||||
|
||||
explicit StubFactory(std::vector<net::Address> v)
|
||||
: addresses(std::move(v)) {}
|
||||
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||
if (addr == addresses[i]) {
|
||||
if (i + 1 == addresses.size()) {
|
||||
stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), addresses[0]));
|
||||
});
|
||||
} else {
|
||||
stub->set_getPrimaryMgmtdFunc([this, i](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(i + 2), addresses[i + 1]));
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses(addresses);
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(addresses), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
|
||||
CO_ASSERT_TRUE(!client.getRoutingInfo());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRetryOnRefreshFail) {
|
||||
std::vector<net::Address> addresses = {ada, adb, adc};
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
int counta = 0;
|
||||
int countb = 0;
|
||||
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
if (addr == ada) {
|
||||
stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
|
||||
if (++counta == 1) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
|
||||
} else {
|
||||
return makeError(RPCCode::kConnectFailed);
|
||||
}
|
||||
})
|
||||
.set_getRoutingInfoFunc([this](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
|
||||
if (++countb == 1)
|
||||
return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
|
||||
else
|
||||
return makeError(RPCCode::kConnectFailed);
|
||||
});
|
||||
} else if (addr == adb) {
|
||||
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(2), adb));
|
||||
})
|
||||
.set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &) {
|
||||
flat::RoutingInfo ri;
|
||||
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
|
||||
return mgmtd::GetRoutingInfoRsp::create(ri);
|
||||
});
|
||||
}
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses(addresses);
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
auto ri = client.getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri && !ri->raw());
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
ri = client.getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri && ri->raw());
|
||||
CO_ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(2));
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRefreshRoutingInfoCallback) {
|
||||
std::vector<net::Address> addresses = {ada};
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
|
||||
})
|
||||
.set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &req) {
|
||||
flat::RoutingInfo ri;
|
||||
ri.routingInfoVersion = flat::RoutingInfoVersion(req.routingInfoVersion + 1);
|
||||
return mgmtd::GetRoutingInfoRsp::create(ri);
|
||||
});
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses(addresses);
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
std::vector<std::shared_ptr<RoutingInfo>> ris;
|
||||
client.addRoutingInfoListener("test", [&](std::shared_ptr<RoutingInfo> ri) { ris.push_back(std::move(ri)); });
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
for (int i = 0; i < 10; ++i) CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
}());
|
||||
|
||||
ASSERT_EQ(ris.size(), 10);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
const auto &ri = ris[i];
|
||||
ASSERT_TRUE(ri && ri->raw());
|
||||
ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(i + 1));
|
||||
if (i > 0) ASSERT_TRUE(ri->lastRefreshTime() > ris[i - 1]->lastRefreshTime());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRetryAllAvailableAddresses) {
|
||||
Machine::visitRecords.clear();
|
||||
Machine ma(flat::NodeId(1), {ada, adb, adc});
|
||||
Machine mb(flat::NodeId(2), {add, adf});
|
||||
Machine mc(flat::NodeId(3), {adj});
|
||||
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::vector<Machine *> machines;
|
||||
|
||||
explicit StubFactory(std::vector<Machine *> v)
|
||||
: machines(std::move(v)) {}
|
||||
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
for (auto *m : machines) {
|
||||
auto stub = m->createStub(addr);
|
||||
if (stub) return stub;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({adj});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
config.set_network_type(net::Address::TCP);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
|
||||
|
||||
flat::RoutingInfo ri;
|
||||
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
|
||||
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
|
||||
ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
|
||||
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
mc.primary = &mc.selfInfo;
|
||||
mc.routingInfo = &ri;
|
||||
CO_START_CLIENT(client);
|
||||
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
|
||||
mc.primary = makeError(RPCCode::kConnectFailed);
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {
|
||||
{"GetRoutingInfo", adj},
|
||||
{"GetPrimaryMgmtd", ada},
|
||||
{"GetPrimaryMgmtd", adb},
|
||||
{"GetPrimaryMgmtd", adc},
|
||||
{"GetPrimaryMgmtd", add},
|
||||
};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRetryEndWhenNoPrimary) {
|
||||
Machine::visitRecords.clear();
|
||||
Machine ma(flat::NodeId(1), {ada, adb, adc});
|
||||
Machine mb(flat::NodeId(2), {add, adf});
|
||||
Machine mc(flat::NodeId(3), {adj});
|
||||
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::vector<Machine *> machines;
|
||||
|
||||
explicit StubFactory(std::vector<Machine *> v)
|
||||
: machines(std::move(v)) {}
|
||||
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
for (auto *m : machines) {
|
||||
auto stub = m->createStub(addr);
|
||||
if (stub) return stub;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({adj});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
config.set_network_type(net::Address::TCP);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
|
||||
|
||||
flat::RoutingInfo ri;
|
||||
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
|
||||
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
|
||||
ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
|
||||
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
mc.primary = &mc.selfInfo;
|
||||
mc.routingInfo = &ri;
|
||||
CO_START_CLIENT(client);
|
||||
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
|
||||
ma.primary = nullptr;
|
||||
mc.primary = makeError(RPCCode::kConnectFailed);
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {
|
||||
{"GetRoutingInfo", adj},
|
||||
{"GetPrimaryMgmtd", ada},
|
||||
};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testRetryUnknownAddrs) {
|
||||
Machine::visitRecords.clear();
|
||||
Machine ma(flat::NodeId(1), {ada, adb, adc});
|
||||
Machine mb(flat::NodeId(2), {add, adf});
|
||||
Machine mc(flat::NodeId(3), {adj});
|
||||
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::vector<Machine *> machines;
|
||||
|
||||
explicit StubFactory(std::vector<Machine *> v)
|
||||
: machines(std::move(v)) {}
|
||||
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
|
||||
for (auto *m : machines) {
|
||||
auto stub = m->createStub(addr);
|
||||
if (stub) return stub;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses({adj, add});
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
config.set_network_type(net::Address::TCP);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
|
||||
|
||||
flat::RoutingInfo ri;
|
||||
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
|
||||
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
|
||||
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
mc.primary = &mc.selfInfo;
|
||||
mc.routingInfo = &ri;
|
||||
CO_START_CLIENT(client);
|
||||
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
|
||||
mc.primary = makeError(RPCCode::kConnectFailed);
|
||||
Machine::visitRecords.clear();
|
||||
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
|
||||
{
|
||||
std::vector<std::pair<String, net::Address>> expected = {
|
||||
{"GetRoutingInfo", adj},
|
||||
{"GetPrimaryMgmtd", ada},
|
||||
{"GetPrimaryMgmtd", adb},
|
||||
{"GetPrimaryMgmtd", adc},
|
||||
{"GetPrimaryMgmtd", add},
|
||||
};
|
||||
CO_ASSERT_RECORDS_EQ(expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(MgmtdClientTest, testSetGetConfigViaInvoke) {
|
||||
std::vector<net::Address> addresses = {ada};
|
||||
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
|
||||
std::map<flat::NodeType, flat::ConfigInfo> configs;
|
||||
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
|
||||
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
|
||||
stub->set_setConfigFunc([this](const mgmtd::SetConfigReq &req) -> Result<mgmtd::SetConfigRsp> {
|
||||
auto &info = configs[req.nodeType];
|
||||
auto cv = flat::ConfigVersion(info.configVersion + 1);
|
||||
info = flat::ConfigInfo::create(cv, req.content, req.desc);
|
||||
return mgmtd::SetConfigRsp::create(cv);
|
||||
})
|
||||
.set_getConfigFunc([this](const mgmtd::GetConfigReq &req) -> Result<mgmtd::GetConfigRsp> {
|
||||
if (configs.contains(req.nodeType)) return mgmtd::GetConfigRsp::create(configs[req.nodeType]);
|
||||
return mgmtd::GetConfigRsp::create(std::nullopt);
|
||||
})
|
||||
.set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
|
||||
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
|
||||
});
|
||||
return stub;
|
||||
}
|
||||
};
|
||||
|
||||
MgmtdClient::Config config;
|
||||
config.set_mgmtd_server_addresses(addresses);
|
||||
config.set_enable_auto_refresh(false);
|
||||
config.set_enable_auto_heartbeat(false);
|
||||
MgmtdClient client("", std::make_unique<StubFactory>(), config);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
CO_START_CLIENT(client);
|
||||
{
|
||||
auto res = co_await client.setConfig(flat::UserInfo{}, flat::NodeType::MGMTD, String("abcd"), "desc");
|
||||
CO_ASSERT_OK(res);
|
||||
}
|
||||
{
|
||||
auto res = co_await client.getConfig(flat::NodeType::MGMTD, flat::ConfigVersion{0});
|
||||
CO_ASSERT_OK(res);
|
||||
CO_ASSERT_TRUE(res->has_value());
|
||||
CO_ASSERT_EQ(res->value().configVersion, flat::ConfigVersion{1});
|
||||
CO_ASSERT_EQ(res->value().content, "abcd");
|
||||
CO_ASSERT_EQ(res->value().desc, "desc");
|
||||
|
||||
res = co_await client.getConfig(flat::NodeType::META, flat::ConfigVersion{0});
|
||||
CO_ASSERT_OK(res);
|
||||
CO_ASSERT_TRUE(!res->has_value());
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::client::tests
|
||||
99
tests/client/TestMgmtdCluster.cc
Normal file
99
tests/client/TestMgmtdCluster.cc
Normal file
@@ -0,0 +1,99 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
|
||||
#include "common/kv/mem/MemKVEngine.h"
|
||||
#include "meta/service/MetaServer.h"
|
||||
#include "mgmtd/MgmtdServer.h"
|
||||
#include "stubs/common/RealStubFactory.h"
|
||||
#include "stubs/mgmtd/MgmtdServiceStub.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/client/ClientWithConfig.h"
|
||||
#include "tests/client/ServerWithConfig.h"
|
||||
|
||||
namespace hf3fs::client::tests {
|
||||
namespace {
|
||||
class MgmtdClusterTest : public ::testing::Test {
|
||||
protected:
|
||||
MgmtdClusterTest() = default;
|
||||
};
|
||||
|
||||
#define CO_START_CLIENT(client) \
|
||||
co_await (client).start(); \
|
||||
co_await folly::coro::co_scope_exit([&]() -> CoTask<void> { co_await (client).stop(); })
|
||||
|
||||
struct ClientWithConfig {
|
||||
net::Client::Config config;
|
||||
std::unique_ptr<net::Client> client;
|
||||
|
||||
Result<Void> init() {
|
||||
client = std::make_unique<net::Client>(config);
|
||||
return client->start("test");
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MgmtdClusterTest, testMgmtdServerRestart) {
|
||||
const String clusterId = "cluster";
|
||||
auto kvEngine = std::make_shared<kv::MemKVEngine>();
|
||||
|
||||
ClientWithConfig client;
|
||||
ASSERT_OK(client.init());
|
||||
|
||||
MgmtdServerWithConfig mgmtdServer(clusterId, flat::NodeId(1));
|
||||
ASSERT_OK(mgmtdServer.start(kvEngine));
|
||||
|
||||
MgmtdClientWithConfig mgmtdClient(
|
||||
clusterId,
|
||||
[&client](net::Address addr) { return client.client->serdeCtx(addr); },
|
||||
mgmtdServer.collectAddressList("Mgmtd"));
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(500));
|
||||
|
||||
CO_START_CLIENT(*mgmtdClient.client);
|
||||
// set chain table
|
||||
flat::ChainTableId tableId{1};
|
||||
std::vector<flat::ChainSetting> chains;
|
||||
std::vector<flat::ChainId> chainIds;
|
||||
{
|
||||
for (int i = 0, t = 0; i < 10; ++i) {
|
||||
flat::ChainSetting chain;
|
||||
chain.chainId = flat::ChainId((tableId << 16) + i + 1);
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
flat::ChainTargetSetting target;
|
||||
target.targetId = flat::TargetId(++t);
|
||||
chain.targets.push_back(target);
|
||||
}
|
||||
chainIds.push_back(chain.chainId);
|
||||
chains.push_back(std::move(chain));
|
||||
}
|
||||
|
||||
CO_ASSERT_OK(co_await mgmtdClient.rawClient->setChains(flat::UserInfo{}, chains));
|
||||
CO_ASSERT_OK(co_await mgmtdClient.rawClient->setChainTable(flat::UserInfo{}, tableId, chainIds, ""));
|
||||
CO_AWAIT_ASSERT_OK(mgmtdClient->refreshRoutingInfo(false));
|
||||
|
||||
auto ri = mgmtdClient->getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri && ri->raw());
|
||||
|
||||
for (const auto &c : chains) {
|
||||
auto ci = ri->getChain(c.chainId);
|
||||
CO_ASSERT_TRUE(ci);
|
||||
CO_ASSERT_EQ(ci->chainId, c.chainId);
|
||||
}
|
||||
}
|
||||
// restart mgmtd, expect chain table exists
|
||||
{
|
||||
CO_ASSERT_OK(mgmtdServer.restart(kvEngine));
|
||||
auto ri = mgmtdClient->getRoutingInfo();
|
||||
CO_ASSERT_TRUE(ri && ri->raw());
|
||||
|
||||
for (const auto &c : chains) {
|
||||
auto ci = ri->getChain(c.chainId);
|
||||
CO_ASSERT_TRUE(ci);
|
||||
CO_ASSERT_EQ(ci->chainId, c.chainId);
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::client::tests
|
||||
1
tests/common/CMakeLists.txt
Normal file
1
tests/common/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
target_add_test(test_common common fdb mgmtd-fbs)
|
||||
274
tests/common/TestWithTransaction.cc
Normal file
274
tests/common/TestWithTransaction.cc
Normal file
@@ -0,0 +1,274 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <string_view>
|
||||
|
||||
#include "common/kv/IKVEngine.h"
|
||||
#include "common/kv/WithTransaction.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/DefaultRetryStrategy.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
using namespace ::hf3fs::kv;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
struct MockSleeper {
|
||||
CoTask<void> operator()(std::chrono::milliseconds d) {
|
||||
records.push_back(d);
|
||||
co_return;
|
||||
}
|
||||
|
||||
std::vector<std::chrono::milliseconds> records;
|
||||
};
|
||||
|
||||
DefaultRetryStrategy<MockSleeper> getDefaultRetryStrategy() {
|
||||
return DefaultRetryStrategy<MockSleeper>(RetryConfig{10ms, 100ms, 5});
|
||||
}
|
||||
|
||||
struct OpResultSeq : public std::vector<uint32_t> {
|
||||
using Base = std::vector<uint32_t>;
|
||||
using Base::Base;
|
||||
|
||||
size_t cur_ = 0;
|
||||
|
||||
explicit OpResultSeq(Base req)
|
||||
: Base(std::move(req)) {}
|
||||
|
||||
CoTryTask<void> next() {
|
||||
auto code = at(cur_++);
|
||||
if (code == StatusCode::kOK) {
|
||||
co_return Void();
|
||||
}
|
||||
co_return makeError(code);
|
||||
}
|
||||
|
||||
std::string_view peak() { return StatusCode::toString(at(cur_)); }
|
||||
};
|
||||
|
||||
class MockROTxn : public IReadOnlyTransaction {
|
||||
public:
|
||||
CoTryTask<std::optional<String>> snapshotGet(std::string_view) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<GetRangeResult> snapshotGetRange(const KeySelector &, const KeySelector &, int32_t) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<GetRangeResult> getRange(const KeySelector &, const KeySelector &, int32_t) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<void> cancel() override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
void reset() override {}
|
||||
|
||||
void setReadVersion(int64_t) override {}
|
||||
};
|
||||
|
||||
class MockRWTxn : public IReadWriteTransaction {
|
||||
public:
|
||||
MockRWTxn(OpResultSeq &commitSeq)
|
||||
: commitSeq_(commitSeq) {}
|
||||
|
||||
CoTryTask<std::optional<String>> snapshotGet(std::string_view) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<GetRangeResult> snapshotGetRange(const KeySelector &, const KeySelector &, int32_t) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<std::optional<String>> get(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
|
||||
CoTryTask<GetRangeResult> getRange(const KeySelector &, const KeySelector &, int32_t) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<void> addReadConflict(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
CoTryTask<void> addReadConflictRange(std::string_view, std::string_view) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<void> set(std::string_view, std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
|
||||
CoTryTask<void> clear(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
|
||||
CoTryTask<void> setVersionstampedKey(std::string_view key, uint32_t offset, std::string_view value) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<void> setVersionstampedValue(std::string_view key, std::string_view value, uint32_t offset) override {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<void> commit() override { co_return co_await commitSeq_.next(); }
|
||||
|
||||
CoTryTask<void> cancel() override { co_return makeError(StatusCode::kNotImplemented); }
|
||||
|
||||
int64_t getCommittedVersion() override { return -1; }
|
||||
|
||||
void setReadVersion(int64_t) override {}
|
||||
|
||||
void reset() override {}
|
||||
|
||||
private:
|
||||
OpResultSeq &commitSeq_;
|
||||
};
|
||||
|
||||
struct MockHandler {
|
||||
explicit MockHandler(OpResultSeq &seq)
|
||||
: seq_(seq) {}
|
||||
|
||||
CoTryTask<int> operator()(IReadOnlyTransaction &) {
|
||||
auto result = co_await seq_.next();
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
co_return 10;
|
||||
}
|
||||
|
||||
CoTryTask<void> operator()(IReadWriteTransaction &) {
|
||||
auto result = co_await seq_.next();
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
co_return Void();
|
||||
}
|
||||
|
||||
OpResultSeq &seq_;
|
||||
};
|
||||
|
||||
struct RetryStrategyRef {
|
||||
DefaultRetryStrategy<MockSleeper> &ref;
|
||||
|
||||
template <typename Txn>
|
||||
Result<Void> init(Txn *txn) {
|
||||
return ref.init(txn);
|
||||
}
|
||||
|
||||
template <typename Txn>
|
||||
CoTryTask<void> onError(Txn *txn, Status error) {
|
||||
co_return co_await ref.onError(txn, error);
|
||||
}
|
||||
};
|
||||
|
||||
CoTask<std::pair<Result<int>, DefaultRetryStrategy<MockSleeper>>> handleROTxn(OpResultSeq &opSeq) {
|
||||
auto handler = MockHandler(opSeq);
|
||||
auto retryStrategy = getDefaultRetryStrategy();
|
||||
MockROTxn txn;
|
||||
auto result = co_await WithTransaction<RetryStrategyRef>({retryStrategy}).run(txn, handler);
|
||||
co_return std::make_pair<Result<int>, DefaultRetryStrategy<MockSleeper>>(std::move(result), std::move(retryStrategy));
|
||||
}
|
||||
|
||||
CoTask<std::pair<Result<Void>, DefaultRetryStrategy<MockSleeper>>> handleRWTxn(OpResultSeq &opSeq,
|
||||
OpResultSeq &commitSeq) {
|
||||
auto handler = MockHandler(opSeq);
|
||||
auto retryStrategy = getDefaultRetryStrategy();
|
||||
MockRWTxn txn(commitSeq);
|
||||
auto result = co_await WithTransaction<RetryStrategyRef>({retryStrategy}).run(txn, handler);
|
||||
co_return std::make_pair<Result<Void>, DefaultRetryStrategy<MockSleeper>>(std::move(result),
|
||||
std::move(retryStrategy));
|
||||
}
|
||||
|
||||
TEST(WithTransaction, testReadOnly) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
{
|
||||
// normal succeed
|
||||
OpResultSeq opSeq = {StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op retry once
|
||||
OpResultSeq opSeq = {TransactionCode::kThrottled, StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op fail, cancel fail (IReadOnlyTransaction allows to ignore cancel fail), op fail, cancel fail again, op ok
|
||||
OpResultSeq opSeq = {TransactionCode::kThrottled, TransactionCode::kTooOld, StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op could fail at most 5 times then succeed
|
||||
OpResultSeq opSeq(5, TransactionCode::kThrottled);
|
||||
opSeq.push_back(StatusCode::kOK);
|
||||
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op failed 6 times and return error
|
||||
OpResultSeq opSeq(6, TransactionCode::kThrottled);
|
||||
opSeq.push_back(StatusCode::kOK);
|
||||
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), TransactionCode::kThrottled);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST(WithTransaction, testReadWrite) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
{
|
||||
// normal succeed
|
||||
OpResultSeq seq = {StatusCode::kOK};
|
||||
OpResultSeq txnCommitSeq = {StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleRWTxn(seq, txnCommitSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op retry once
|
||||
OpResultSeq opSeq = {TransactionCode::kThrottled, StatusCode::kOK};
|
||||
OpResultSeq txnCommitSeq = {StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op succeed, commit fail, retry op and commit succeed
|
||||
OpResultSeq opSeq = {StatusCode::kOK, StatusCode::kOK};
|
||||
OpResultSeq txnCommitSeq = {TransactionCode::kThrottled, StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms};
|
||||
std::cout << retryStrategy.getSleeper().records.at(0).count() << std::endl;
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op could fail at most 5 times then succeed
|
||||
OpResultSeq opSeq(5, TransactionCode::kThrottled);
|
||||
opSeq.push_back(StatusCode::kOK);
|
||||
OpResultSeq txnCommitSeq = {StatusCode::kOK};
|
||||
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
|
||||
CO_ASSERT_OK(result);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
{
|
||||
// op failed 6 times and return error
|
||||
OpResultSeq opSeq(6, TransactionCode::kThrottled);
|
||||
opSeq.push_back(StatusCode::kOK);
|
||||
OpResultSeq txnCommitSeq;
|
||||
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), TransactionCode::kThrottled);
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
|
||||
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
58
tests/common/folly/TestConcurrentHashMap.cc
Normal file
58
tests/common/folly/TestConcurrentHashMap.cc
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/concurrency/ConcurrentHashMap.h>
|
||||
#include <folly/portability/GTest.h>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
struct Dummy {
|
||||
std::array<uint8_t, 64u> x;
|
||||
};
|
||||
|
||||
TEST(TestConcurrentHashMap, Normal) {
|
||||
constexpr auto N = 1000000;
|
||||
|
||||
std::atomic<bool> stop = false;
|
||||
std::atomic<uint64_t> readTimes = 0;
|
||||
std::atomic<uint64_t> writeTimes = 0;
|
||||
|
||||
folly::ConcurrentHashMap<uint64_t, Dummy> map;
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
map.insert_or_assign(i, Dummy{});
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
std::jthread writer([&] {
|
||||
while (!stop) {
|
||||
map.insert_or_assign(folly::Random::rand32() % N, Dummy{});
|
||||
++writeTimes;
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<std::jthread> readers(4);
|
||||
for (auto &reader : readers) {
|
||||
reader = std::jthread([&] {
|
||||
while (!stop) {
|
||||
auto it = map.find(folly::Random::rand32() % N);
|
||||
++readTimes;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
stop = true;
|
||||
writer.join();
|
||||
for (auto &reader : readers) {
|
||||
reader.join();
|
||||
}
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
auto ratio = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
|
||||
fmt::print("OPS: {} read, {} write\n", readTimes.load() * 1000 / ratio, writeTimes.load() * 1000 / ratio);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
29
tests/common/folly/TestEventBase.cc
Normal file
29
tests/common/folly/TestEventBase.cc
Normal file
@@ -0,0 +1,29 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/FutureUtil.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Promise.h>
|
||||
#include <folly/io/async/EventBase.h>
|
||||
#include <folly/portability/GTest.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
TEST(TestEventBase, Normal) {
|
||||
folly::EventBase evb;
|
||||
bool finished = false;
|
||||
|
||||
folly::coro::co_invoke([&]() -> folly::coro::Task<void> {
|
||||
auto executor = co_await folly::coro::co_current_executor;
|
||||
EXPECT_EQ(executor, &evb);
|
||||
finished = true;
|
||||
evb.terminateLoopSoon();
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&evb)
|
||||
.start();
|
||||
|
||||
evb.loopForever();
|
||||
ASSERT_TRUE(finished);
|
||||
}
|
||||
341
tests/common/folly/TestTransport.cc
Normal file
341
tests/common/folly/TestTransport.cc
Normal file
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <folly/IPAddressV6.h>
|
||||
#include <folly/SocketAddress.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/io/async/test/AsyncSocketTest.h>
|
||||
#include <folly/io/async/test/MockAsyncTransport.h>
|
||||
#include <folly/io/async/test/ScopedBoundPort.h>
|
||||
#include <folly/io/coro/ServerSocket.h>
|
||||
#include <folly/io/coro/Transport.h>
|
||||
#include <folly/portability/GTest.h>
|
||||
|
||||
static_assert(FOLLY_HAS_COROUTINES, "");
|
||||
|
||||
#if FOLLY_HAS_COROUTINES
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace folly;
|
||||
using namespace folly::coro;
|
||||
|
||||
class TransportTest : public testing::Test {
|
||||
public:
|
||||
template <typename F>
|
||||
void run(F f) {
|
||||
blockingWait(co_invoke(std::move(f)), &evb);
|
||||
}
|
||||
|
||||
folly::coro::Task<> requestCancellation() {
|
||||
cancelSource.requestCancellation();
|
||||
co_return;
|
||||
}
|
||||
|
||||
EventBase evb;
|
||||
CancellationSource cancelSource;
|
||||
};
|
||||
|
||||
class ServerTransportTest : public TransportTest {
|
||||
public:
|
||||
folly::coro::Task<Transport> connect() {
|
||||
co_return co_await Transport::newConnectedSocket(&evb, srv.getAddress(), 0ms);
|
||||
}
|
||||
TestServer srv;
|
||||
};
|
||||
|
||||
TEST_F(TransportTest, ConnectFailure) {
|
||||
run([&]() -> Task<> {
|
||||
// note: currently, docker CI runner doesn't support IPv6
|
||||
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
|
||||
auto serverAddr = ph.getAddress();
|
||||
EXPECT_THROW(co_await Transport::newConnectedSocket(&evb, serverAddr, 0ms), AsyncSocketException);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, ConnectSuccess) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
EXPECT_EQ(srv.getAddress(), cs.getPeerAddress());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, ConnectCancelled) {
|
||||
run([&]() -> Task<> {
|
||||
co_await folly::coro::collectAll(
|
||||
// token would be cancelled while waiting on connect
|
||||
[&]() -> Task<> {
|
||||
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(), connect()), OperationCancelled);
|
||||
}(),
|
||||
requestCancellation());
|
||||
// token was cancelled before read was called
|
||||
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(),
|
||||
Transport::newConnectedSocket(&evb, srv.getAddress(), 0ms)),
|
||||
OperationCancelled);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, SimpleRead) {
|
||||
run([&]() -> Task<> {
|
||||
constexpr auto kBufSize = 65536;
|
||||
auto cs = co_await connect();
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
|
||||
std::array<uint8_t, kBufSize> sndBuf;
|
||||
std::memset(sndBuf.data(), 'a', sndBuf.size());
|
||||
ss->write(sndBuf.data(), sndBuf.size());
|
||||
|
||||
// read using coroutines
|
||||
std::array<uint8_t, kBufSize> rcvBuf;
|
||||
auto reader = [&rcvBuf, &cs]() -> Task<Unit> {
|
||||
int totalBytes{0};
|
||||
while (totalBytes < kBufSize) {
|
||||
auto bytesRead =
|
||||
co_await cs.read(MutableByteRange(rcvBuf.data() + totalBytes, (rcvBuf.data() + rcvBuf.size() - totalBytes)),
|
||||
0ms);
|
||||
totalBytes += bytesRead;
|
||||
}
|
||||
co_return unit;
|
||||
};
|
||||
|
||||
co_await reader();
|
||||
EXPECT_EQ(0, memcmp(sndBuf.data(), rcvBuf.data(), rcvBuf.size()));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, SimpleIOBufRead) {
|
||||
run([&]() -> Task<> {
|
||||
// Exactly fills a buffer mid-loop and triggers deferredReadEOF handling
|
||||
constexpr auto kBufSize = 55 * 1184;
|
||||
|
||||
auto cs = co_await connect();
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
|
||||
std::array<uint8_t, kBufSize> sndBuf;
|
||||
std::memset(sndBuf.data(), 'a', sndBuf.size());
|
||||
ss->write(sndBuf.data(), sndBuf.size());
|
||||
ss->close();
|
||||
|
||||
// read using coroutines
|
||||
IOBufQueue rcvBuf(IOBufQueue::cacheChainLength());
|
||||
int totalBytes{0};
|
||||
while (totalBytes < kBufSize) {
|
||||
auto bytesRead = co_await cs.read(rcvBuf, 1000, 1000, 0ms);
|
||||
totalBytes += bytesRead;
|
||||
}
|
||||
auto bytesRead = co_await cs.read(rcvBuf, 1000, 1000, 50ms);
|
||||
EXPECT_EQ(bytesRead, 0); // closed
|
||||
|
||||
auto data = rcvBuf.move();
|
||||
data->coalesce();
|
||||
EXPECT_EQ(0, memcmp(sndBuf.data(), data->data(), data->length()));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, ReadCancelled) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
auto reader = [&cs]() -> Task<Unit> {
|
||||
std::array<uint8_t, 1024> rcvBuf;
|
||||
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 0ms),
|
||||
OperationCancelled);
|
||||
co_return unit;
|
||||
};
|
||||
|
||||
co_await co_withCancellation(cancelSource.getToken(), folly::coro::collectAll(requestCancellation(), reader()));
|
||||
// token was cancelled before read was called
|
||||
co_await co_withCancellation(cancelSource.getToken(), reader());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, ReadTimeout) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
std::array<uint8_t, 1024> rcvBuf;
|
||||
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 50ms),
|
||||
AsyncSocketException);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, ReadError) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
ss->closeWithReset();
|
||||
|
||||
std::array<uint8_t, 1024> rcvBuf;
|
||||
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 50ms),
|
||||
AsyncSocketException);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, SimpleWrite) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
|
||||
constexpr auto kBufSize = 65536;
|
||||
std::array<uint8_t, kBufSize> sndBuf;
|
||||
std::memset(sndBuf.data(), 'a', sndBuf.size());
|
||||
|
||||
// write use co-routine
|
||||
co_await cs.write(ByteRange(sndBuf.data(), sndBuf.data() + sndBuf.size()));
|
||||
// read on server side
|
||||
std::array<uint8_t, kBufSize> rcvBuf;
|
||||
ss->readAll(rcvBuf.data(), rcvBuf.size());
|
||||
|
||||
EXPECT_EQ(0, memcmp(sndBuf.data(), rcvBuf.data(), rcvBuf.size()));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, SimpleWritev) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
|
||||
IOBufQueue sndBuf;
|
||||
constexpr auto kBufSize = 65536;
|
||||
std::array<uint8_t, kBufSize> bufA;
|
||||
std::memset(bufA.data(), 'a', bufA.size());
|
||||
std::array<uint8_t, kBufSize> bufB;
|
||||
std::memset(bufB.data(), 'b', bufB.size());
|
||||
sndBuf.append(bufA.data(), bufA.size());
|
||||
sndBuf.append(bufB.data(), bufB.size());
|
||||
|
||||
// write use co-routine
|
||||
co_await cs.write(sndBuf);
|
||||
|
||||
// read on server side
|
||||
std::array<uint8_t, kBufSize> rcvBufA;
|
||||
ss->readAll(rcvBufA.data(), rcvBufA.size());
|
||||
EXPECT_EQ(0, memcmp(bufA.data(), rcvBufA.data(), rcvBufA.size()));
|
||||
std::array<uint8_t, kBufSize> rcvBufB;
|
||||
ss->readAll(rcvBufB.data(), rcvBufB.size());
|
||||
EXPECT_EQ(0, memcmp(bufB.data(), rcvBufB.data(), rcvBufB.size()));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ServerTransportTest, WriteCancelled) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
// reduce the send buffer size so the write wouldn't complete immediately
|
||||
auto asyncSocket = dynamic_cast<folly::AsyncSocket *>(cs.getTransport());
|
||||
CHECK(asyncSocket);
|
||||
EXPECT_EQ(asyncSocket->setSendBufSize(4096), 0);
|
||||
// produces blocking socket
|
||||
auto ss = srv.accept(-1);
|
||||
constexpr auto kBufSize = 65536;
|
||||
std::array<uint8_t, kBufSize> sndBuf;
|
||||
std::memset(sndBuf.data(), 'a', sndBuf.size());
|
||||
|
||||
// write use co-routine
|
||||
auto writer = [&]() -> Task<> {
|
||||
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(),
|
||||
cs.write(ByteRange(sndBuf.data(), sndBuf.data() + sndBuf.size()))),
|
||||
OperationCancelled);
|
||||
};
|
||||
|
||||
co_await folly::coro::collectAll(requestCancellation(), writer());
|
||||
co_await co_withCancellation(cancelSource.getToken(), writer());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TransportTest, SimpleAccept) {
|
||||
run([&]() -> Task<> {
|
||||
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
|
||||
ServerSocket css(AsyncServerSocket::newSocket(&evb), ph.getAddress(), 16);
|
||||
auto serverAddr = css.getAsyncServerSocket()->getAddress();
|
||||
|
||||
co_await folly::coro::collectAll(css.accept(), Transport::newConnectedSocket(&evb, serverAddr, 0ms));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TransportTest, AcceptCancelled) {
|
||||
run([&]() -> Task<> {
|
||||
co_await folly::coro::collectAll(requestCancellation(), [&]() -> Task<> {
|
||||
ServerSocket css(AsyncServerSocket::newSocket(&evb), std::nullopt, 16);
|
||||
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(), css.accept()), OperationCancelled);
|
||||
}());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TransportTest, AsyncClientAndServer) {
|
||||
run([&]() -> Task<> {
|
||||
constexpr int kSize = 128;
|
||||
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
|
||||
ServerSocket css(AsyncServerSocket::newSocket(&evb), ph.getAddress(), 16);
|
||||
auto serverAddr = css.getAsyncServerSocket()->getAddress();
|
||||
auto cs = co_await Transport::newConnectedSocket(&evb, serverAddr, 0ms);
|
||||
|
||||
co_await folly::coro::collectAll(
|
||||
[&css]() -> Task<> {
|
||||
auto sock = co_await css.accept();
|
||||
std::array<uint8_t, kSize> buf;
|
||||
memset(buf.data(), 'a', kSize);
|
||||
co_await sock->write(ByteRange(buf.begin(), buf.end()));
|
||||
css.close();
|
||||
}(),
|
||||
|
||||
[&cs]() -> Task<> {
|
||||
std::array<uint8_t, kSize> buf;
|
||||
// For fun, shutdown the write half -- we don't need it
|
||||
cs.shutdownWrite();
|
||||
auto len = co_await cs.read(MutableByteRange(buf.begin(), buf.end()), 0ms);
|
||||
cs.close();
|
||||
EXPECT_TRUE(len == buf.size());
|
||||
}());
|
||||
});
|
||||
}
|
||||
|
||||
class MockTransportTest : public TransportTest {
|
||||
public:
|
||||
folly::coro::Task<Transport> connect() {
|
||||
mockTransport = new testing::NiceMock<test::MockAsyncTransport>();
|
||||
folly::AsyncTransport::UniquePtr transport(mockTransport);
|
||||
co_return Transport(&evb, std::move(transport));
|
||||
}
|
||||
test::MockAsyncTransport *mockTransport;
|
||||
};
|
||||
|
||||
TEST_F(MockTransportTest, readSuccessCanceled) {
|
||||
run([&]() -> Task<> {
|
||||
auto cs = co_await connect();
|
||||
constexpr auto kBufSize = 65536;
|
||||
std::array<uint8_t, kBufSize> rcvBuf;
|
||||
EXPECT_CALL(*mockTransport, setReadCB(testing::_)).WillOnce(testing::Invoke([](AsyncReader::ReadCallback *rcb) {
|
||||
rcb->readEOF();
|
||||
}));
|
||||
EXPECT_CALL(*mockTransport, setReadCB(nullptr)).Times(2);
|
||||
folly::CancellationSource cancellationSource;
|
||||
auto readFut = co_withCancellation(cancellationSource.getToken(),
|
||||
cs.read(MutableByteRange(rcvBuf.data(), rcvBuf.data() + rcvBuf.size()), 100ms))
|
||||
.scheduleOn(&evb)
|
||||
.start();
|
||||
// Let the read coro start and get the EOF
|
||||
co_await co_reschedule_on_current_executor;
|
||||
// cancel
|
||||
cancellationSource.requestCancellation();
|
||||
// read succeeds with nRead == 0
|
||||
auto nRead = co_await std::move(readFut);
|
||||
EXPECT_EQ(nRead, 0);
|
||||
});
|
||||
}
|
||||
#endif // FOLLY_HAS_COROUTINES
|
||||
48
tests/common/folly/TestWaitCallback.cc
Normal file
48
tests/common/folly/TestWaitCallback.cc
Normal file
@@ -0,0 +1,48 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/FutureUtil.h>
|
||||
#include <folly/experimental/coro/Promise.h>
|
||||
#include <folly/portability/GTest.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace folly::coro;
|
||||
|
||||
TEST(CoroWaitCallbackTest, WaitCallback) {
|
||||
bool same_thread = false;
|
||||
|
||||
auto task = [&same_thread]() -> folly::coro::Task<void> {
|
||||
auto tid_before = std::this_thread::get_id();
|
||||
auto [promise, future] = makePromiseContract<void>();
|
||||
std::thread notify_at_another_thread([promise = std::move(promise)]() mutable {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
promise.setValue();
|
||||
});
|
||||
co_await toTask(std::move(future));
|
||||
auto tid_after = std::this_thread::get_id();
|
||||
same_thread = tid_before == tid_after;
|
||||
notify_at_another_thread.join();
|
||||
};
|
||||
|
||||
blockingWait(task());
|
||||
ASSERT_TRUE(same_thread);
|
||||
}
|
||||
|
||||
TEST(CoroWaitCallbackTest, MultiTasks) {
|
||||
int orders = 0;
|
||||
auto create_task = [&orders]() -> folly::coro::Task<void> {
|
||||
auto [promise, future] = makePromiseContract<void>();
|
||||
std::thread notify_at_another_thread([promise = std::move(promise)]() mutable {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
promise.setValue();
|
||||
});
|
||||
orders = orders * 10 + 1;
|
||||
co_await toTask(std::move(future));
|
||||
orders = orders * 10 + 2;
|
||||
notify_at_another_thread.join();
|
||||
};
|
||||
|
||||
blockingWait(collectAll(create_task(), create_task()));
|
||||
ASSERT_EQ(orders, 1122);
|
||||
}
|
||||
29
tests/common/kv/TestKeyPrefix.cc
Normal file
29
tests/common/kv/TestKeyPrefix.cc
Normal file
@@ -0,0 +1,29 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
|
||||
#include "common/kv/KeyPrefix.h"
|
||||
#include "fdb/FDB.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
TEST(KeyPrefixTest, test) {
|
||||
ASSERT_EQ(static_cast<uint32_t>(kv::KeyPrefix::Inode), kv::makePrefixValue("INOD"));
|
||||
ASSERT_EQ(kv::toStr(kv::KeyPrefix::Inode), "INOD");
|
||||
ASSERT_EQ(kv::toStr(static_cast<kv::KeyPrefix>(-1)), "UNKW");
|
||||
}
|
||||
|
||||
TEST(PrefixListEndKey, test) {
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a"), std::string("b"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a1"), std::string("a2"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff"), std::string("b"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff\xff"), std::string("b"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff\xff\xff"), std::string("b"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("\xff"
|
||||
"a\xff\xff"),
|
||||
std::string("\xff"
|
||||
"b"));
|
||||
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("\xff\xff"), std::string(""));
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
27
tests/common/kv/fdb/FDBTestBase.h
Normal file
27
tests/common/kv/fdb/FDBTestBase.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "fdb/FDB.h"
|
||||
#include "fdb/FDBContext.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "tests/fdb/SetupFDB.h"
|
||||
|
||||
namespace hf3fs::kv {
|
||||
class FDBTestBase : public testing::SetupFDB {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
auto cluster = std::getenv("FDB_UNITTEST_CLUSTER");
|
||||
if (!cluster) {
|
||||
GTEST_SKIP_("FDB_UNITTEST_CLUSTER not set skip test");
|
||||
}
|
||||
db_ = fdb::DB(cluster, false);
|
||||
}
|
||||
|
||||
protected:
|
||||
fdb::DB db_;
|
||||
};
|
||||
|
||||
} // namespace hf3fs::kv
|
||||
220
tests/common/kv/fdb/TestFDB.cc
Normal file
220
tests/common/kv/fdb/TestFDB.cc
Normal file
@@ -0,0 +1,220 @@
|
||||
#include <cstdint>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "FDBTestBase.h"
|
||||
|
||||
namespace hf3fs::kv {
|
||||
|
||||
class TestFDB : public FDBTestBase {};
|
||||
|
||||
TEST_F(TestFDB, WaitFuture) {
|
||||
ASSERT_EQ(db_.error(), 0);
|
||||
|
||||
folly::coro::blockingWait([this]() -> folly::coro::Task<void> {
|
||||
fdb::Transaction tr(db_);
|
||||
auto sizeResult = co_await tr.getApproximateSize();
|
||||
EXPECT_TRUE(sizeResult.error() == 0);
|
||||
|
||||
auto result = co_await tr.get("foo", true);
|
||||
EXPECT_TRUE(result.error() == 0);
|
||||
EXPECT_FALSE(result.value().has_value());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDB, GetRange) {
|
||||
ASSERT_EQ(db_.error(), 0);
|
||||
|
||||
::FDBTransaction *tr = nullptr;
|
||||
|
||||
std::string startKey = "unittest.get_range";
|
||||
std::string endKey = "unittest.get_rangez";
|
||||
|
||||
// 1. set
|
||||
{
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
auto k = startKey + fmt::format("{:02d}", i);
|
||||
auto v = fmt::format("unittest.value{:02d}", i);
|
||||
fdb_transaction_set(tr, (const uint8_t *)k.data(), k.length(), (const uint8_t *)v.data(), v.length());
|
||||
}
|
||||
auto f = fdb_transaction_commit(tr);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
|
||||
// 2. get range
|
||||
{
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
auto f =
|
||||
fdb_transaction_get_range(tr,
|
||||
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
|
||||
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
|
||||
10,
|
||||
0,
|
||||
FDB_STREAMING_MODE_SERIAL,
|
||||
0,
|
||||
false,
|
||||
false);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
|
||||
const FDBKeyValue *kv = nullptr;
|
||||
int count = 0;
|
||||
fdb_bool_t more = false;
|
||||
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
|
||||
ASSERT_TRUE(more);
|
||||
ASSERT_EQ(count, 10);
|
||||
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
|
||||
{
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
auto f =
|
||||
fdb_transaction_get_range(tr,
|
||||
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
|
||||
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
|
||||
0,
|
||||
0,
|
||||
FDB_STREAMING_MODE_SERIAL,
|
||||
0,
|
||||
false,
|
||||
false);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
|
||||
const FDBKeyValue *kv = nullptr;
|
||||
int count = 0;
|
||||
fdb_bool_t more = false;
|
||||
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
|
||||
ASSERT_FALSE(more);
|
||||
ASSERT_EQ(count, 100);
|
||||
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
|
||||
{
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
auto f =
|
||||
fdb_transaction_get_range(tr,
|
||||
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
|
||||
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
|
||||
-1,
|
||||
0,
|
||||
FDB_STREAMING_MODE_SERIAL,
|
||||
0,
|
||||
false,
|
||||
false);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
|
||||
const FDBKeyValue *kv = nullptr;
|
||||
int count = 0;
|
||||
fdb_bool_t more = false;
|
||||
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
|
||||
ASSERT_FALSE(more);
|
||||
ASSERT_EQ(count, 100);
|
||||
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
// 3. clear
|
||||
{
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
auto k = startKey + fmt::format("{:02d}", i);
|
||||
fdb_transaction_clear(tr, (const uint8_t *)k.data(), k.length());
|
||||
}
|
||||
auto f = fdb_transaction_commit(tr);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestFDB, Version) {
|
||||
ASSERT_EQ(db_.error(), 0);
|
||||
|
||||
for (auto delay : {0, 10, 100, 200, 500, 1000}) {
|
||||
::FDBTransaction *tr1 = nullptr;
|
||||
::FDBTransaction *tr2 = nullptr;
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr1), 0);
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr2), 0);
|
||||
std::string_view key = "random-key";
|
||||
fdb_transaction_clear(tr1, (uint8_t *)key.data(), key.size());
|
||||
fdb_transaction_clear(tr2, (uint8_t *)key.data(), key.size());
|
||||
auto f1 = fdb_transaction_commit(tr1);
|
||||
if (delay) usleep(delay);
|
||||
auto f2 = fdb_transaction_commit(tr2);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f1), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f1), 0);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f2), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f2), 0);
|
||||
int64_t v1, v2;
|
||||
ASSERT_EQ(fdb_transaction_get_committed_version(tr1, &v1), 0);
|
||||
ASSERT_EQ(fdb_transaction_get_committed_version(tr2, &v2), 0);
|
||||
fmt::print("delay {}us, v1 {}, v2 {}, v1 == v2 {}\n", delay, v1, v2, v1 == v2);
|
||||
fdb_future_destroy(f1);
|
||||
fdb_transaction_destroy(tr1);
|
||||
fdb_future_destroy(f2);
|
||||
fdb_transaction_destroy(tr2);
|
||||
}
|
||||
|
||||
::FDBTransaction *tr = nullptr;
|
||||
std::string_view key = "test-version";
|
||||
std::vector<std::pair<int64_t, std::optional<std::string_view>>> vec;
|
||||
for (auto value :
|
||||
std::vector<std::optional<std::string_view>>{std::nullopt, std::optional("value1"), std::optional("value2")}) {
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
if (value) {
|
||||
fdb_transaction_set(tr, (uint8_t *)key.data(), key.size(), (uint8_t *)value->data(), value->size());
|
||||
} else {
|
||||
fdb_transaction_clear(tr, (uint8_t *)key.data(), key.size());
|
||||
}
|
||||
int64_t version;
|
||||
auto f = fdb_transaction_commit(tr);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
fdb_future_destroy(f);
|
||||
ASSERT_EQ(fdb_transaction_get_committed_version(tr, &version), 0);
|
||||
fdb_transaction_destroy(tr);
|
||||
vec.push_back({version, value});
|
||||
}
|
||||
|
||||
for (auto [version, expected] : vec) {
|
||||
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
|
||||
fdb_transaction_set_read_version(tr, version);
|
||||
auto f = fdb_transaction_get(tr, (uint8_t *)key.data(), key.size(), true);
|
||||
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
|
||||
ASSERT_EQ(fdb_future_get_error(f), 0);
|
||||
const uint8_t *value = nullptr;
|
||||
int length = 0;
|
||||
fdb_bool_t present = false;
|
||||
ASSERT_EQ(fdb_future_get_value(f, &present, &value, &length), 0);
|
||||
if (!expected) {
|
||||
ASSERT_FALSE(present);
|
||||
} else {
|
||||
ASSERT_TRUE(present);
|
||||
std::string str((const char *)value, length);
|
||||
ASSERT_EQ(str, *expected);
|
||||
}
|
||||
fdb_future_destroy(f);
|
||||
fdb_transaction_destroy(tr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hf3fs::kv
|
||||
17
tests/common/kv/fdb/TestFDBKVEngine.cc
Normal file
17
tests/common/kv/fdb/TestFDBKVEngine.cc
Normal file
@@ -0,0 +1,17 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "FDBTestBase.h"
|
||||
#include "fdb/FDBKVEngine.h"
|
||||
|
||||
namespace hf3fs::kv {
|
||||
|
||||
class TestFDBKVEngine : public FDBTestBase {};
|
||||
|
||||
TEST_F(TestFDBKVEngine, Construct) {
|
||||
ASSERT_EQ(db_.error(), 0);
|
||||
FDBKVEngine engine(std::move(db_));
|
||||
ASSERT_TRUE(engine.createReadonlyTransaction() != nullptr);
|
||||
ASSERT_TRUE(engine.createReadWriteTransaction() != nullptr);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::kv
|
||||
306
tests/common/kv/fdb/TestFDBTransaction.cc
Normal file
306
tests/common/kv/fdb/TestFDBTransaction.cc
Normal file
@@ -0,0 +1,306 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <folly/CancellationToken.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <numeric>
|
||||
#include <ostream>
|
||||
#include <set>
|
||||
|
||||
#include "FDBTestBase.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
#include "common/utils/Uuid.h"
|
||||
#include "fdb/FDBKVEngine.h"
|
||||
#include "fdb/FDBTransaction.h"
|
||||
#include "fmt/core.h"
|
||||
#include "foundationdb/fdb_c_options.g.h"
|
||||
#include "foundationdb/fdb_c_types.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::kv {
|
||||
|
||||
class TestFDBTransaction : public FDBTestBase {
|
||||
public:
|
||||
void SetUp() override {
|
||||
FDBTestBase::SetUp();
|
||||
engine_ = FDBKVEngine(std::move(db_));
|
||||
}
|
||||
|
||||
Status convertError(fdb_error_t errCode, bool commit) { return FDBTransaction::testFDBError(errCode, commit); }
|
||||
|
||||
protected:
|
||||
constexpr static auto testKey = "unittest.foo";
|
||||
constexpr static auto testKey2 = "unittest.foo1";
|
||||
constexpr static auto testKey3 = "unittest.foo2";
|
||||
constexpr static auto testValue = "unittest.bar";
|
||||
constexpr static auto conflictKey = "unittest.conflict.";
|
||||
|
||||
FDBKVEngine engine_{std::move(db_)};
|
||||
};
|
||||
|
||||
TEST_F(TestFDBTransaction, SetValue) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->set(testKey, testValue);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->set(testKey2, testValue);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->set(testKey3, testValue);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
auto commit = co_await transaction->commit();
|
||||
EXPECT_TRUE(commit.hasValue());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, SnapshotGet) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
auto result = co_await transaction->snapshotGet(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
EXPECT_TRUE(result.value().has_value());
|
||||
EXPECT_EQ(result.value().value(), testValue);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, SnapshotGetSameKey) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
std::set<uint64_t> latency;
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto begin = SteadyClock::now();
|
||||
auto result = co_await transaction->snapshotGet(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
EXPECT_TRUE(result.value().has_value());
|
||||
EXPECT_EQ(result.value().value(), testValue);
|
||||
auto lat = SteadyClock::now() - begin;
|
||||
latency.insert(lat.count() / 1000);
|
||||
}
|
||||
fmt::print("snapshot get same key, max {}us, min {}us, avg {}us\n",
|
||||
*latency.rbegin(),
|
||||
*latency.begin(),
|
||||
std::accumulate(latency.begin(), latency.end(), 0ul) / latency.size());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, GetRangeSnapshot) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, true);
|
||||
IReadOnlyTransaction::KeySelector end(testKey, true);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 1);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey);
|
||||
EXPECT_FALSE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, GetRangeSnapshot2) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, false);
|
||||
IReadOnlyTransaction::KeySelector end(testKey3, false);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 1);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey2);
|
||||
EXPECT_FALSE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, GetRangeSnapshot3) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, true);
|
||||
IReadOnlyTransaction::KeySelector end(testKey3, true);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 2);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 2);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey);
|
||||
EXPECT_TRUE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, Get) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->get(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
EXPECT_TRUE(result.value().has_value());
|
||||
EXPECT_EQ(result.value().value(), testValue);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, Clear) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->clear(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->clear(testKey2);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->clear(testKey3);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
auto commit = co_await transaction->commit();
|
||||
EXPECT_TRUE(commit.hasValue());
|
||||
auto cancel = co_await transaction->cancel();
|
||||
EXPECT_TRUE(cancel.hasValue());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, TransactionConflict) {
|
||||
constexpr auto M = 8;
|
||||
constexpr auto N = 100;
|
||||
enum OP { GET, SNAPSHOT_GET, ADD_READ_CONFLICT };
|
||||
|
||||
for (const auto op : {GET, SNAPSHOT_GET, ADD_READ_CONFLICT}) {
|
||||
std::atomic<size_t> conflictTimes{0};
|
||||
folly::CPUThreadPoolExecutor executor(M);
|
||||
auto parallelSet = [&]() -> CoTask<void> {
|
||||
for (auto i = 0; i < M; ++i) {
|
||||
auto func = [&]() -> CoTask<void> {
|
||||
for (auto j = 0; j < N; ++j) {
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
auto key = conflictKey + std::to_string(j % 100);
|
||||
switch (op) {
|
||||
case GET:
|
||||
co_await tr->get(key);
|
||||
break;
|
||||
case SNAPSHOT_GET:
|
||||
co_await tr->snapshotGet(key);
|
||||
break;
|
||||
case ADD_READ_CONFLICT:
|
||||
default:
|
||||
co_await tr->addReadConflict(key);
|
||||
break;
|
||||
}
|
||||
co_await tr->set(key, key);
|
||||
auto result = co_await tr->commit();
|
||||
if (result.hasError()) {
|
||||
if (result.error().code() == TransactionCode::kConflict) {
|
||||
++conflictTimes;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return;
|
||||
};
|
||||
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
|
||||
}
|
||||
};
|
||||
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
|
||||
executor.join();
|
||||
|
||||
fmt::print("Transaction conflict times: {}\n", conflictTimes.load());
|
||||
if (op != SNAPSHOT_GET) {
|
||||
ASSERT_NE(conflictTimes, 0);
|
||||
} else {
|
||||
ASSERT_EQ(conflictTimes, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, CancellationSafety) {
|
||||
folly::CPUThreadPoolExecutor exec(1);
|
||||
folly::CancellationSource source;
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto future =
|
||||
folly::coro::co_withCancellation(source.getToken(), transaction->get("not found key")).scheduleOn(&exec).start();
|
||||
source.requestCancellation();
|
||||
ASSERT_THROW(future.wait().value(), folly::OperationCancelled);
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, TestErrorcode) {
|
||||
for (int errCode = 1; errCode < 10000; errCode++) {
|
||||
if (errCode == 1039) continue;
|
||||
auto status = convertError(errCode, false);
|
||||
ASSERT_EQ(TransactionHelper::isRetryable(status, false),
|
||||
fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE_NOT_COMMITTED, errCode))
|
||||
<< status.describe();
|
||||
ASSERT_EQ(TransactionHelper::isRetryable(status, true), fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, errCode))
|
||||
<< status.describe();
|
||||
}
|
||||
|
||||
for (int errCode = 1; errCode < 10000; errCode++) {
|
||||
auto status = convertError(errCode, true);
|
||||
if (errCode == 1039) continue;
|
||||
ASSERT_EQ(TransactionHelper::isRetryable(status, false),
|
||||
fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE_NOT_COMMITTED, errCode))
|
||||
<< status.describe();
|
||||
ASSERT_EQ(TransactionHelper::isRetryable(status, true), fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, errCode))
|
||||
<< status.describe();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, VersionstampedKey) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto prefix = fmt::format("test.{}", Uuid::random());
|
||||
auto key = fmt::format("{}0000000000", prefix);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
CO_ASSERT_OK(co_await tr->setVersionstampedKey(key, prefix.size(), folly::to<std::string>(i)));
|
||||
CO_ASSERT_OK(co_await tr->commit());
|
||||
}
|
||||
|
||||
auto tr = engine_.createReadonlyTransaction();
|
||||
auto result = co_await tr->getRange({prefix, true}, {TransactionHelper::prefixListEndKey(prefix), false}, 120);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->kvs.size(), 100);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto [key, value] = result->kvs.at(i).pair();
|
||||
CO_ASSERT_EQ(key.substr(0, prefix.size()), prefix);
|
||||
CO_ASSERT_EQ(key.size(), prefix.size() + 10);
|
||||
CO_ASSERT_EQ(value, folly::to<std::string>(i));
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, VersionstampedValue) {
|
||||
std::string prev;
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto key = fmt::format("test.{}-", Uuid::random());
|
||||
std::string buffer(1024, 'a');
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto offset = folly::Random::rand32(1024 - 10);
|
||||
{
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
CO_ASSERT_OK(co_await tr->setVersionstampedValue(key, buffer, offset));
|
||||
CO_ASSERT_OK(co_await tr->commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto tr = engine_.createReadonlyTransaction();
|
||||
auto value = co_await tr->get(key);
|
||||
CO_ASSERT_OK(value);
|
||||
CO_ASSERT_TRUE(value->has_value());
|
||||
auto str = **value;
|
||||
CO_ASSERT_EQ(str.substr(0, offset), buffer.substr(0, offset));
|
||||
CO_ASSERT_GE(str.substr(offset, 10), prev);
|
||||
CO_ASSERT_EQ(str.substr(offset + 10), buffer.substr(offset + 10));
|
||||
prev = str.substr(offset, 10);
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestFDBTransaction, Readonly) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
engine_.setReadonly(true);
|
||||
SCOPE_EXIT { engine_.setReadonly(false); };
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto commit = co_await transaction->commit();
|
||||
EXPECT_TRUE(commit.hasError());
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::kv
|
||||
295
tests/common/kv/mem/TestMemTransaction.cc
Normal file
295
tests/common/kv/mem/TestMemTransaction.cc
Normal file
@@ -0,0 +1,295 @@
|
||||
#include <double-conversion/utils.h>
|
||||
#include <folly/Conv.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "common/kv/IKVEngine.h"
|
||||
#include "common/kv/ITransaction.h"
|
||||
#include "common/kv/mem/MemKV.h"
|
||||
#include "common/kv/mem/MemKVEngine.h"
|
||||
#include "fmt/core.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::kv {
|
||||
|
||||
class TestMemTransaction : public ::testing::Test {
|
||||
protected:
|
||||
constexpr static auto testKey = "unittest.foo";
|
||||
constexpr static auto testKey2 = "unittest.foo1";
|
||||
constexpr static auto testKey3 = "unittest.foo2";
|
||||
constexpr static auto testValue = "unittest.bar";
|
||||
|
||||
CoTryTask<void> setupTestKeys() {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->set(testKey, testValue);
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
result = co_await transaction->set(testKey2, testValue);
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
result = co_await transaction->set(testKey3, testValue);
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
auto commit = co_await transaction->commit();
|
||||
CO_RETURN_ON_ERROR(commit);
|
||||
co_return Void();
|
||||
}
|
||||
|
||||
void blockingSetupTestKeys() {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto result = co_await setupTestKeys();
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
}());
|
||||
}
|
||||
|
||||
MemKVEngine engine_;
|
||||
};
|
||||
|
||||
TEST_F(TestMemTransaction, SetValue) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto oldTransaction = engine_.createReadWriteTransaction();
|
||||
|
||||
auto result = co_await setupTestKeys();
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto readOldResult = co_await oldTransaction->get(testKey);
|
||||
EXPECT_TRUE(readOldResult.hasValue());
|
||||
EXPECT_FALSE(readOldResult.value().has_value());
|
||||
|
||||
auto newTransaction = engine_.createReadWriteTransaction();
|
||||
auto readNewResult = co_await newTransaction->get(testKey);
|
||||
EXPECT_TRUE(readNewResult.hasValue());
|
||||
EXPECT_TRUE(readNewResult.value().has_value());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, SnapshotGet) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
auto result = co_await transaction->snapshotGet(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
EXPECT_TRUE(result.value().has_value());
|
||||
EXPECT_EQ(result.value().value(), testValue);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, GetRangeSnapshot) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, true);
|
||||
IReadOnlyTransaction::KeySelector end(testKey, true);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 1);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey);
|
||||
EXPECT_FALSE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, GetRangeSnapshot2) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, false);
|
||||
IReadOnlyTransaction::KeySelector end(testKey3, false);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 1);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey2);
|
||||
EXPECT_FALSE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, GetRangeSnapshot3) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadonlyTransaction();
|
||||
IReadOnlyTransaction::KeySelector begin(testKey, true);
|
||||
IReadOnlyTransaction::KeySelector end(testKey3, true);
|
||||
auto result = co_await transaction->snapshotGetRange(begin, end, 2);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
|
||||
auto value = std::move(result.value());
|
||||
EXPECT_EQ(value.kvs.size(), 2);
|
||||
EXPECT_EQ(value.kvs.front().key, testKey);
|
||||
EXPECT_TRUE(value.hasMore);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, Get) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->get(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
EXPECT_TRUE(result.value().has_value());
|
||||
EXPECT_EQ(result.value().value(), testValue);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, Clear) {
|
||||
blockingSetupTestKeys();
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto transaction = engine_.createReadWriteTransaction();
|
||||
auto result = co_await transaction->clear(testKey);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->clear(testKey2);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
result = co_await transaction->clear(testKey3);
|
||||
EXPECT_TRUE(result.hasValue());
|
||||
auto commit = co_await transaction->commit();
|
||||
EXPECT_TRUE(commit.hasValue());
|
||||
auto cancel = co_await transaction->cancel();
|
||||
EXPECT_TRUE(cancel.hasValue());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, TransactionNoConflict) {
|
||||
constexpr auto M = 8;
|
||||
constexpr auto N = 10000;
|
||||
|
||||
blockingSetupTestKeys();
|
||||
|
||||
std::atomic<size_t> conflictTimes{0};
|
||||
folly::CPUThreadPoolExecutor executor(M);
|
||||
auto parallelSet = [&]() -> CoTask<void> {
|
||||
for (auto i = 0; i < M; ++i) {
|
||||
auto func = [&]() -> CoTask<void> {
|
||||
for (auto j = 0; j < N; ++j) {
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
auto key = std::to_string(j % (N / 100));
|
||||
co_await tr->snapshotGet(key);
|
||||
co_await tr->set(key, key);
|
||||
auto result = co_await tr->commit();
|
||||
if (result.hasError()) {
|
||||
if (result.error().code() == TransactionCode::kConflict) {
|
||||
++conflictTimes;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return;
|
||||
};
|
||||
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
|
||||
}
|
||||
};
|
||||
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
|
||||
executor.join();
|
||||
|
||||
ASSERT_EQ(conflictTimes, 0);
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, TransactionConflict) {
|
||||
constexpr auto M = 8;
|
||||
constexpr auto N = 10000;
|
||||
|
||||
enum OP { GET, SNAPSHOT_GET, ADD_READ_CONFLICT };
|
||||
|
||||
blockingSetupTestKeys();
|
||||
for (const auto op : {GET, SNAPSHOT_GET, ADD_READ_CONFLICT}) {
|
||||
std::atomic<size_t> conflictTimes{0};
|
||||
folly::CPUThreadPoolExecutor executor(M);
|
||||
auto parallelSet = [&]() -> CoTask<void> {
|
||||
for (auto i = 0; i < M; ++i) {
|
||||
auto func = [&]() -> CoTask<void> {
|
||||
for (auto j = 0; j < N; ++j) {
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
auto key = std::to_string(j % (N / 100));
|
||||
switch (op) {
|
||||
case GET:
|
||||
co_await tr->get(key);
|
||||
break;
|
||||
case SNAPSHOT_GET:
|
||||
co_await tr->snapshotGet(key);
|
||||
break;
|
||||
case ADD_READ_CONFLICT:
|
||||
default:
|
||||
co_await tr->addReadConflict(key);
|
||||
break;
|
||||
}
|
||||
co_await tr->set(key, key);
|
||||
auto result = co_await tr->commit();
|
||||
if (result.hasError()) {
|
||||
if (result.error().code() == TransactionCode::kConflict) {
|
||||
++conflictTimes;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return;
|
||||
};
|
||||
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
|
||||
}
|
||||
};
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
|
||||
executor.join();
|
||||
|
||||
fmt::print("Transaction conflict times: {}\n", conflictTimes.load());
|
||||
if (op != SNAPSHOT_GET) {
|
||||
ASSERT_NE(conflictTimes, 0);
|
||||
} else {
|
||||
ASSERT_EQ(conflictTimes, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, VersionstampedKey) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
std::string_view prefix = "key";
|
||||
auto key = fmt::format("{}0000000000", prefix);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
CO_ASSERT_OK(co_await tr->setVersionstampedKey(key, prefix.size(), folly::to<std::string>(i)));
|
||||
CO_ASSERT_OK(co_await tr->commit());
|
||||
}
|
||||
CO_ASSERT_EQ(prefix, "key");
|
||||
|
||||
auto tr = engine_.createReadonlyTransaction();
|
||||
auto result = co_await tr->getRange({prefix, true}, {TransactionHelper::prefixListEndKey(prefix), false}, 120);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->kvs.size(), 100);
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto [key, value] = result->kvs.at(i).pair();
|
||||
CO_ASSERT_EQ(key.substr(0, prefix.size()), prefix);
|
||||
CO_ASSERT_EQ(key.size(), prefix.size() + 10);
|
||||
CO_ASSERT_EQ(value, folly::to<std::string>(i));
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestMemTransaction, VersionstampedValue) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
std::string key("key");
|
||||
std::string buffer(1024, 'a');
|
||||
std::string prev;
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
auto offset = folly::Random::rand32(1024 - 10);
|
||||
{
|
||||
auto tr = engine_.createReadWriteTransaction();
|
||||
CO_ASSERT_OK(co_await tr->setVersionstampedValue(key, buffer, offset));
|
||||
CO_ASSERT_OK(co_await tr->commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto tr = engine_.createReadonlyTransaction();
|
||||
auto value = co_await tr->get(key);
|
||||
CO_ASSERT_OK(value);
|
||||
CO_ASSERT_TRUE(value->has_value());
|
||||
auto str = **value;
|
||||
CO_ASSERT_EQ(str.substr(0, offset), buffer.substr(0, offset));
|
||||
CO_ASSERT_GE(str.substr(offset, 10), prev);
|
||||
CO_ASSERT_EQ(str.substr(offset + 10), buffer.substr(offset + 10));
|
||||
prev = str.substr(offset, 10);
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::kv
|
||||
215
tests/common/logging/TestLogConfig.cc
Normal file
215
tests/common/logging/TestLogConfig.cc
Normal file
@@ -0,0 +1,215 @@
|
||||
#include <folly/json.h>
|
||||
#include <folly/logging/LogConfig.h>
|
||||
#include <folly/logging/LogConfigParser.h>
|
||||
#include <folly/logging/LogLevel.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/logging/LogConfig.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
using namespace ::hf3fs::logging;
|
||||
|
||||
void checkConfig(std::string config, std::string expected) {
|
||||
auto realObj = folly::parseJson(config);
|
||||
auto expectedObj = folly::parseJson(expected);
|
||||
ASSERT_EQ(realObj["categories"], expectedObj["categories"]);
|
||||
ASSERT_EQ(realObj["handlers"], expectedObj["handlers"]);
|
||||
ASSERT_NO_THROW(folly::parseLogConfig(config));
|
||||
}
|
||||
|
||||
TEST(TestLogConfig, testDefault) {
|
||||
LogConfig config;
|
||||
checkConfig(generateLogConfig(config, "x"), R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": { "path": "x.log", "async": "true", "rotate": "true", "max_files": "100", "max_file_size": "10485760", "rotate_on_open": "false" },
|
||||
"type": "file"
|
||||
},
|
||||
"err": {
|
||||
"options": { "level": "ERR", "path": "x.err.log", "async": "false", "rotate": "true", "max_files": "100", "max_file_size": "10485760", "rotate_on_open": "false" },
|
||||
"type": "file"
|
||||
},
|
||||
"fatal": {
|
||||
"options": { "level": "FATAL", "stream": "stderr"},
|
||||
"type": "stream"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
}
|
||||
|
||||
TEST(TestLogConfig, testFileWriter) {
|
||||
LogHandlerConfig handlerConfig;
|
||||
handlerConfig.set_name("normal");
|
||||
handlerConfig.set_writer_type(LogHandlerConfig::WriterType::FILE);
|
||||
handlerConfig.set_file_path("a.log");
|
||||
handlerConfig.set_async(false);
|
||||
handlerConfig.set_rotate(false);
|
||||
LogConfig config;
|
||||
// this case does not care about other handlers
|
||||
config.set_handlers({handlerConfig});
|
||||
String s = generateLogConfig(config, "x");
|
||||
checkConfig(s, R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": { "path": "a.log", "async": "false", "rotate": "false" },
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
|
||||
handlerConfig.set_file_path("");
|
||||
config.set_handlers({handlerConfig});
|
||||
s = generateLogConfig(config, "x");
|
||||
checkConfig(s, R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": { "path": "x.log", "async": "false", "rotate": "false" },
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
|
||||
handlerConfig.set_start_level(folly::LogLevel::INFO);
|
||||
config.set_handlers({handlerConfig});
|
||||
s = generateLogConfig(config, "x");
|
||||
checkConfig(s, R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": { "level": "INFO", "path": "x.log", "async": "false", "rotate": "false" },
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
|
||||
handlerConfig.set_rotate(true);
|
||||
handlerConfig.set_max_files(10);
|
||||
handlerConfig.set_max_file_size(100);
|
||||
handlerConfig.set_rotate_on_open(true);
|
||||
config.set_handlers({handlerConfig});
|
||||
s = generateLogConfig(config, "x");
|
||||
checkConfig(s, R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": {
|
||||
"level": "INFO",
|
||||
"path": "x.log",
|
||||
"async": "false",
|
||||
"rotate": "true",
|
||||
"max_files": "10",
|
||||
"max_file_size": "100",
|
||||
"rotate_on_open": "true"
|
||||
},
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
}
|
||||
|
||||
TEST(TestLogConfig, testStreamWriter) {
|
||||
LogHandlerConfig handlerConfig;
|
||||
handlerConfig.set_name("normal");
|
||||
handlerConfig.set_writer_type(LogHandlerConfig::WriterType::STREAM);
|
||||
handlerConfig.set_stream_type(LogHandlerConfig::StreamType::STDOUT);
|
||||
handlerConfig.set_start_level(folly::LogLevel::INFO);
|
||||
LogConfig config;
|
||||
// this case does not care about other categories
|
||||
config.set_handlers({handlerConfig});
|
||||
|
||||
checkConfig(generateLogConfig(config, "x"), R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal","err","fatal"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": { "options": { "level": "INFO", "stream": "stdout" }, "type": "stream" }
|
||||
}
|
||||
})JSON");
|
||||
}
|
||||
|
||||
TEST(TestLogConfig, testOtherHandlers) {
|
||||
std::vector<LogHandlerConfig> handlers(2);
|
||||
handlers[0].set_name("normal");
|
||||
handlers[0].set_writer_type(LogHandlerConfig::WriterType::STREAM);
|
||||
handlers[0].set_async(true);
|
||||
handlers[0].set_start_level(folly::LogLevel::MIN_LEVEL);
|
||||
handlers[1].set_name("err");
|
||||
handlers[1].set_writer_type(LogHandlerConfig::WriterType::FILE);
|
||||
handlers[1].set_rotate(false);
|
||||
handlers[1].set_async(false);
|
||||
handlers[1].set_start_level(folly::LogLevel::ERR);
|
||||
LogConfig config;
|
||||
config.set_handlers(handlers);
|
||||
checkConfig(generateLogConfig(config, "X"), R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"]}
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"options": { "stream": "stderr" },
|
||||
"type": "stream"
|
||||
},
|
||||
"err": {
|
||||
"options": { "level": "ERR", "path": "X.err.log", "async": "false", "rotate": "false" },
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
}
|
||||
|
||||
TEST(TestLogConfig, testEventLog) {
|
||||
std::vector<LogCategoryConfig> categories(2);
|
||||
std::vector<LogHandlerConfig> handlers(2);
|
||||
categories[0].set_categories({"."});
|
||||
categories[0].set_level(folly::LogLevel::INFO);
|
||||
categories[0].set_handlers({"normal", "err", "fatal"});
|
||||
categories[1].set_categories({"eventlog"});
|
||||
categories[1].set_inherit(false);
|
||||
categories[1].set_propagate(folly::LogLevel::CRITICAL);
|
||||
categories[1].set_level(folly::LogLevel::INFO);
|
||||
categories[1].set_handlers({"event"});
|
||||
handlers[0].set_name("normal");
|
||||
handlers[0].set_writer_type(LogHandlerConfig::WriterType::STREAM);
|
||||
handlers[0].set_start_level(folly::LogLevel::MIN_LEVEL);
|
||||
handlers[1].set_name("err");
|
||||
handlers[1].set_writer_type(LogHandlerConfig::WriterType::EVENT);
|
||||
handlers[1].set_rotate(false);
|
||||
handlers[1].set_async(false);
|
||||
handlers[1].set_start_level(folly::LogLevel::ERR);
|
||||
LogConfig config;
|
||||
config.set_categories(categories);
|
||||
config.set_handlers(handlers);
|
||||
checkConfig(generateLogConfig(config, "X"), R"JSON({
|
||||
"categories": {
|
||||
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal","err","fatal"] },
|
||||
"eventlog": { "level": "INFO", "inherit": false, "propagate": "CRITICAL", "handlers": ["event"] }
|
||||
},
|
||||
"handlers": {
|
||||
"normal": {
|
||||
"type": "stream",
|
||||
"options": { "stream": "stderr" }
|
||||
},
|
||||
"err": {
|
||||
"type": "event",
|
||||
"options": { "level": "ERR", "path": "X.err.log", "async": "false", "rotate": "false" }
|
||||
}
|
||||
}
|
||||
})JSON");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
244
tests/common/monitor/TestMonitor.cc
Normal file
244
tests/common/monitor/TestMonitor.cc
Normal file
@@ -0,0 +1,244 @@
|
||||
#include <chrono>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "common/monitor/Monitor.h"
|
||||
#include "common/monitor/Recorder.h"
|
||||
#include "common/monitor/Sample.h"
|
||||
|
||||
namespace hf3fs::monitor {
|
||||
|
||||
bool checkRecorderHasTag(const Recorder &rec, const TagSet &tag) { return rec.map_.find(tag) != rec.map_.end(); }
|
||||
|
||||
void printSamples(const std::vector<Sample> &samples) {
|
||||
XLOGF(INFO, "{} Samples:", samples.size());
|
||||
for (auto &sample : samples) {
|
||||
if (sample.isNumber()) {
|
||||
XLOGF(INFO, "\t{}: {}", sample.name, sample.number());
|
||||
} else if (sample.isDistribution()) {
|
||||
auto &dist = sample.dist();
|
||||
XLOGF(INFO,
|
||||
"\t{}: cnt {} sum {} min {} p50 {} p90 {} p95 {} p99 {} max {}",
|
||||
sample.name,
|
||||
dist.cnt,
|
||||
dist.sum,
|
||||
dist.min,
|
||||
dist.p50,
|
||||
dist.p90,
|
||||
dist.p95,
|
||||
dist.p99,
|
||||
dist.max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace test {
|
||||
|
||||
size_t countMatchedSamples(const std::string &counterPrefix, const std::vector<Sample> &samples) {
|
||||
size_t sampleSize = 0;
|
||||
for (const auto &s : samples)
|
||||
if (s.name.starts_with(counterPrefix)) sampleSize++;
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
size_t filterSamples(const std::string &counterPrefix, std::vector<Sample> &samples) {
|
||||
std::vector<Sample> matchedSamples;
|
||||
for (const auto &s : samples)
|
||||
if (s.name.starts_with(counterPrefix)) matchedSamples.push_back(s);
|
||||
matchedSamples.swap(samples);
|
||||
return samples.size();
|
||||
}
|
||||
|
||||
TEST(TestMonitor, StartAndStop) {
|
||||
Monitor::Config config;
|
||||
Monitor::start(config);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
Monitor::stop();
|
||||
}
|
||||
|
||||
TEST(TestMonitor, CountRecorder) {
|
||||
auto &collector = Monitor::getDefaultInstance().getCollector();
|
||||
CountRecorder testAdder("test_addr");
|
||||
CountRecorder tagAdderA("tag_adder", monitor::TagSet{{"tag", "a"}});
|
||||
CountRecorder tagAdderB("tag_adder", monitor::TagSet{{"tag", "b"}});
|
||||
|
||||
constexpr auto N = 8;
|
||||
constexpr auto M = 1000000;
|
||||
std::vector<std::thread> threads(N);
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&] {
|
||||
for (auto m = 0; m < M; ++m) {
|
||||
testAdder.addSample(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
std::vector<Sample> samples;
|
||||
testAdder.collect(samples);
|
||||
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 1);
|
||||
filterSamples(testAdder.name(), samples);
|
||||
ASSERT_EQ(samples.front().number(), N * M);
|
||||
|
||||
// TODO_221013
|
||||
monitor::TagSet recorder_tag_a;
|
||||
recorder_tag_a.addTag("tag", "a");
|
||||
monitor::TagSet recorder_tag_b;
|
||||
recorder_tag_b.addTag("tag", "b");
|
||||
testAdder.addSample(1, recorder_tag_a);
|
||||
testAdder.addSample(1, recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 2);
|
||||
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_a));
|
||||
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_b));
|
||||
|
||||
testAdder.addSample(1, recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 1);
|
||||
printSamples(samples);
|
||||
ASSERT_FALSE(checkRecorderHasTag(testAdder, recorder_tag_a));
|
||||
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_b));
|
||||
|
||||
testAdder.addSample(1, recorder_tag_a);
|
||||
testAdder.addSample(1, recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, false);
|
||||
printSamples(samples);
|
||||
|
||||
monitor::TagSet recorder_tag_1{{"anothertag", "1"}};
|
||||
monitor::TagSet recorder_tag_2{{"anothertag", "2"}};
|
||||
tagAdderA.addSample(1, recorder_tag_1);
|
||||
tagAdderA.addSample(1, recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 2);
|
||||
tagAdderB.addSample(1, recorder_tag_1);
|
||||
tagAdderB.addSample(1, recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagAdderB.name(), samples), 2);
|
||||
tagAdderA.addSample(1, recorder_tag_1);
|
||||
tagAdderB.addSample(1, recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 2);
|
||||
tagAdderA.addSample(1, recorder_tag_1);
|
||||
tagAdderA.addSample(1, recorder_tag_2);
|
||||
tagAdderB.addSample(1, recorder_tag_1);
|
||||
tagAdderB.addSample(1, recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 4);
|
||||
}
|
||||
|
||||
TEST(TestMonitor, LatencyRecorder) {
|
||||
auto &collector = Monitor::getDefaultInstance().getCollector();
|
||||
LatencyRecorder testLatency("test_latency");
|
||||
LatencyRecorder tagLatencyA("tag_latency", monitor::TagSet{{"tag", "a"}});
|
||||
LatencyRecorder tagLatencyB("tag_latency", monitor::TagSet{{"tag", "b"}});
|
||||
|
||||
constexpr auto N = 10;
|
||||
constexpr auto M = 1000000;
|
||||
std::vector<std::thread> threads(N);
|
||||
int cnt = 0;
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&, duration = cnt++ * M] {
|
||||
for (auto m = 0; m < M; ++m) {
|
||||
testLatency.addSample(std::chrono::nanoseconds(duration + m));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
std::vector<Sample> samples;
|
||||
testLatency.collect(samples);
|
||||
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 1);
|
||||
auto &sample = samples.front();
|
||||
ASSERT_TRUE(sample.isDistribution());
|
||||
|
||||
ASSERT_EQ(sample.dist().cnt, N * M);
|
||||
ASSERT_NEAR(sample.dist().mean(), N * M / 2.0, 1e1);
|
||||
ASSERT_NEAR(sample.dist().min, 0, 1e-4);
|
||||
ASSERT_NEAR(sample.dist().max, N * M - 1.0, 1e-4);
|
||||
ASSERT_NEAR(sample.dist().p90, N * M * 0.9, 1e4);
|
||||
ASSERT_NEAR(sample.dist().p99, N * M * 0.99, 1e4);
|
||||
|
||||
samples.clear();
|
||||
testLatency.collect(samples);
|
||||
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 0);
|
||||
|
||||
// TODO_221013
|
||||
monitor::TagSet recorder_tag_a;
|
||||
recorder_tag_a.addTag("tag", "a");
|
||||
monitor::TagSet recorder_tag_b;
|
||||
recorder_tag_b.addTag("tag", "b");
|
||||
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_a);
|
||||
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 2);
|
||||
printSamples(samples);
|
||||
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_a));
|
||||
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_b));
|
||||
|
||||
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 1);
|
||||
ASSERT_FALSE(checkRecorderHasTag(testLatency, recorder_tag_a));
|
||||
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_b));
|
||||
|
||||
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_a);
|
||||
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, false);
|
||||
printSamples(samples);
|
||||
|
||||
monitor::TagSet recorder_tag_1{{"anothertag", "1"}};
|
||||
monitor::TagSet recorder_tag_2{{"anothertag", "2"}};
|
||||
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
|
||||
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 2);
|
||||
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
|
||||
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagLatencyB.name(), samples), 2);
|
||||
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
|
||||
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 2);
|
||||
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
|
||||
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
|
||||
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
|
||||
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
|
||||
samples.clear();
|
||||
collector.collectAll(0, samples, true);
|
||||
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 4);
|
||||
}
|
||||
|
||||
TEST(TestMonitor, DISABLED_abort_on_duplicate) {
|
||||
LatencyRecorder recorder1("tag_latency", monitor::TagSet{{"tag", "a"}});
|
||||
LatencyRecorder recorder2("tag_latency", monitor::TagSet{{"tag", "b"}});
|
||||
// abort here.
|
||||
LatencyRecorder recorder3("tag_latency", monitor::TagSet{{"tag", "b"}});
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace hf3fs::monitor
|
||||
66
tests/common/monitor/TestMonitorCollectorClient.cc
Normal file
66
tests/common/monitor/TestMonitorCollectorClient.cc
Normal file
@@ -0,0 +1,66 @@
|
||||
#include <chrono>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "common/monitor/Monitor.h"
|
||||
#include "common/monitor/Recorder.h"
|
||||
|
||||
namespace hf3fs::monitor {
|
||||
|
||||
namespace test {
|
||||
// const std::string kDatabase = "unit_test";
|
||||
|
||||
// All test disabled because they need a running monitor collector server.
|
||||
|
||||
TEST(TestMonitorCollector, DISABLED_MonitorCollectorStartAndStop) {
|
||||
try {
|
||||
monitor::TagSet recorder_tag_a;
|
||||
recorder_tag_a.addTag("tag", "this_is_a_tag");
|
||||
CountRecorder testAdder("test_addr_count", recorder_tag_a);
|
||||
testAdder.addSample(1);
|
||||
Monitor::Config config_;
|
||||
config_.set_reporters_length(1);
|
||||
config_.reporters(0).set_type("monitor_collector");
|
||||
config_.reporters(0).monitor_collector().set_remote_ip("1.2.3.4:5678");
|
||||
Monitor::start(config_);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
Monitor::stop();
|
||||
} catch (error_t e) {
|
||||
ASSERT_TRUE(false);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestMonitorCollector, DISABLED_CountRecorderWithMonitorCollector) {
|
||||
try {
|
||||
monitor::TagSet recorder_tag_a;
|
||||
recorder_tag_a.addTag("tag", "a");
|
||||
monitor::TagSet recorder_tag_b;
|
||||
recorder_tag_b.addTag("tag", "b");
|
||||
CountRecorder testAdder("test_addr_count");
|
||||
testAdder.addSample(1, recorder_tag_a);
|
||||
testAdder.addSample(1, recorder_tag_a);
|
||||
testAdder.addSample(1, recorder_tag_b);
|
||||
|
||||
Monitor::Config config_;
|
||||
config_.set_reporters_length(1);
|
||||
config_.reporters(0).set_type("monitor_collector");
|
||||
config_.reporters(0).monitor_collector().set_remote_ip("1.2.3.4:5678");
|
||||
Monitor::start(config_);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
Monitor::stop();
|
||||
} catch (error_t e) {
|
||||
ASSERT_TRUE(false);
|
||||
}
|
||||
ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace hf3fs::monitor
|
||||
62
tests/common/monitor/TestTaosClient.cc
Normal file
62
tests/common/monitor/TestTaosClient.cc
Normal file
@@ -0,0 +1,62 @@
|
||||
#include <chrono>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/monitor/Monitor.h"
|
||||
|
||||
namespace hf3fs::monitor::test {
|
||||
namespace {
|
||||
|
||||
/*
|
||||
TEST(TestTaosClient, Normal) {
|
||||
constexpr auto kInsertCount = 10000;
|
||||
const std::string kDatabase = "unittest";
|
||||
|
||||
TaosClient::Config config;
|
||||
config.set_db(kDatabase);
|
||||
TaosClient client(config);
|
||||
ASSERT_TRUE(client.init());
|
||||
|
||||
ASSERT_TRUE(client.query(fmt::format("drop database if exists {};", kDatabase)));
|
||||
ASSERT_TRUE(client.query(fmt::format("create database {} precision 'us' update 1 keep 30;", kDatabase)));
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
std::vector<Sample> samples;
|
||||
samples.reserve(kInsertCount);
|
||||
for (int64_t i = 0; i < kInsertCount; ++i) {
|
||||
TagSet sample_tag;
|
||||
samples.emplace_back(Sample{"count", sample_tag, UtcClock::now() + std::chrono::microseconds(i), i});
|
||||
}
|
||||
ASSERT_TRUE(client.commit(samples));
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
fmt::print("Write {} count samples, takes {}ms\n",
|
||||
kInsertCount,
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
|
||||
|
||||
start = std::chrono::steady_clock::now();
|
||||
samples.clear();
|
||||
samples.reserve(kInsertCount);
|
||||
for (uint64_t i = 0; i < kInsertCount; ++i) {
|
||||
Distribution dist;
|
||||
dist.cnt = i + 1;
|
||||
dist.sum = (i + 1) * 50;
|
||||
dist.min = 0;
|
||||
dist.max = 100;
|
||||
dist.p50 = 50;
|
||||
dist.p90 = 90;
|
||||
dist.p95 = 95;
|
||||
dist.p99 = 99;
|
||||
TagSet sample_tag;
|
||||
samples.emplace_back(Sample{"latency", sample_tag, UtcClock::now() + std::chrono::microseconds(i), dist});
|
||||
}
|
||||
ASSERT_TRUE(client.commit(samples));
|
||||
elapsed = std::chrono::steady_clock::now() - start;
|
||||
fmt::print("Write {} latency samples, takes {}ms\n",
|
||||
kInsertCount,
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
|
||||
|
||||
client.stop();
|
||||
}
|
||||
*/
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::monitor::test
|
||||
32
tests/common/net/Echo.h
Normal file
32
tests/common/net/Echo.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/serde/Service.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
|
||||
struct EchoReq {
|
||||
SERDE_STRUCT_FIELD(val, std::string{});
|
||||
SERDE_STRUCT_FIELD(rdma_bufs, std::vector<RDMARemoteBuf>{});
|
||||
};
|
||||
|
||||
struct EchoRsp {
|
||||
SERDE_STRUCT_FIELD(val, std::string{});
|
||||
};
|
||||
|
||||
struct HelloReq {
|
||||
SERDE_STRUCT_FIELD(val, std::string{});
|
||||
};
|
||||
|
||||
struct HelloRsp {
|
||||
SERDE_STRUCT_FIELD(val, std::string{});
|
||||
SERDE_STRUCT_FIELD(idx, uint32_t{});
|
||||
};
|
||||
|
||||
SERDE_SERVICE(Echo, 86) {
|
||||
SERDE_SERVICE_METHOD(echo, 1, EchoReq, EchoRsp);
|
||||
SERDE_SERVICE_METHOD(hello, 2, HelloReq, HelloRsp);
|
||||
SERDE_SERVICE_METHOD(fail, 3, HelloReq, HelloRsp);
|
||||
};
|
||||
|
||||
} // namespace hf3fs::net::test
|
||||
441
tests/common/net/TestEcho.cc
Normal file
441
tests/common/net/TestEcho.cc
Normal file
@@ -0,0 +1,441 @@
|
||||
#include <chrono>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/IOThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/Listener.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/net/sync/Client.h"
|
||||
#include "common/serde/ClientContext.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/common/net/Echo.h"
|
||||
#include "tests/common/net/ib/SetupIB.h"
|
||||
|
||||
DEFINE_uint32(test_echo_loops, 1000, "run echo RPC times per coroutine in TestEcho.MultiThreads");
|
||||
DEFINE_uint32(test_echo_threads, 8, "threads number in TestEcho.MultiThreads");
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
|
||||
public:
|
||||
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &req) {
|
||||
EchoRsp rsp;
|
||||
rsp.val = req.val;
|
||||
co_return rsp;
|
||||
}
|
||||
|
||||
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &req) {
|
||||
HelloRsp rsp;
|
||||
rsp.val = "Hello, " + req.val;
|
||||
rsp.idx = ++idx_;
|
||||
co_return rsp;
|
||||
}
|
||||
|
||||
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) {
|
||||
fmt::print("Request from {}\n", ctx.transport()->describe());
|
||||
co_return makeError(RPCCode::kInvalidMessageType, "failed");
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t idx_ = 0;
|
||||
};
|
||||
static_assert(EchoServiceImpl::kServiceID == 86, "check service id failed");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class TestEcho : public testing::TestWithParam<std::tuple<Address::Type, bool>> {
|
||||
public:
|
||||
static void SetUpTestSuite() { SetupIB::SetUpTestSuite(); }
|
||||
|
||||
void TearDown() override {
|
||||
client_.stopAndJoin();
|
||||
server_.stopAndJoin();
|
||||
}
|
||||
|
||||
Address serverAddr() const { return server_.groups().front()->addressList().front(); }
|
||||
|
||||
protected:
|
||||
Client::Config clientConfig_;
|
||||
bool initClientConfig = [this] {
|
||||
auto rwInEventThread = std::get<1>(GetParam());
|
||||
clientConfig_.io_worker().set_read_write_rdma_in_event_thread(rwInEventThread);
|
||||
clientConfig_.io_worker().set_read_write_tcp_in_event_thread(rwInEventThread);
|
||||
return true;
|
||||
}();
|
||||
Client client_{clientConfig_};
|
||||
|
||||
Server::Config serverConfig_;
|
||||
bool initServerConfig = [this] {
|
||||
serverConfig_.groups(0).set_network_type(std::get<0>(GetParam()));
|
||||
|
||||
auto rwInEventThread = std::get<1>(GetParam());
|
||||
serverConfig_.groups(0).io_worker().set_read_write_rdma_in_event_thread(rwInEventThread);
|
||||
serverConfig_.groups(0).io_worker().set_read_write_tcp_in_event_thread(rwInEventThread);
|
||||
return true;
|
||||
}();
|
||||
Server server_{serverConfig_};
|
||||
};
|
||||
|
||||
TEST_P(TestEcho, RequestAndResponse) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
if (result.hasError()) {
|
||||
std::cout << result.error().describe() << std::endl;
|
||||
}
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, "foo");
|
||||
}
|
||||
|
||||
{
|
||||
HelloReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.hello(ctx, req);
|
||||
CO_ASSERT_OK(result);
|
||||
|
||||
CO_ASSERT_EQ(result->val, "Hello, foo");
|
||||
CO_ASSERT_EQ(result->idx, 1);
|
||||
|
||||
result = co_await client.hello(ctx, req);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->idx, 2);
|
||||
}
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, "foo");
|
||||
}
|
||||
|
||||
{
|
||||
HelloReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.fail(ctx, req);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), RPCCode::kInvalidMessageType);
|
||||
CO_ASSERT_EQ(result.error().message(), "failed");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, AddrType) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
auto addr = serverAddr();
|
||||
addr.type = addr.isTCP() ? Address::RDMA : Address::TCP;
|
||||
auto ctx = client_.serdeCtx(addr);
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
if (addr.isTCP()) {
|
||||
CO_ASSERT_TRUE(result.hasValue());
|
||||
} else {
|
||||
CO_ASSERT_FALSE(result.hasValue());
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, TimeoutStopped) {
|
||||
// Timeout caused by a stopped service
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
net::UserRequestOptions options{};
|
||||
options.timeout = 100_ms;
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
server_.stopAndJoin();
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req, &options);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), RPCCode::kTimeout);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, WithTimestamp) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
serde::Timestamp timestamp;
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req, nullptr, ×tamp);
|
||||
if (result.hasError()) {
|
||||
XLOGF(CRITICAL, "error: {}", result.error().describe());
|
||||
}
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, "foo");
|
||||
}));
|
||||
|
||||
ASSERT_NE(timestamp.serverLatency(), Duration{});
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, Frozen) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
server_.setFrozen(true);
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), RPCCode::kTimeout);
|
||||
}
|
||||
|
||||
server_.setFrozen(false);
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, "foo");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, SendFailed) {
|
||||
client_.start();
|
||||
net::UserRequestOptions options;
|
||||
options.timeout = 1_s;
|
||||
auto ctx = client_.serdeCtx(Address{0});
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req, &options);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), RPCCode::kSendFailed);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, LargeMessage) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
net::UserRequestOptions options;
|
||||
options.timeout = 5_s;
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
EchoReq req;
|
||||
req.val = std::string(10_MB + 1, 'A');
|
||||
auto result = co_await client.echo(ctx, req, &options);
|
||||
if (result.hasError()) {
|
||||
XLOGF(CRITICAL, "error: {}", result.error().describe());
|
||||
}
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, req.val);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, CompressedLargeMessage) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
net::UserRequestOptions options;
|
||||
options.timeout = 5_s;
|
||||
options.compression = {1, 1_KB};
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
|
||||
EchoReq req;
|
||||
req.val = std::string(10_MB + 1, 'A');
|
||||
auto result = co_await client.echo(ctx, req, &options);
|
||||
if (result.hasError()) {
|
||||
XLOGF(CRITICAL, "error: {}", result.error().describe());
|
||||
}
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, req.val);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_P(TestEcho, MultiThreads) {
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server_.setup());
|
||||
ASSERT_TRUE(server_.start());
|
||||
// some CI machine enable kmemleak test, may case long delay.
|
||||
clientConfig_.set_default_timeout(10000_ms);
|
||||
client_.start();
|
||||
net::UserRequestOptions options;
|
||||
options.timeout = 5_s;
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
auto testCoro = [&]() -> CoTask<bool> {
|
||||
for (size_t i = 0; i < FLAGS_test_echo_loops; i++) {
|
||||
Echo<> client;
|
||||
|
||||
EchoReq req;
|
||||
req.val = std::to_string(folly::Random::rand32());
|
||||
auto result = co_await client.echo(ctx, req, &options);
|
||||
if (result.hasError()) {
|
||||
XLOGF(CRITICAL, "error: {}", result.error().describe());
|
||||
co_return false;
|
||||
}
|
||||
EXPECT_EQ(result->val, req.val);
|
||||
}
|
||||
co_return true;
|
||||
};
|
||||
|
||||
for (size_t numThreads = 2; numThreads <= FLAGS_test_echo_threads; numThreads *= 2) {
|
||||
folly::CPUThreadPoolExecutor exec(numThreads);
|
||||
std::vector<folly::SemiFuture<bool>> tasks;
|
||||
for (size_t coroId = 0; coroId < 16 * numThreads; coroId++) {
|
||||
tasks.emplace_back(testCoro().scheduleOn(folly::Executor::getKeepAliveToken(exec)).start());
|
||||
}
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
std::vector<bool> results = co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
CO_ASSERT_TRUE(std::all_of(results.begin(), results.end(), [](auto ok) { return ok; }));
|
||||
}));
|
||||
exec.join();
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(TestEcho,
|
||||
TestEcho,
|
||||
testing::Combine(testing::Values(Address::RDMA, Address::TCP, Address::UNIX),
|
||||
testing::Values(true, false)));
|
||||
|
||||
TEST(TestEchoSync, Normal) {
|
||||
std::array<Address::Type, 4> networks{Address::LOCAL, Address::TCP /*, Address::IPoIB */};
|
||||
for (auto network : networks) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(network);
|
||||
Server server_{serverConfig};
|
||||
|
||||
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_OK(server_.setup());
|
||||
auto initResult = server_.start();
|
||||
if (!initResult) {
|
||||
fmt::print("init failed: {}\n", initResult.error().describe());
|
||||
ASSERT_TRUE(initResult);
|
||||
}
|
||||
auto serverAddr = server_.groups().front()->addressList().front();
|
||||
|
||||
sync::Client::Config clientConfig;
|
||||
sync::Client client_{clientConfig};
|
||||
auto ctx = client_.serdeCtx(serverAddr);
|
||||
Echo<> client;
|
||||
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = client.echoSync(ctx, req);
|
||||
if (result.hasError()) {
|
||||
std::cout << result.error().describe() << std::endl;
|
||||
}
|
||||
ASSERT_FALSE(result.hasError());
|
||||
|
||||
auto &rsp = result.value();
|
||||
ASSERT_EQ(rsp.val, "foo");
|
||||
|
||||
req.val = "bar";
|
||||
result = client.echoSync(ctx, req);
|
||||
if (result.hasError()) {
|
||||
std::cout << result.error().describe() << std::endl;
|
||||
}
|
||||
ASSERT_FALSE(result.hasError());
|
||||
ASSERT_EQ(rsp.val, "bar");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestEchoListener, Normal) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(Address::TCP);
|
||||
serverConfig.groups(0).listener().set_listen_port(folly::Random::rand32(10000, 20000)); // same port.
|
||||
Server server1{serverConfig};
|
||||
Server server2{serverConfig};
|
||||
ASSERT_OK(server1.setup());
|
||||
ASSERT_FALSE(server2.setup());
|
||||
ASSERT_OK(server1.start());
|
||||
ASSERT_FALSE(server2.start());
|
||||
}
|
||||
|
||||
TEST(TestEchoListener, StartAndStop) {
|
||||
constexpr auto N = 100;
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(Address::TCP);
|
||||
Server server{serverConfig};
|
||||
ASSERT_OK(server.setup());
|
||||
server.stopAndJoin();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestEchoListener, DomainSocket) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(Address::UNIX);
|
||||
serverConfig.groups(0).listener().set_listen_port(folly::Random::rand32(10000, 20000)); // same port.
|
||||
Server server{serverConfig};
|
||||
ASSERT_OK(server.setup());
|
||||
ASSERT_OK(server.start());
|
||||
std::this_thread::sleep_for(1_s);
|
||||
server.stopAndJoin();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
58
tests/common/net/TestEventLoop.cc
Normal file
58
tests/common/net/TestEventLoop.cc
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/net/EventLoop.h"
|
||||
#include "common/net/Network.h"
|
||||
#include "common/utils/FdWrapper.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestEventLoop, Normal) {
|
||||
auto eventLoop = EventLoop::create();
|
||||
ASSERT_TRUE(eventLoop->start());
|
||||
eventLoop->stopAndJoin();
|
||||
}
|
||||
|
||||
TEST(TestEventLoop, AddObject) {
|
||||
auto eventLoop = EventLoop::create();
|
||||
ASSERT_TRUE(eventLoop->start());
|
||||
|
||||
class PipeReader : public EventLoop::EventHandler {
|
||||
public:
|
||||
PipeReader(int fd)
|
||||
: fd_(fd) {}
|
||||
|
||||
int fd() const final { return fd_; }
|
||||
void handleEvents(uint32_t epollEvents) final {
|
||||
ASSERT_TRUE(epollEvents & EPOLLIN);
|
||||
uint32_t val;
|
||||
ASSERT_EQ(::read(fd_, &val, sizeof(val)), sizeof(val));
|
||||
val_ = val;
|
||||
}
|
||||
|
||||
auto &val() const { return val_; }
|
||||
|
||||
private:
|
||||
FdWrapper fd_;
|
||||
std::atomic<uint32_t> val_{0};
|
||||
};
|
||||
|
||||
int p[2];
|
||||
ASSERT_EQ(::pipe(p), 0);
|
||||
auto pipeReader = std::make_shared<PipeReader>(p[0]);
|
||||
ASSERT_TRUE(eventLoop->add(pipeReader, EPOLLIN | EPOLLET));
|
||||
ASSERT_EQ(pipeReader->val().load(), 0);
|
||||
|
||||
uint32_t val = rand();
|
||||
ASSERT_EQ(::write(p[1], &val, sizeof(val)), sizeof(val));
|
||||
while (pipeReader->val().load() == 0) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
ASSERT_EQ(pipeReader->val().load(), val);
|
||||
|
||||
ASSERT_TRUE(eventLoop->remove(pipeReader.get()));
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
25
tests/common/net/TestIOWorker.cc
Normal file
25
tests/common/net/TestIOWorker.cc
Normal file
@@ -0,0 +1,25 @@
|
||||
#include <folly/net/NetworkSocket.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/net/IOWorker.h"
|
||||
#include "common/net/Network.h"
|
||||
#include "common/net/Processor.h"
|
||||
#include "common/net/Transport.h"
|
||||
|
||||
namespace hf3fs::net {
|
||||
|
||||
TEST(TestIOWorker, Normal) {
|
||||
CPUExecutorGroup procExecutor(2, "");
|
||||
CPUExecutorGroup ioExecutor(2, "");
|
||||
folly::IOThreadPoolExecutor connExecutor(2);
|
||||
serde::Services serdeServices;
|
||||
Processor::Config processorConfig{};
|
||||
Processor processor(serdeServices, procExecutor, processorConfig);
|
||||
|
||||
IOWorker::Config ioWorkerConfig{};
|
||||
IOWorker ioWorker(processor, ioExecutor, connExecutor, ioWorkerConfig);
|
||||
|
||||
ASSERT_TRUE(ioWorker.start("Test"));
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net
|
||||
97
tests/common/net/TestProcessor.cc
Normal file
97
tests/common/net/TestProcessor.cc
Normal file
@@ -0,0 +1,97 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/Network.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/serde/Service.h"
|
||||
#include "tests/common/net/Echo.h"
|
||||
#include "tests/common/net/ib/SetupIB.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
|
||||
public:
|
||||
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &) {
|
||||
co_await folly::coro::sleep(50ms);
|
||||
co_return EchoRsp{};
|
||||
}
|
||||
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &) { co_return HelloRsp{}; }
|
||||
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) { co_return HelloRsp{}; }
|
||||
|
||||
Status onError(Status status) {
|
||||
++error_;
|
||||
return status;
|
||||
}
|
||||
|
||||
uint32_t error() { return error_; }
|
||||
|
||||
private:
|
||||
std::atomic<uint32_t> error_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestProcessor : public SetupIB {};
|
||||
|
||||
TEST_F(TestProcessor, TooManyProcessingRequests) {
|
||||
constexpr auto kMaxProcessingRequestsNum = 10;
|
||||
constexpr auto kConcurrentRequestsNum = 16;
|
||||
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).processor().set_max_processing_requests_num(kMaxProcessingRequestsNum);
|
||||
Server server(serverConfig);
|
||||
ASSERT_TRUE(server.setup());
|
||||
auto impl = std::make_unique<EchoServiceImpl>();
|
||||
auto ptr = impl.get();
|
||||
server.addSerdeService(std::move(impl));
|
||||
ASSERT_TRUE(server.start());
|
||||
ASSERT_EQ(ptr->error(), 0);
|
||||
|
||||
Client::Config clientConfig;
|
||||
Client client{clientConfig};
|
||||
client.start();
|
||||
auto ctx = client.serdeCtx(server.groups().front()->addressList().front());
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
std::vector<folly::SemiFuture<Result<EchoRsp>>> requests;
|
||||
requests.reserve(kConcurrentRequestsNum);
|
||||
EchoReq echoReq{};
|
||||
for (auto i = 0; i < kConcurrentRequestsNum; ++i) {
|
||||
requests.push_back(client.echo(ctx, echoReq).semi());
|
||||
}
|
||||
auto results = co_await folly::collectAll(std::move(requests));
|
||||
|
||||
auto succ = 0;
|
||||
auto fail = 0;
|
||||
for (auto &result : results) {
|
||||
auto &rsp = *result;
|
||||
if (rsp.hasError()) {
|
||||
++fail;
|
||||
CO_ASSERT_EQ(rsp.error().code(), RPCCode::kRequestRefused);
|
||||
} else {
|
||||
++succ;
|
||||
}
|
||||
}
|
||||
|
||||
CO_ASSERT_GE(succ, kMaxProcessingRequestsNum);
|
||||
CO_ASSERT_GT(fail, 0);
|
||||
CO_ASSERT_LE(fail, kConcurrentRequestsNum - kMaxProcessingRequestsNum);
|
||||
CO_ASSERT_NE(ptr->error(), 0);
|
||||
CO_ASSERT_EQ(ptr->error(), fail);
|
||||
|
||||
auto [rsp, _] = co_await folly::coro::collectAll(client.echo(ctx, EchoReq{}), [&]() -> CoTask<void> {
|
||||
co_await folly::coro::sleep(10ms);
|
||||
server.stopAndJoin();
|
||||
}());
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net::test
|
||||
51
tests/common/net/TestRDMAControl.cc
Normal file
51
tests/common/net/TestRDMAControl.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/RDMAControl.h"
|
||||
#include "common/net/RequestOptions.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/serde/ClientContext.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/common/net/ib/SetupIB.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
struct RDMAControlService : serde::ServiceWrapper<RDMAControlService, RDMAControl> {
|
||||
CoTryTask<RDMATransmissionRsp> apply(serde::CallContext &ctx, const RDMATransmissionReq &req) {
|
||||
auto &tr = ctx.transport();
|
||||
if (!tr->isRDMA()) {
|
||||
co_return makeError(StatusCode::kInvalidArg);
|
||||
}
|
||||
serde::ClientContext clientCtx(tr);
|
||||
auto result = co_await RDMAControl<>::apply(clientCtx, req);
|
||||
co_return result;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(TestRDMAControl, Normal) {
|
||||
SetupIB::SetUpTestSuite();
|
||||
|
||||
net::Server::Config serverConfig;
|
||||
net::Server server_{serverConfig};
|
||||
ASSERT_OK(server_.setup());
|
||||
ASSERT_OK(server_.start());
|
||||
ASSERT_OK(server_.addSerdeService(std::make_unique<RDMAControlService>()));
|
||||
|
||||
net::Client::Config clientConfig;
|
||||
net::Client client_{clientConfig};
|
||||
ASSERT_OK(client_.start());
|
||||
auto ctx = client_.serdeCtx(server_.groups().front()->addressList().front());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
{
|
||||
RDMATransmissionReq req;
|
||||
auto result = co_await RDMAControl<>::apply(ctx, req);
|
||||
if (result.hasError()) {
|
||||
std::cout << result.error().describe() << std::endl;
|
||||
}
|
||||
CO_ASSERT_OK(result);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
62
tests/common/net/TestWaiter.cc
Normal file
62
tests/common/net/TestWaiter.cc
Normal file
@@ -0,0 +1,62 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "common/net/Waiter.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestTimer, Normal) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
Waiter::Item item;
|
||||
auto uuid = Waiter::instance().bind(item);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
Waiter::instance().schedule(uuid, std::chrono::milliseconds(50));
|
||||
co_await item.baton;
|
||||
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
CO_ASSERT_TRUE(30ms <= elapsed && elapsed <= 70ms);
|
||||
CO_ASSERT_EQ(item.status.code(), RPCCode::kTimeout);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST(TestTimer, TwoUUIDs) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
Waiter::Item item1;
|
||||
auto uuid1 = Waiter::instance().bind(item1);
|
||||
Waiter::Item item2;
|
||||
auto uuid2 = Waiter::instance().bind(item2);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
Waiter::instance().schedule(uuid1, std::chrono::milliseconds(50));
|
||||
Waiter::instance().schedule(uuid2, std::chrono::milliseconds(80));
|
||||
|
||||
co_await item1.baton;
|
||||
co_await item2.baton;
|
||||
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
CO_ASSERT_TRUE(60ms <= elapsed);
|
||||
CO_ASSERT_TRUE(elapsed <= 100ms);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST(TestTimer, TimeoutBeforeWait) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
Waiter::Item item;
|
||||
auto uuid = Waiter::instance().bind(item);
|
||||
Waiter::instance().schedule(uuid, std::chrono::milliseconds(0));
|
||||
|
||||
std::this_thread::sleep_for(10ms);
|
||||
co_await item.baton;
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
22
tests/common/net/ib/SetupIB.h
Normal file
22
tests/common/net/ib/SetupIB.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
|
||||
class SetupIB : public ::testing::Test {
|
||||
public:
|
||||
static void SetUpTestSuite() {
|
||||
static IBConfig config;
|
||||
auto ib = IBManager::start(config);
|
||||
XLOGF_IF(FATAL, ib.hasError(), "IBManager start failed, result {}", ib.error());
|
||||
ASSERT_FALSE(IBDevice::all().empty());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hf3fs::net::test
|
||||
263
tests/common/net/ib/TestIBDevice.cc
Normal file
263
tests/common/net/ib/TestIBDevice.cc
Normal file
@@ -0,0 +1,263 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <fmt/compile.h>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Conv.h>
|
||||
#include <folly/IPAddressV4.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/String.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/futures/detail/Types.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <infiniband/verbs.h>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#include "SetupIB.h"
|
||||
#include "common/net/IfAddrs.h"
|
||||
#include "common/net/ib/IBConnect.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
#include "common/utils/UtcTime.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
DEFINE_uint64(ibdev_reg_mem_size, 1ull << 30, "Total memory size to register.");
|
||||
DEFINE_uint32(ibdev_reg_mem_ms, 100, "Time to run reg memory test for each case.");
|
||||
|
||||
DEFINE_uint64(reg_gb, 1, "reg memory size.");
|
||||
DEFINE_bool(reg_odp, false, "odp");
|
||||
DEFINE_bool(reg_releaxed_ording, false, "releaxed ording");
|
||||
DEFINE_uint32(reg_split, 1, "reg split large memory");
|
||||
|
||||
namespace hf3fs::net {
|
||||
|
||||
class TestIBDevice : public test::SetupIB {};
|
||||
|
||||
TEST_F(TestIBDevice, IfAddrs) {
|
||||
auto result = IfAddrs::load();
|
||||
ASSERT_TRUE(!result.hasError());
|
||||
for (auto [name, addr] : *result) {
|
||||
fmt::print("{} -> ip {}, mask {}, up {}\n", name, addr.ip.str(), addr.mask, addr.up);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestIBDevice, Normal) {
|
||||
for (auto &dev : IBDevice::all()) {
|
||||
fmt::print("dev: {}\n", dev->name());
|
||||
ASSERT_NE(dev->ports().size(), 0);
|
||||
ASSERT_NE(dev->context(), nullptr);
|
||||
for (size_t portNum = 1; portNum <= dev->ports().size(); portNum++) {
|
||||
auto port = dev->openPort(portNum);
|
||||
ASSERT_FALSE(port.hasError()) << port.error().describe();
|
||||
fmt::print("\tport {}, zones {}\n", *port, fmt::join(port->zones().begin(), port->zones().end(), ";"));
|
||||
ASSERT_EQ(port->dev(), dev.get());
|
||||
|
||||
if (port->isRoCE()) {
|
||||
auto r = port->getRoCEv2Gid();
|
||||
fmt::print("\tRoCE v2 GID[{}]: {}\n", r.second, r.first);
|
||||
}
|
||||
|
||||
for (int i = 0; true; i++) {
|
||||
ibv_gid gid;
|
||||
auto ret = ibv_query_gid(dev->context(), port->portNum(), i, &gid);
|
||||
if (ret || std::all_of(&gid.raw[0], &gid.raw[sizeof(gid)], [](auto v) { return v == 0; })) {
|
||||
break;
|
||||
}
|
||||
auto ip = folly::IPAddressV6::fromBinary(folly::ByteRange(gid.raw, sizeof(gid.raw)));
|
||||
fmt::print("\t\tGID[{}]: {}, {} {}\n", i, gid, ip.str(), ip.is6To4());
|
||||
}
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
__be16 pkey;
|
||||
auto ret = ibv_query_pkey(dev->context(), portNum, i, &pkey);
|
||||
if (ret != 0 || pkey == 0) {
|
||||
break;
|
||||
}
|
||||
fmt::print("dev {}, port {}, pkey {} => {:x}\n", dev->name(), portNum, i, pkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestIBDevice, UpdatePortStatus) {
|
||||
ASSERT_FALSE(IBDevice::all().empty());
|
||||
ASSERT_FALSE(IBDevice::get(0)->ports().empty());
|
||||
|
||||
for (auto &dev : IBDevice::all()) {
|
||||
for (auto &[portNum, port] : dev->ports()) {
|
||||
auto state = port.attr.rlock()->state;
|
||||
|
||||
// disable a port by set state to DOWN
|
||||
port.attr.withWLock([](auto &attr) { attr.state = IBV_PORT_DOWN; });
|
||||
// open port, then port state should be updated
|
||||
auto result = dev->openPort(portNum);
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(result->attr().state, state);
|
||||
ASSERT_EQ(port.attr.rlock()->state, state);
|
||||
|
||||
// disable a port by set state to DOWN
|
||||
port.attr.withWLock([](auto &attr) { attr.state = IBV_PORT_DOWN; });
|
||||
// wait sometime, period runner will update port status
|
||||
auto begin = SteadyClock::now();
|
||||
while (true) {
|
||||
auto attr = *port.attr.rlock();
|
||||
if (attr.state == state) {
|
||||
break;
|
||||
}
|
||||
ASSERT_TRUE(SteadyClock::now() < begin + 30_s) << "port status doesn't update in 30s";
|
||||
std::this_thread::sleep_for(1_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto regFlags() {
|
||||
auto flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
|
||||
if (FLAGS_reg_odp) {
|
||||
flags |= IBV_ACCESS_ON_DEMAND;
|
||||
}
|
||||
if (FLAGS_reg_releaxed_ording) {
|
||||
flags |= IBV_ACCESS_RELAXED_ORDERING;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
TEST_F(TestIBDevice, RegMemory) {
|
||||
void *mem = malloc(FLAGS_ibdev_reg_mem_size);
|
||||
auto guard = folly::makeGuard([&]() { free(mem); });
|
||||
auto benchmark = [&]() {
|
||||
for (uint64_t bs = 4096; bs <= (16 << 20); bs *= 4) {
|
||||
auto numBlock = FLAGS_ibdev_reg_mem_size / bs;
|
||||
std::chrono::nanoseconds regDur(0), deregDur(0);
|
||||
auto begin = std::chrono::steady_clock::now();
|
||||
size_t iters = 0;
|
||||
while (std::chrono::steady_clock::now() - begin < std::chrono::milliseconds(FLAGS_ibdev_reg_mem_ms)) {
|
||||
std::array<ibv_mr *, 100> mrs;
|
||||
auto regBegin = std::chrono::steady_clock::now();
|
||||
for (size_t i = 0; i < mrs.size(); i++) {
|
||||
size_t offset = (i % numBlock) * bs;
|
||||
void *ptr = (uint8_t *)mem + offset;
|
||||
mrs[i] = IBDevice::get(0)->regMemory(ptr, bs, regFlags());
|
||||
|
||||
iters++;
|
||||
}
|
||||
regDur += std::chrono::steady_clock::now() - regBegin;
|
||||
|
||||
auto deregBegin = std::chrono::steady_clock::now();
|
||||
for (auto mr : mrs) {
|
||||
IBDevice::get(0)->deregMemory(mr);
|
||||
}
|
||||
deregDur += std::chrono::steady_clock::now() - deregBegin;
|
||||
}
|
||||
|
||||
fmt::print("bs: {:8}, reg {:192.168f} us, dereg {:192.168f} us\n",
|
||||
bs,
|
||||
regDur.count() / 1000.0 / iters,
|
||||
deregDur.count() / 1000.0 / iters);
|
||||
}
|
||||
};
|
||||
|
||||
benchmark();
|
||||
auto mr = IBDevice::get(0)->regMemory(mem, FLAGS_ibdev_reg_mem_size, regFlags());
|
||||
fmt::print("Register reged memory.\n");
|
||||
benchmark();
|
||||
IBDevice::get(0)->deregMemory(mr);
|
||||
}
|
||||
|
||||
TEST_F(TestIBDevice, DISABLED_RegMR) {
|
||||
std::atomic_bool exit = false;
|
||||
auto progress = std::jthread([&]() {
|
||||
while (!exit) {
|
||||
sleep(5);
|
||||
XLOGF(INFO, "still alive");
|
||||
}
|
||||
});
|
||||
|
||||
auto memsize = FLAGS_reg_gb * (1ULL << 30);
|
||||
XLOGF(INFO, "malloc begin {}", memsize);
|
||||
auto *mem = (uint8_t *)malloc(memsize);
|
||||
XLOGF(INFO, "malloc success, {} {}", memsize, (void *)mem);
|
||||
|
||||
std::vector<ibv_mr *> mrs;
|
||||
auto bs = memsize / FLAGS_reg_split;
|
||||
XLOGF(INFO, "begin reg");
|
||||
for (size_t i = 0; i < FLAGS_reg_split; i++) {
|
||||
auto mr = IBDevice::get(0)->regMemory(mem + bs * i, bs, regFlags());
|
||||
XLOGF(INFO, "reg get {}", (void *)mr);
|
||||
ASSERT_NE(mr, nullptr) << memsize;
|
||||
mrs.push_back(mr);
|
||||
}
|
||||
|
||||
XLOGF(INFO, "dereg begin");
|
||||
for (auto mr : mrs) {
|
||||
IBDevice::get(0)->deregMemory(mr);
|
||||
}
|
||||
XLOGF(INFO, "dereg finish");
|
||||
exit = true;
|
||||
}
|
||||
|
||||
TEST_F(TestIBDevice, DISABLED_VerbsRegMR) {
|
||||
int num_devices;
|
||||
struct ibv_device **dev_list = ibv_get_device_list(&num_devices);
|
||||
SCOPE_EXIT { ibv_free_device_list(dev_list); };
|
||||
ASSERT_NE(dev_list, nullptr);
|
||||
XLOGF(INFO, "Use device {}", ibv_get_device_name(dev_list[0]));
|
||||
|
||||
auto dev = dev_list[0];
|
||||
auto ctx = ibv_open_device(dev);
|
||||
ASSERT_NE(ctx, nullptr);
|
||||
auto pd = ibv_alloc_pd(ctx);
|
||||
ASSERT_NE(pd, nullptr);
|
||||
|
||||
// malloc
|
||||
auto memsize = FLAGS_reg_gb * (1ULL << 30);
|
||||
XLOGF(INFO, "malloc begin {}", memsize);
|
||||
auto *mem = (uint8_t *)malloc(memsize);
|
||||
XLOGF(INFO, "malloc success, {} {}", memsize, (void *)mem);
|
||||
|
||||
// ibv_reg_mr
|
||||
std::vector<ibv_mr *> mrs;
|
||||
auto bs = memsize / FLAGS_reg_split;
|
||||
XLOGF(INFO, "reg begin");
|
||||
for (size_t i = 0; i < FLAGS_reg_split; i++) {
|
||||
auto mr = ibv_reg_mr(pd, mem + bs * i, bs, regFlags());
|
||||
XLOGF(INFO, "reg get {}", (void *)mr);
|
||||
ASSERT_NE(mr, nullptr) << memsize;
|
||||
mrs.push_back(mr);
|
||||
}
|
||||
|
||||
XLOGF(INFO, "dereg begin");
|
||||
for (auto mr : mrs) {
|
||||
ibv_dereg_mr(mr);
|
||||
}
|
||||
XLOGF(INFO, "dereg finish");
|
||||
}
|
||||
|
||||
// on CI machine with virtualized RDMA NIC, this test will fail.
|
||||
TEST_F(TestIBDevice, DISABLED_reregMR) {
|
||||
static constexpr size_t kMemFlags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
|
||||
for (auto i = 0; i < 100; i++) {
|
||||
void *mem = malloc(4 << 10);
|
||||
auto guard = folly::makeGuard([&]() { free(mem); });
|
||||
|
||||
auto mr1 = IBDevice::get(0)->regMemory(mem, 4 << 10, kMemFlags);
|
||||
auto rkey1 = mr1->rkey;
|
||||
ASSERT_EQ(IBDevice::get(0)->deregMemory(mr1), 0);
|
||||
auto mr2 = IBDevice::get(0)->regMemory(mem, 4 << 10, kMemFlags);
|
||||
ASSERT_NE(rkey1, mr2->rkey);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net
|
||||
105
tests/common/net/ib/TestIBNotInitialized.cc
Normal file
105
tests/common/net/ib/TestIBNotInitialized.cc
Normal file
@@ -0,0 +1,105 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/Listener.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/net/WriteItem.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/net/sync/Client.h"
|
||||
#include "common/serde/ClientContext.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/common/net/Echo.h"
|
||||
|
||||
namespace hf3fs::net {
|
||||
using namespace test;
|
||||
|
||||
class TestIBNotInitialized : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { IBManager::stop(); }
|
||||
};
|
||||
|
||||
TEST_F(TestIBNotInitialized, Basic) {
|
||||
ASSERT_FALSE(IBManager::initialized());
|
||||
ASSERT_TRUE(IBDevice::all().empty());
|
||||
}
|
||||
|
||||
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
|
||||
public:
|
||||
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &req) {
|
||||
EchoRsp rsp;
|
||||
rsp.val = req.val;
|
||||
co_return rsp;
|
||||
}
|
||||
|
||||
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &req) {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
|
||||
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) {
|
||||
co_return makeError(StatusCode::kNotImplemented);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TestIBNotInitialized, TCP) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(Address::TCP);
|
||||
Server server{serverConfig};
|
||||
ASSERT_TRUE(server.setup());
|
||||
server.addSerdeService(std::make_unique<EchoServiceImpl>());
|
||||
ASSERT_TRUE(server.start());
|
||||
|
||||
Client::Config clientConfig;
|
||||
Client client{clientConfig};
|
||||
client.start();
|
||||
|
||||
auto ctx = client.serdeCtx(server.groups().front()->addressList().front());
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result->val, "foo");
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestIBNotInitialized, RDMAServer) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(Address::RDMA);
|
||||
Server server{serverConfig};
|
||||
ASSERT_OK(server.setup());
|
||||
ASSERT_ERROR(server.start(), RPCCode::kIBDeviceNotInitialized);
|
||||
}
|
||||
|
||||
TEST_F(TestIBNotInitialized, RDMAClient) {
|
||||
Client::Config clientConfig;
|
||||
Client client{clientConfig};
|
||||
client.start();
|
||||
|
||||
auto ctx = client.serdeCtx(Address::fromString("rdma://127.0.0.1:8000"));
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
Echo<> client;
|
||||
EchoReq req;
|
||||
req.val = "foo";
|
||||
auto result = co_await client.echo(ctx, req);
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
CO_ASSERT_EQ(result.error().code(), RPCCode::kSendFailed);
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net
|
||||
817
tests/common/net/ib/TestIBSocket.cc
Normal file
817
tests/common/net/ib/TestIBSocket.cc
Normal file
@@ -0,0 +1,817 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bits/types/struct_iovec.h>
|
||||
#include <boost/thread/barrier.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/IPAddress.h>
|
||||
#include <folly/IPAddressV4.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/Range.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/SocketAddress.h>
|
||||
#include <folly/Synchronized.h>
|
||||
#include <folly/Utility.h>
|
||||
#include <folly/experimental/coro/Baton.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/futures/detail/Types.h>
|
||||
#include <folly/hash/Checksum.h>
|
||||
#include <folly/io/async/AsyncServerSocket.h>
|
||||
#include <folly/io/async/EventBase.h>
|
||||
#include <folly/io/coro/ServerSocket.h>
|
||||
#include <folly/io/coro/Transport.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <folly/net/NetworkSocket.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <infiniband/verbs.h>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <pthread.h>
|
||||
#include <queue>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "SetupIB.h"
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/MessageHeader.h"
|
||||
#include "common/net/Network.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/net/WriteItem.h"
|
||||
#include "common/net/ib/IBConnect.h"
|
||||
#include "common/net/ib/IBConnectService.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/net/ib/IBSocket.h"
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/serde/ClientContext.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
|
||||
// DEFINE_uint32(gid_index, 0, "RoCE gid index.");
|
||||
DEFINE_bool(ib_wait_fd, true, "IBSocket wait on fd instead of busy polling.");
|
||||
DEFINE_uint64(ib_bench_mb, (4 << 10), "IBSocket benchmark send data size (mb).");
|
||||
DEFINE_uint32(ib_bench_buf_kb, 0, "IBSocket benchmark send buf size");
|
||||
DEFINE_uint32(ib_bench_buf_cnt, 0, "IBSocket benchmark send buf cnt");
|
||||
DEFINE_uint64(ib_pingpong_loops, 10000, "IBSocket pingpong loops.");
|
||||
DEFINE_uint64(ib_pingpong_size, 64, "IBSocket pingpong msg size (bytes).");
|
||||
DEFINE_uint64(ib_rdma_iters, 500, "IBSocket RDMA test iters.");
|
||||
|
||||
struct TestUtils {
|
||||
static IBSocket::Config testCfg() {
|
||||
IBSocket::Config cfg;
|
||||
cfg.set_retry_cnt(0);
|
||||
cfg.set_buf_size(512);
|
||||
cfg.set_send_buf_cnt(32);
|
||||
cfg.set_buf_ack_batch(4);
|
||||
cfg.set_buf_signal_batch(4);
|
||||
cfg.set_max_rdma_wr(7);
|
||||
cfg.set_max_rdma_wr_per_post(5);
|
||||
cfg.set_max_sge(3);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static std::vector<std::jthread> poll(std::vector<IBSocket *> sockets, std::atomic<bool> &stop) {
|
||||
std::vector<std::jthread> threads;
|
||||
threads.reserve(sockets.size());
|
||||
for (auto socket : sockets) {
|
||||
threads.emplace_back([&stop, socket]() {
|
||||
while (!stop) {
|
||||
TestUtils::wait(*socket, 0, 100ms);
|
||||
}
|
||||
});
|
||||
}
|
||||
return threads;
|
||||
}
|
||||
|
||||
static Result<Socket::Events> wait(IBSocket &socket, Socket::Events events, std::chrono::milliseconds timeout) {
|
||||
while (true) {
|
||||
if (FLAGS_ib_wait_fd && timeout.count() != 0) {
|
||||
pollfd fd{
|
||||
.fd = socket.fd(),
|
||||
.events = POLL_IN,
|
||||
.revents = 0,
|
||||
};
|
||||
int ret = ::poll(&fd, 1, timeout.count());
|
||||
if (ret < 0 || (ret == 0 && timeout.count())) {
|
||||
return makeError(RPCCode::kTimeout, fmt::format("poll ret is {}", ret));
|
||||
}
|
||||
}
|
||||
|
||||
auto result = socket.poll(0);
|
||||
if (result.hasError()) {
|
||||
return makeError(std::move(result.error()));
|
||||
}
|
||||
if (events == 0 || (result.value() & events) != 0) {
|
||||
return result.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Result<Void> send(IBSocket &socket, folly::ByteRange buf, bool poll = true) {
|
||||
while (!buf.empty()) {
|
||||
// try send first
|
||||
auto sendResult = socket.send(buf);
|
||||
RETURN_ON_ERROR(sendResult);
|
||||
buf.advance(sendResult.value());
|
||||
|
||||
if (buf.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// wait writable
|
||||
if (poll) {
|
||||
auto waitResult = wait(socket, Socket::kEventWritableFlag, 1000ms);
|
||||
RETURN_ON_ERROR(waitResult);
|
||||
}
|
||||
}
|
||||
|
||||
return socket.flush();
|
||||
}
|
||||
|
||||
static Result<Void> send(IBSocket &socket, iovec *iovs, int iovCnts, bool poll) {
|
||||
for (int i = 0; i < iovCnts; i++) {
|
||||
auto result = send(socket, folly::ByteRange((uint8_t *)iovs[i].iov_base, iovs[i].iov_len), poll);
|
||||
RETURN_ON_ERROR(result);
|
||||
}
|
||||
|
||||
return Void{};
|
||||
}
|
||||
|
||||
static Result<Void> recv(IBSocket &socket, folly::MutableByteRange buf, bool poll = true) {
|
||||
if (buf.empty()) {
|
||||
return Void{};
|
||||
}
|
||||
while (true) {
|
||||
auto recvResult = socket.recv(buf);
|
||||
RETURN_ON_ERROR(recvResult);
|
||||
buf.advance(recvResult.value());
|
||||
|
||||
if (buf.empty()) {
|
||||
break;
|
||||
}
|
||||
// wait readable
|
||||
if (poll) {
|
||||
auto waitResult = wait(socket, Socket::kEventReadableFlag, 1000ms);
|
||||
RETURN_ON_ERROR(waitResult);
|
||||
}
|
||||
}
|
||||
|
||||
return Void{};
|
||||
}
|
||||
};
|
||||
|
||||
class TestMsg {
|
||||
public:
|
||||
static Result<Void> send(IBSocket &socket, size_t bytes, bool poll = true) {
|
||||
TestMsg msg;
|
||||
msg.bytes_ = bytes;
|
||||
msg.data_.resize(bytes);
|
||||
for (size_t i = 0; i < bytes; i++) {
|
||||
msg.data_[i] = (uint8_t)folly::Random::rand32();
|
||||
}
|
||||
msg.crc32c_ = folly::crc32c(msg.data_.data(), msg.data_.size(), 0);
|
||||
|
||||
iovec iovs[] = {
|
||||
{.iov_base = &msg.bytes_, .iov_len = sizeof(msg.bytes_)},
|
||||
{.iov_base = &msg.crc32c_, .iov_len = sizeof(msg.crc32c_)},
|
||||
{.iov_base = msg.data_.data(), .iov_len = msg.data_.size()},
|
||||
};
|
||||
|
||||
return TestUtils::send(socket, iovs, sizeof(iovs) / sizeof(iovs[0]), poll);
|
||||
}
|
||||
|
||||
static Result<Void> recv(IBSocket &socket, bool poll = true) {
|
||||
TestMsg msg;
|
||||
auto result =
|
||||
TestUtils::recv(socket,
|
||||
folly::MutableByteRange((uint8_t *)&msg.bytes_, sizeof(msg.bytes_) + sizeof(msg.crc32c_)),
|
||||
poll);
|
||||
RETURN_ON_ERROR(result);
|
||||
msg.data_.resize(msg.bytes_);
|
||||
result = TestUtils::recv(socket, folly::MutableByteRange(msg.data_.data(), msg.data_.size()), poll);
|
||||
RETURN_ON_ERROR(result);
|
||||
|
||||
uint32_t crc32c = folly::crc32c(msg.data_.data(), msg.data_.size(), 0);
|
||||
EXPECT_EQ(msg.crc32c_, crc32c);
|
||||
if (msg.crc32c_ != crc32c) {
|
||||
return makeError(StatusCode::kDataCorruption);
|
||||
}
|
||||
return Void{};
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t bytes_ = 0;
|
||||
uint32_t crc32c_ = 0;
|
||||
std::vector<uint8_t> data_;
|
||||
};
|
||||
|
||||
class TestIBSocket : public SetupIB {
|
||||
public:
|
||||
auto &group() { return *server_.groups().front(); }
|
||||
const auto &group() const { return *server_.groups().front(); }
|
||||
Address serverAddr() const { return group().addressList().front(); }
|
||||
|
||||
void SetUp() override {
|
||||
ASSERT_TRUE(server_.setup());
|
||||
auto accept = [&](auto socket) { accepted_.lock()->push(std::move(socket)); };
|
||||
auto service = std::make_unique<IBConnectService>(serverConfig_.groups(0).io_worker().ibsocket(), accept, []() {
|
||||
return 5_s;
|
||||
});
|
||||
group().addSerdeService(std::move(service), Address::Type::TCP);
|
||||
ASSERT_TRUE(server_.start());
|
||||
client_.start();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
client_.stopAndJoin();
|
||||
server_.stopAndJoin();
|
||||
}
|
||||
|
||||
CoTryTask<std::pair<std::unique_ptr<IBSocket>, std::unique_ptr<IBSocket>>> connect(
|
||||
IBSocket::Config &cfg,
|
||||
std::chrono::milliseconds timeout = 220ms) {
|
||||
// cfg.set_gid_index(FLAGS_gid_index);
|
||||
auto addr = serverAddr();
|
||||
Address tcpAddr(addr.ip, addr.port, Address::TCP);
|
||||
auto ctx = client_.serdeCtx(tcpAddr);
|
||||
|
||||
auto clientSocket = std::make_unique<IBSocket>(cfg);
|
||||
auto result = co_await clientSocket->connect(ctx, Duration{timeout});
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
|
||||
auto queue = accepted_.lock();
|
||||
EXPECT_EQ(queue->size(), 1);
|
||||
auto serverSocket = std::move(queue->front());
|
||||
queue->pop();
|
||||
queue.unlock();
|
||||
|
||||
EXPECT_FALSE(serverSocket->check().hasError());
|
||||
EXPECT_FALSE(clientSocket->check().hasError());
|
||||
|
||||
auto serverReady = TestUtils::wait(*serverSocket, Socket::kEventWritableFlag, 500ms);
|
||||
auto clientReady = TestUtils::wait(*clientSocket, Socket::kEventWritableFlag, 500ms);
|
||||
EXPECT_FALSE(serverReady.hasError()) << serverReady.error().describe();
|
||||
EXPECT_FALSE(clientReady.hasError()) << serverReady.error().describe();
|
||||
CO_RETURN_ON_ERROR(serverReady);
|
||||
CO_RETURN_ON_ERROR(clientReady);
|
||||
|
||||
co_return std::pair(std::move(serverSocket), std::move(clientSocket));
|
||||
}
|
||||
|
||||
Result<std::pair<std::unique_ptr<IBSocket>, std::unique_ptr<IBSocket>>> connectSync(
|
||||
IBSocket::Config &cfg,
|
||||
std::chrono::milliseconds timeout = 100ms) {
|
||||
return folly::coro::blockingWait(connect(cfg, timeout));
|
||||
}
|
||||
|
||||
protected:
|
||||
Client::Config clientConfig_;
|
||||
Client client_{clientConfig_};
|
||||
|
||||
Server::Config serverConfig_;
|
||||
bool initServerConfig = [this] {
|
||||
serverConfig_.groups(0).set_network_type(Address::TCP);
|
||||
serverConfig_.groups(0).listener().set_reuse_port(true);
|
||||
return true;
|
||||
}();
|
||||
Server server_{serverConfig_};
|
||||
folly::Synchronized<std::queue<std::unique_ptr<IBSocket>>, std::mutex> accepted_;
|
||||
};
|
||||
|
||||
TEST_F(TestIBSocket, InvalidConfig) {
|
||||
auto dev = IBDevice::get(0);
|
||||
ASSERT_NE(dev, nullptr);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
IBSocket::Config cfg;
|
||||
cfg.set_max_rd_atomic(dev->attr().max_qp_rd_atom + 1);
|
||||
|
||||
auto result = co_await connect(cfg);
|
||||
CO_ASSERT_ERROR(result, StatusCode::kInvalidConfig);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, ConnectTimeout) {
|
||||
server_.stopAndJoin();
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = co_await connect(config);
|
||||
CO_ASSERT_ERROR(result, RPCCode::kSendFailed);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, Connect) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = co_await connect(config);
|
||||
CO_ASSERT_OK(result);
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, ClientToServer) {
|
||||
static constexpr int msgs = 100;
|
||||
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
|
||||
std::jthread server([socket = std::move(result->first)]() {
|
||||
while (true) {
|
||||
auto msg = TestMsg::recv(*socket);
|
||||
if (msg.hasError()) {
|
||||
ASSERT_ERROR(msg, RPCCode::kSocketClosed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
std::jthread client([socket = std::move(result->second)]() mutable {
|
||||
for (int i = 0; i < msgs; i++) {
|
||||
ASSERT_OK(TestMsg::send(*socket, folly::Random::rand32(4, 2 << 20)));
|
||||
}
|
||||
IBManager::close(std::move(socket));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, ServerToClient) {
|
||||
static constexpr int msgs = 100;
|
||||
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
|
||||
std::jthread server([socket = std::move(result->first)]() mutable {
|
||||
for (int i = 0; i < msgs; i++) {
|
||||
ASSERT_OK(TestMsg::send(*socket, folly::Random::rand32(4, 2 << 20)));
|
||||
}
|
||||
IBManager::close(std::move(socket));
|
||||
});
|
||||
std::jthread clntTh([socket = std::move(result->second)]() {
|
||||
for ([[maybe_unused]] int i = 0; true; i++) {
|
||||
auto msg = TestMsg::recv(*socket);
|
||||
if (msg.hasError()) {
|
||||
ASSERT_ERROR(msg, RPCCode::kSocketClosed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, BIDirection) {
|
||||
static constexpr int msgs = 100;
|
||||
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
SCOPE_EXIT {
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
};
|
||||
|
||||
std::atomic<uint32_t> cnt(0);
|
||||
auto send = [&](IBSocket &socket) {
|
||||
SCOPE_EXIT { cnt++; };
|
||||
for (int i = 0; i < msgs; i++) {
|
||||
ASSERT_OK(TestMsg::send(socket, folly::Random::rand32(4, 2 << 20), false));
|
||||
}
|
||||
};
|
||||
auto recv = [&](IBSocket &socket) {
|
||||
SCOPE_EXIT { cnt++; };
|
||||
for (int i = 0; i < msgs; i++) {
|
||||
ASSERT_OK(TestMsg::recv(socket, false));
|
||||
}
|
||||
};
|
||||
auto poll = [&](IBSocket &socket) {
|
||||
while (cnt < 4) {
|
||||
auto result = socket.poll(0);
|
||||
if (result.hasError()) {
|
||||
ASSERT_ERROR(result, RPCCode::kSocketClosed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::jthread server_send([&]() { send(*result->first); });
|
||||
std::jthread server_recv([&]() { recv(*result->first); });
|
||||
std::jthread server_poll([&]() { poll(*result->first); });
|
||||
std::jthread client_send([&]() { send(*result->second); });
|
||||
std::jthread client_recv([&]() { recv(*result->second); });
|
||||
std::jthread client_poll([&]() { poll(*result->second); });
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, BIDirectionFaultInjection) {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
SCOPE_EXIT {
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
};
|
||||
|
||||
std::atomic<uint32_t> cnt(0);
|
||||
|
||||
auto inject = [&](IBSocket &socket) {
|
||||
SCOPE_EXIT { cnt++; };
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
// try to inject a error by post a invalid RDMA operation, shouldn't receive corruption message.
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(1000));
|
||||
// underlying RDMABuf is deregistered immediately, so remote buffer is invalid
|
||||
auto remote = RDMABuf::allocate(1024).toRemoteBuf();
|
||||
auto local = RDMABuf::allocate(1024);
|
||||
auto batch = socket.rdmaWriteBatch();
|
||||
batch.add(remote, local);
|
||||
auto result = co_await batch.post();
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
}());
|
||||
};
|
||||
auto send = [&](IBSocket &socket) {
|
||||
SCOPE_EXIT { cnt++; };
|
||||
while (true) {
|
||||
auto result = TestMsg::send(socket, folly::Random::rand32(4, 2 << 20), false);
|
||||
if (result.hasError()) {
|
||||
ASSERT_ERROR(result, RPCCode::kSocketError);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
auto recv = [&](IBSocket &socket) {
|
||||
SCOPE_EXIT { cnt++; };
|
||||
while (true) {
|
||||
auto result = TestMsg::recv(socket, false);
|
||||
if (result.hasError()) {
|
||||
ASSERT_ERROR(result, RPCCode::kSocketError);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
auto poll = [&](IBSocket &socket) {
|
||||
while (cnt < 5) {
|
||||
auto result = socket.poll(0);
|
||||
if (result.hasError()) {
|
||||
bool closed = result.error().code() == RPCCode::kSocketClosed;
|
||||
bool error = result.error().code() == RPCCode::kSocketError;
|
||||
ASSERT_TRUE(closed || error) << result.error().describe();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::jthread inject_th([&]() { inject(*result->first); });
|
||||
std::jthread server_send([&]() { send(*result->first); });
|
||||
std::jthread server_recv([&]() { recv(*result->first); });
|
||||
std::jthread server_poll([&]() { poll(*result->first); });
|
||||
std::jthread client_send([&]() { send(*result->second); });
|
||||
std::jthread client_recv([&]() { recv(*result->second); });
|
||||
std::jthread client_poll([&]() { poll(*result->second); });
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, Benchmark) {
|
||||
size_t benchSize = FLAGS_ib_bench_mb * (1ULL << 20);
|
||||
auto config = IBSocket::Config();
|
||||
if (FLAGS_ib_bench_buf_kb) config.set_buf_size(FLAGS_ib_bench_buf_kb * 1024);
|
||||
if (FLAGS_ib_bench_buf_cnt) config.set_send_buf_cnt(FLAGS_ib_bench_buf_cnt);
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
|
||||
UtcTime begin = UtcClock::now();
|
||||
std::jthread server([benchSize, socket = std::move(result->first)]() {
|
||||
std::vector<uint8_t> buf(4 << 20, 0);
|
||||
size_t recved = 0;
|
||||
while (recved < benchSize) {
|
||||
size_t rsize = std::min(buf.size(), benchSize - recved);
|
||||
auto result = TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), rsize));
|
||||
ASSERT_OK(result);
|
||||
recved += rsize;
|
||||
}
|
||||
});
|
||||
std::jthread client([benchSize, socket = std::move(result->second)]() {
|
||||
std::vector<uint8_t> buf(4 << 20, 0);
|
||||
size_t sended = 0;
|
||||
while (sended < benchSize) {
|
||||
size_t wsize = std::min(buf.size(), benchSize - sended);
|
||||
auto result = TestUtils::send(*socket, folly::ByteRange(buf.data(), wsize));
|
||||
ASSERT_OK(result);
|
||||
sended += wsize;
|
||||
}
|
||||
});
|
||||
server.join();
|
||||
client.join();
|
||||
|
||||
auto dur = UtcClock::now() - begin;
|
||||
std::cout << "IBSocket benchmark bandwidth: "
|
||||
<< ((double)FLAGS_ib_bench_mb / std::chrono::duration_cast<std::chrono::milliseconds>(dur).count() * 1000.0)
|
||||
<< " MB/s" << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, PingPong) {
|
||||
size_t kLoops = FLAGS_ib_pingpong_loops;
|
||||
size_t kMsgSize = FLAGS_ib_pingpong_size;
|
||||
|
||||
auto config = IBSocket::Config();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
UtcTime begin = UtcClock::now();
|
||||
std::jthread server([kMsgSize, kLoops, socket = std::move(result->first)]() {
|
||||
std::vector<uint8_t> buf(kMsgSize, 0);
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
ASSERT_OK(TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), kMsgSize)));
|
||||
ASSERT_OK(TestUtils::send(*socket, folly::ByteRange(buf.data(), kMsgSize)));
|
||||
}
|
||||
});
|
||||
std::jthread client([kMsgSize, kLoops, socket = std::move(result->second)]() {
|
||||
std::vector<uint8_t> buf(kMsgSize, 0);
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
ASSERT_OK(TestUtils::send(*socket, folly::ByteRange(buf.data(), kMsgSize)));
|
||||
ASSERT_OK(TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), kMsgSize)));
|
||||
}
|
||||
});
|
||||
server.join();
|
||||
client.join();
|
||||
|
||||
auto dur = UtcClock::now() - begin;
|
||||
std::cout << "IBSocket pingpong latency: "
|
||||
<< std::chrono::duration_cast<std::chrono::nanoseconds>(dur).count() / 1000.0 / kLoops << "us" << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, RDMAWrite) {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
SCOPE_EXIT {
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
};
|
||||
|
||||
std::atomic<bool> stop = false;
|
||||
auto jthreads = TestUtils::poll({result->first.get(), result->second.get()}, stop);
|
||||
|
||||
folly::coro::blockingWait([&, &socket = *result->first]() -> CoTask<void> {
|
||||
auto pool = RDMABufPool::create(4 << 10, 10);
|
||||
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
|
||||
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
|
||||
// RDMABufPool
|
||||
auto src = co_await pool->allocate();
|
||||
auto dst = co_await pool->allocate();
|
||||
CO_ASSERT_TRUE(src);
|
||||
CO_ASSERT_TRUE(dst);
|
||||
|
||||
for (size_t i = 0; i < src.size(); i++) {
|
||||
((uint8_t *)src.ptr())[i] = folly::Random::rand32();
|
||||
}
|
||||
auto crc32c = folly::crc32c((uint8_t *)src.ptr(), src.size());
|
||||
|
||||
auto dstRemote = dst.toRemoteBuf();
|
||||
CO_ASSERT_TRUE(dstRemote);
|
||||
|
||||
auto result = co_await socket.rdmaWrite(dstRemote, src);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(folly::crc32c((uint8_t *)dst.ptr(), dst.size()), crc32c);
|
||||
}
|
||||
|
||||
auto remotePool = RDMABufPool::create(4096 * 16, -1);
|
||||
auto localPool = RDMABufPool::create(4096, -1);
|
||||
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
|
||||
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
|
||||
std::vector<RDMABuf> remotes;
|
||||
std::vector<RDMABuf> locals;
|
||||
auto batch = socket.rdmaWriteBatch();
|
||||
int numReqs = folly::Random::rand32(1, 64);
|
||||
for (int i = 0; i < numReqs; i++) {
|
||||
size_t total = 0;
|
||||
int numLocalBufs = folly::Random::rand32(1, 16);
|
||||
for (int j = 0; j < numLocalBufs; j++) {
|
||||
auto size = folly::Random::rand32(4, 4096);
|
||||
auto local = co_await localPool->allocate();
|
||||
local = local.first(size);
|
||||
CO_ASSERT_TRUE(local);
|
||||
locals.push_back(local);
|
||||
total += size;
|
||||
}
|
||||
auto remote = co_await remotePool->allocate();
|
||||
remote = remote.first(total);
|
||||
CO_ASSERT_TRUE(remote);
|
||||
remotes.push_back(remote);
|
||||
|
||||
batch.add(remote.toRemoteBuf(), std::span(locals).last(numLocalBufs));
|
||||
}
|
||||
|
||||
auto crc32c = 0;
|
||||
for (auto buf : locals) {
|
||||
for (size_t i = 0; i < buf.size(); i++) {
|
||||
((uint8_t *)buf.ptr())[i] = folly::Random::rand32();
|
||||
}
|
||||
crc32c = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32c);
|
||||
}
|
||||
|
||||
auto result = co_await batch.post();
|
||||
CO_ASSERT_OK(result);
|
||||
|
||||
auto crc32cOut = 0;
|
||||
for (auto buf : remotes) {
|
||||
crc32cOut = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32cOut);
|
||||
}
|
||||
CO_ASSERT_EQ(crc32c, crc32cOut);
|
||||
}
|
||||
|
||||
stop = true;
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, RDMARead) {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
SCOPE_EXIT {
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
};
|
||||
|
||||
std::atomic<bool> stop = false;
|
||||
auto jthreads = TestUtils::poll({result->first.get(), result->second.get()}, stop);
|
||||
|
||||
folly::coro::blockingWait([&, &socket = *result->first]() -> CoTask<void> {
|
||||
auto pool = RDMABufPool::create(4 << 10, 10);
|
||||
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
|
||||
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
|
||||
// RDMABufPool
|
||||
auto src = co_await pool->allocate();
|
||||
auto dst = co_await pool->allocate();
|
||||
CO_ASSERT_TRUE(src);
|
||||
CO_ASSERT_TRUE(dst);
|
||||
|
||||
for (size_t i = 0; i < src.size(); i++) {
|
||||
((uint8_t *)src.ptr())[i] = folly::Random::rand32();
|
||||
}
|
||||
auto crc32c = folly::crc32c((uint8_t *)src.ptr(), src.size());
|
||||
|
||||
auto srcRemote = src.toRemoteBuf();
|
||||
CO_ASSERT_TRUE(srcRemote);
|
||||
|
||||
auto result = co_await socket.rdmaRead(srcRemote, dst);
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(folly::crc32c((uint8_t *)dst.ptr(), dst.size()), crc32c);
|
||||
}
|
||||
|
||||
auto remotePool = RDMABufPool::create(4096 * 16, -1);
|
||||
auto localPool = RDMABufPool::create(4096, -1);
|
||||
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
|
||||
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
|
||||
std::vector<RDMABuf> remotes;
|
||||
std::vector<RDMABuf> locals;
|
||||
auto batch = socket.rdmaReadBatch();
|
||||
int numReqs = folly::Random::rand32(1, 64);
|
||||
for (int i = 0; i < numReqs; i++) {
|
||||
size_t total = 0;
|
||||
int numLocalBufs = folly::Random::rand32(1, 16);
|
||||
for (int j = 0; j < numLocalBufs; j++) {
|
||||
auto size = folly::Random::rand32(4, 4 << 10);
|
||||
auto local = co_await localPool->allocate();
|
||||
local = local.first(size);
|
||||
CO_ASSERT_TRUE(local);
|
||||
locals.push_back(local);
|
||||
total += size;
|
||||
}
|
||||
auto remote = co_await remotePool->allocate();
|
||||
remote = remote.first(total);
|
||||
CO_ASSERT_TRUE(remote);
|
||||
remotes.push_back(remote);
|
||||
|
||||
batch.add(remote.toRemoteBuf(), std::span(locals).last(numLocalBufs));
|
||||
}
|
||||
|
||||
auto crc32c = 0;
|
||||
for (auto buf : remotes) {
|
||||
for (size_t i = 0; i < buf.size(); i++) {
|
||||
((uint8_t *)buf.ptr())[i] = folly::Random::rand32();
|
||||
}
|
||||
crc32c = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32c);
|
||||
}
|
||||
|
||||
auto result = co_await batch.post();
|
||||
CO_ASSERT_OK(result);
|
||||
|
||||
auto crc32cOut = 0;
|
||||
for (auto buf : locals) {
|
||||
crc32cOut = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32cOut);
|
||||
}
|
||||
CO_ASSERT_EQ(crc32c, crc32cOut);
|
||||
}
|
||||
|
||||
stop = true;
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, RemoteClosed) {
|
||||
auto config = IBSocket::Config();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
|
||||
boost::barrier barrier(2);
|
||||
std::jthread server([socket = std::move(result->first)]() mutable {
|
||||
ASSERT_OK(TestMsg::send(*socket, 4 << 20));
|
||||
IBManager::close(std::move(socket));
|
||||
});
|
||||
std::jthread client([socket = std::move(result->second)]() mutable {
|
||||
ASSERT_OK(TestMsg::recv(*socket));
|
||||
ASSERT_ERROR(TestMsg::recv(*socket), RPCCode::kSocketClosed);
|
||||
ASSERT_ERROR(TestMsg::send(*socket, 1 << 20), RPCCode::kSocketClosed);
|
||||
ASSERT_ERROR(TestMsg::send(*socket, 1 << 20), RPCCode::kSocketClosed);
|
||||
IBManager::close(std::move(socket));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, RDMAFailure) {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto result = connectSync(config);
|
||||
ASSERT_OK(result);
|
||||
SCOPE_EXIT {
|
||||
IBManager::close(std::move(result->first));
|
||||
IBManager::close(std::move(result->second));
|
||||
};
|
||||
|
||||
// connection is OK
|
||||
{
|
||||
auto &client = *result->first;
|
||||
auto deadline = RelativeTime::now() + 5_s;
|
||||
while (RelativeTime::now() < deadline) {
|
||||
ASSERT_OK(client.check());
|
||||
auto result = client.poll(0);
|
||||
ASSERT_OK(result);
|
||||
}
|
||||
}
|
||||
|
||||
// post invalid RDMA request on server side to make
|
||||
std::atomic<bool> stop = false;
|
||||
auto jthreads = TestUtils::poll({result->second.get()}, stop);
|
||||
folly::coro::blockingWait([&, &server = *result->second]() -> CoTask<void> {
|
||||
// post RDMA with invalid rkey
|
||||
// underlying RDMABuf is deregistered immediately, so remote buffer is invalid
|
||||
auto remote = RDMABuf::allocate(1024).toRemoteBuf();
|
||||
auto local = RDMABuf::allocate(1024);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
auto batch = server.rdmaWriteBatch();
|
||||
batch.add(remote, local);
|
||||
batch.add(remote, local);
|
||||
batch.add(remote, local);
|
||||
batch.add(remote, local);
|
||||
auto result = co_await batch.post();
|
||||
CO_ASSERT_TRUE(result.hasError());
|
||||
std::cout << result.error().describe() << std::endl;
|
||||
}
|
||||
stop = true;
|
||||
}());
|
||||
|
||||
// connection is broken, send check message on client side
|
||||
auto &client = *result->first;
|
||||
auto deadline = RelativeTime::now() + 10_s;
|
||||
auto error = false;
|
||||
while (RelativeTime::now() < deadline) {
|
||||
ASSERT_OK(client.check());
|
||||
auto result = client.poll(0);
|
||||
if (result.hasError()) {
|
||||
error = true;
|
||||
fmt::print("{}\n", result.error());
|
||||
break;
|
||||
} else {
|
||||
fmt::print(".");
|
||||
}
|
||||
std::this_thread::sleep_for(1_s);
|
||||
}
|
||||
ASSERT_TRUE(error) << "client side doesn't get error after send check message";
|
||||
}
|
||||
|
||||
TEST_F(TestIBSocket, CloseBeforeConnect) {
|
||||
auto config = TestUtils::testCfg();
|
||||
auto addr = serverAddr();
|
||||
Address tcpAddr(addr.ip, addr.port, Address::TCP);
|
||||
auto ctx = client_.serdeCtx(tcpAddr);
|
||||
|
||||
auto socket = std::make_unique<IBSocket>(config);
|
||||
folly::coro::blockingWait(socket->close());
|
||||
ASSERT_ERROR(folly::coro::blockingWait(socket->connect(ctx, 1_s)), RPCCode::kSocketClosed);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net::test
|
||||
405
tests/common/net/ib/TestRDMA.cc
Normal file
405
tests/common/net/ib/TestRDMA.cc
Normal file
@@ -0,0 +1,405 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <folly/Executor.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/IOThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <folly/logging/LogConfig.h>
|
||||
#include <folly/logging/LogConfigParser.h>
|
||||
#include <folly/logging/Logger.h>
|
||||
#include <folly/logging/LoggerDB.h>
|
||||
#include <folly/logging/ObjectToString.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/Listener.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/net/ib/IBConnectService.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/net/sync/Client.h"
|
||||
#include "common/serde/ClientContext.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/serde/Service.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/common/net/ib/SetupIB.h"
|
||||
|
||||
DEFINE_bool(test_rdma_disable_log, false, "disable in TestRDMA");
|
||||
DEFINE_double(test_rdma_fault_injection_rate, 0.001, "fault injection rate in TestRDMA");
|
||||
DEFINE_int32(test_rdma_connections, 4, "connections in TestRDMA");
|
||||
DEFINE_int32(test_rdma_pkey_index, 0, "pkey_index in TestRDMA");
|
||||
namespace hf3fs::net {
|
||||
|
||||
struct RDMAReq {
|
||||
SERDE_STRUCT_FIELD(bufs, std::vector<RDMARemoteBuf>{});
|
||||
SERDE_STRUCT_FIELD(val, uint64_t(0));
|
||||
};
|
||||
|
||||
struct RDMARsp {
|
||||
SERDE_STRUCT_FIELD(dummy, Void());
|
||||
};
|
||||
|
||||
SERDE_SERVICE(RDMAService, 87) { SERDE_SERVICE_METHOD(test, 1, RDMAReq, RDMARsp); };
|
||||
|
||||
class RDMAServiceImpl : public serde::ServiceWrapper<RDMAServiceImpl, RDMAService> {
|
||||
public:
|
||||
RDMAServiceImpl()
|
||||
: bufPool_() {
|
||||
bufPool_ = net::RDMABufPool::create(1 << 20, 1024);
|
||||
}
|
||||
|
||||
CoTryTask<RDMARsp> test(serde::CallContext &ctx, const RDMAReq &req) {
|
||||
RDMARsp rsp;
|
||||
auto *ibsocket = ctx.transport()->ibSocket();
|
||||
XLOGF_IF(FATAL, ibsocket == nullptr, "Not IBSocket");
|
||||
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(folly::Random::rand32(100)));
|
||||
if (!req.bufs.empty()) {
|
||||
auto buf = co_await bufPool_->allocate();
|
||||
if (!buf) {
|
||||
co_return makeError(RPCCode::kRDMANoBuf);
|
||||
}
|
||||
auto batch = ibsocket->rdmaWriteBatch();
|
||||
for (auto remote : req.bufs) {
|
||||
if (buf.size() < remote.size()) {
|
||||
auto result = co_await batch.post();
|
||||
if (result.hasError()) {
|
||||
co_return makeError(std::move(result.error()));
|
||||
}
|
||||
batch.clear();
|
||||
buf.resetRange();
|
||||
}
|
||||
auto subbuf = buf.takeFirst(remote.size());
|
||||
if (!subbuf) {
|
||||
EXPECT_TRUE(false);
|
||||
co_return makeError(StatusCode::kInvalidArg);
|
||||
}
|
||||
XLOGF_IF(FATAL, req.val == 0, "here");
|
||||
*(uint64_t *)subbuf.ptr() = req.val;
|
||||
if (subbuf.size() >= 16) {
|
||||
*(uint64_t *)(subbuf.ptr() + subbuf.size() - 8) = req.val;
|
||||
}
|
||||
batch.add(remote, subbuf);
|
||||
}
|
||||
auto result = co_await batch.post();
|
||||
if (result.hasError()) {
|
||||
co_return makeError(std::move(result.error()));
|
||||
}
|
||||
}
|
||||
co_return rsp;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<net::RDMABufPool> bufPool_;
|
||||
};
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class TestRDMA : public test::SetupIB {
|
||||
public:
|
||||
void SetUp() override {
|
||||
auto &logger = folly::LoggerDB::get();
|
||||
if (FLAGS_test_rdma_disable_log) {
|
||||
oldLogConfig_ = logger.getConfig();
|
||||
logger.updateConfig(folly::parseLogConfig("CRITICAL"));
|
||||
}
|
||||
|
||||
server_ = std::make_unique<Server>(serverConfig_);
|
||||
server_->addSerdeService(std::make_unique<RDMAServiceImpl>());
|
||||
ASSERT_TRUE(server_->setup());
|
||||
ASSERT_TRUE(server_->start());
|
||||
ASSERT_TRUE(client_.start());
|
||||
|
||||
clientConfig_.io_worker().ibsocket().set_pkey_index(FLAGS_test_rdma_pkey_index);
|
||||
}
|
||||
void TearDown() override {
|
||||
if (FLAGS_test_rdma_disable_log) {
|
||||
folly::LoggerDB::get().updateConfig(oldLogConfig_);
|
||||
}
|
||||
client_.stopAndJoin();
|
||||
if (server_) {
|
||||
server_->stopAndJoin();
|
||||
server_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
Address serverAddr() const { return server_->groups().front()->addressList().front(); }
|
||||
|
||||
void serverRestart() {
|
||||
if (server_) {
|
||||
serverConfig_.groups(0).listener().set_listen_port(serverAddr().port);
|
||||
server_->stopAndJoin();
|
||||
corpse_.push_back(std::move(server_));
|
||||
}
|
||||
server_ = std::make_unique<Server>(serverConfig_);
|
||||
server_->addSerdeService(std::make_unique<RDMAServiceImpl>());
|
||||
ASSERT_TRUE(server_->setup());
|
||||
ASSERT_TRUE(server_->start());
|
||||
}
|
||||
|
||||
void serverDropConnections() {
|
||||
for (auto &grp : server_->groups()) {
|
||||
grp->ioWorker().dropConnections(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Client::Config clientConfig_;
|
||||
bool initClientConfig = [this] {
|
||||
clientConfig_.io_worker().transport_pool().set_max_connections(FLAGS_test_rdma_connections);
|
||||
clientConfig_.io_worker().ibsocket().set_max_rdma_wr(16);
|
||||
clientConfig_.io_worker().ibsocket().set_max_rdma_wr_per_post(4);
|
||||
clientConfig_.io_worker().set_num_event_loop(2);
|
||||
clientConfig_.thread_pool().set_num_connect_threads(2);
|
||||
clientConfig_.thread_pool().set_num_io_threads(4);
|
||||
clientConfig_.thread_pool().set_num_proc_threads(4);
|
||||
clientConfig_.set_default_timeout(2_s);
|
||||
return true;
|
||||
}();
|
||||
Client client_{clientConfig_};
|
||||
|
||||
Server::Config serverConfig_;
|
||||
bool initServerConfig = [this] {
|
||||
serverConfig_.groups(0).set_network_type(Address::RDMA);
|
||||
serverConfig_.thread_pool().set_num_io_threads(4);
|
||||
serverConfig_.thread_pool().set_num_proc_threads(4);
|
||||
return true;
|
||||
}();
|
||||
std::unique_ptr<Server> server_;
|
||||
std::vector<std::unique_ptr<Server>> corpse_;
|
||||
|
||||
folly::LogConfig oldLogConfig_;
|
||||
};
|
||||
|
||||
CoTask<void> runClient(RDMAService<> client,
|
||||
serde::ClientContext ctx,
|
||||
size_t bufSize,
|
||||
size_t bufCnt,
|
||||
bool injectFault,
|
||||
bool allowError,
|
||||
Duration dur,
|
||||
net::UserRequestOptions opts = {}) {
|
||||
auto invalidMR = net::RDMABuf::allocate(bufSize).toRemoteBuf();
|
||||
auto buf = net::RDMABuf::allocate(bufSize * bufCnt);
|
||||
CO_ASSERT_TRUE(buf);
|
||||
std::vector<net::RDMABuf> rdmaBufs;
|
||||
for (size_t i = 0; i < bufCnt; i++) {
|
||||
auto subBuf = buf.takeFirst(bufSize);
|
||||
CO_ASSERT_TRUE(subBuf);
|
||||
rdmaBufs.push_back(subBuf);
|
||||
}
|
||||
auto begin = SteadyClock::now();
|
||||
do {
|
||||
// call RPCs here
|
||||
auto val = folly::Random::rand64();
|
||||
RDMAReq req;
|
||||
req.val = val;
|
||||
|
||||
XLOGF(DBG, "request {}", req.val);
|
||||
|
||||
// use random number of buf
|
||||
size_t numBuf = folly::Random::rand32(rdmaBufs.size());
|
||||
for (size_t bufIdx = 0; bufIdx < numBuf; bufIdx++) {
|
||||
// buf with random length
|
||||
auto buf = rdmaBufs.at(bufIdx);
|
||||
auto len = folly::Random::rand32(8, buf.size() + 1);
|
||||
auto subbuf = buf.first(len);
|
||||
CO_ASSERT_TRUE(subbuf);
|
||||
*(uint64_t *)subbuf.ptr() = 0;
|
||||
if (subbuf.size() >= 16) {
|
||||
*(uint64_t *)(subbuf.ptr() + subbuf.size() - 8) = 0;
|
||||
}
|
||||
auto mr = subbuf.toRemoteBuf();
|
||||
if (injectFault && folly::Random::randDouble01() < FLAGS_test_rdma_fault_injection_rate) {
|
||||
mr = invalidMR.first(mr.size());
|
||||
}
|
||||
req.bufs.push_back(mr);
|
||||
}
|
||||
std::shuffle(req.bufs.begin(), req.bufs.end(), std::mt19937());
|
||||
|
||||
auto result = co_await client.test(ctx, req, &opts);
|
||||
if (!allowError) {
|
||||
CO_ASSERT_OK(result);
|
||||
}
|
||||
if (!result.hasError()) {
|
||||
for (auto subbuf : req.bufs) {
|
||||
// check data
|
||||
CO_ASSERT_EQ(*(uint64_t *)subbuf.addr(), val);
|
||||
if (subbuf.size() >= 16) {
|
||||
CO_ASSERT_EQ(*(uint64_t *)(subbuf.addr() + subbuf.size() - 8), val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
XLOGF(DBG, "req {}, error {}", req.val, result.error());
|
||||
}
|
||||
} while (SteadyClock::now() - begin <= dur);
|
||||
}
|
||||
|
||||
CoTask<void> startMultiClient(folly::Executor::KeepAlive<> exec,
|
||||
size_t nClients,
|
||||
RDMAService<> client,
|
||||
serde::ClientContext ctx,
|
||||
size_t bufSize,
|
||||
size_t bufCnt,
|
||||
bool injectFault,
|
||||
bool allowError,
|
||||
Duration dur,
|
||||
net::UserRequestOptions opts = {}) {
|
||||
std::vector<folly::SemiFuture<Void>> tasks;
|
||||
for (size_t i = 0; i < nClients; i++) {
|
||||
tasks.push_back(
|
||||
runClient(client, ctx, bufSize, bufCnt, injectFault, allowError, dur, opts).scheduleOn(exec).start());
|
||||
}
|
||||
co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, Basic) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, false, 5_s);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, Concurrent) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 10_s);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, ShortTimeout) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
net::UserRequestOptions opt;
|
||||
opt.timeout = 100_ms;
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s, opt);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, ShortTimeoutConcurrent) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
net::UserRequestOptions opt;
|
||||
opt.timeout = 100_ms;
|
||||
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, true, 10_s, opt);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, FaultInject) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
co_await runClient(client, ctx, 512 << 10, 8, true, true, 5_s);
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s);
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, false, 5_s);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, FaultInjectConcurrent) {
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, true, true, 5_s);
|
||||
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, false, true, 5_s);
|
||||
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, false, false, 5_s);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, SlowConnect) {
|
||||
IBConnectService::delayMs() = 200;
|
||||
SCOPE_EXIT { IBConnectService::delayMs() = 0; };
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
net::UserRequestOptions opt;
|
||||
opt.timeout = 200_ms;
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s, opt);
|
||||
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, true, 10_s, opt);
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, ConnectitonLost) {
|
||||
IBConnectService::connectionLost() = true;
|
||||
SCOPE_EXIT { IBConnectService::connectionLost() = false; };
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
net::UserRequestOptions opt;
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, true, 100_ms, opt);
|
||||
co_await folly::coro::sleep(std::chrono::seconds(20));
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, ServerDropConnections) {
|
||||
auto opts = net::UserRequestOptions{};
|
||||
opts.sendRetryTimes = 0;
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
serverDropConnections();
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(200));
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
|
||||
serverDropConnections();
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(200));
|
||||
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(TestRDMA, ServerRestart) {
|
||||
auto opts = net::UserRequestOptions{};
|
||||
opts.sendRetryTimes = 0;
|
||||
auto ctx = client_.serdeCtx(serverAddr());
|
||||
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
RDMAService<> client;
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
serverRestart();
|
||||
co_await runClient(client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
|
||||
serverRestart();
|
||||
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net
|
||||
230
tests/common/net/ib/TestRDMABuf.cc
Normal file
230
tests/common/net/ib/TestRDMABuf.cc
Normal file
@@ -0,0 +1,230 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "SetupIB.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace hf3fs::net {
|
||||
|
||||
class TestRDMARemoteBuf : public test::SetupIB {};
|
||||
|
||||
TEST_F(TestRDMARemoteBuf, Basic) {
|
||||
RDMARemoteBuf buf;
|
||||
ASSERT_FALSE(buf);
|
||||
for (auto rkey : buf.rkeys_) {
|
||||
EXPECT_EQ(rkey, RDMARemoteBuf::Rkey());
|
||||
}
|
||||
|
||||
auto rdmaBuf = RDMABuf::allocate(4 << 20);
|
||||
ASSERT_TRUE(rdmaBuf);
|
||||
buf = rdmaBuf.toRemoteBuf();
|
||||
ASSERT_TRUE(buf);
|
||||
}
|
||||
|
||||
TEST_F(TestRDMARemoteBuf, Subrange) {
|
||||
auto rdmaBuf = RDMABuf::allocate(4 << 20);
|
||||
ASSERT_TRUE(rdmaBuf);
|
||||
const auto buf = rdmaBuf.toRemoteBuf();
|
||||
ASSERT_TRUE(buf);
|
||||
ASSERT_EQ(buf.size(), 4 << 20);
|
||||
|
||||
auto buf1 = buf;
|
||||
ASSERT_EQ(buf1, buf);
|
||||
ASSERT_EQ(buf1.advance(1 << 20), true);
|
||||
ASSERT_EQ(buf1.size(), 3 << 20);
|
||||
ASSERT_EQ(buf1.subtract(1 << 20), true);
|
||||
ASSERT_EQ(buf1.size(), 2 << 20);
|
||||
ASSERT_FALSE(buf1.advance(buf1.size() + 1));
|
||||
ASSERT_FALSE(buf1.subtract(buf1.size() + 1));
|
||||
|
||||
ASSERT_NE(buf, buf1);
|
||||
ASSERT_EQ(buf.subrange(1 << 20, 2 << 20), buf1);
|
||||
ASSERT_EQ(buf.subrange(2 << 20, 1 << 20), buf1.subrange(1 << 20, 1 << 20));
|
||||
|
||||
auto buf2 = buf;
|
||||
auto buf3 = buf2.first(1 << 20);
|
||||
auto buf4 = buf2.takeFirst(1 << 20);
|
||||
ASSERT_EQ(buf3, buf4);
|
||||
ASSERT_EQ(buf2, buf.subrange(1 << 20, 3 << 20));
|
||||
ASSERT_EQ(buf3.addr(), buf.addr());
|
||||
ASSERT_EQ(buf2.size(), 3 << 20);
|
||||
ASSERT_EQ(buf2.addr(), buf.addr() + (1 << 20));
|
||||
|
||||
auto buf5 = buf;
|
||||
auto buf6 = buf.last(1 << 20);
|
||||
auto buf7 = buf5.takeLast(1 << 20);
|
||||
ASSERT_EQ(buf6, buf7);
|
||||
ASSERT_EQ(buf6, buf.subrange(3 << 20, 1 << 20));
|
||||
ASSERT_EQ(buf5.size(), 3 << 20);
|
||||
ASSERT_EQ(buf5.addr(), buf.addr());
|
||||
|
||||
ASSERT_TRUE(buf.subrange(1 << 20, 3 << 20));
|
||||
ASSERT_FALSE(buf.subrange(4 << 20, 1));
|
||||
ASSERT_TRUE(buf.subrange(4 << 20, 0));
|
||||
ASSERT_TRUE(buf.first(buf.size()));
|
||||
ASSERT_TRUE(buf.last(buf.size()));
|
||||
ASSERT_FALSE(buf.first(buf.size() + 1));
|
||||
ASSERT_FALSE(buf.last(buf.size() + 1));
|
||||
|
||||
auto buf8 = buf;
|
||||
ASSERT_FALSE(buf8.advance(buf.size() + 1));
|
||||
ASSERT_FALSE(buf8.subtract(buf.size() + 1));
|
||||
}
|
||||
|
||||
class TestRDMABuf : public test::SetupIB {};
|
||||
|
||||
TEST_F(TestRDMABuf, Default) {
|
||||
RDMABuf buf;
|
||||
ASSERT_FALSE(buf);
|
||||
ASSERT_FALSE(buf.toRemoteBuf());
|
||||
ASSERT_FALSE(buf.toRemoteBuf().getRkey(0));
|
||||
|
||||
RDMABuf buf2 = buf;
|
||||
ASSERT_FALSE(buf2);
|
||||
}
|
||||
|
||||
TEST_F(TestRDMABuf, Allocate) {
|
||||
std::queue<RDMABuf> bufs;
|
||||
for (auto i = 0; i < 100; i++) {
|
||||
auto size = folly::Random::rand32(8, 8 << 20);
|
||||
auto buf = RDMABuf::allocate(size);
|
||||
ASSERT_TRUE(buf);
|
||||
ASSERT_NE(buf.ptr(), nullptr);
|
||||
ASSERT_EQ(buf.size(), size);
|
||||
bufs.push(buf);
|
||||
}
|
||||
|
||||
while (!bufs.empty()) {
|
||||
auto buf = std::move(bufs.front());
|
||||
bufs.pop();
|
||||
|
||||
std::map<int, RDMABufMR> map;
|
||||
for (auto &dev : IBDevice::all()) {
|
||||
auto mr = buf.getMR(dev->id());
|
||||
map[dev->id()] = mr;
|
||||
ASSERT_NE(mr, nullptr);
|
||||
}
|
||||
ASSERT_TRUE(buf.toRemoteBuf());
|
||||
// ASSERT_EQ(buf.reregMR(), 0);
|
||||
// for (auto &dev : IBDevice::all()) {
|
||||
// auto mr = buf.getMR(dev->id());
|
||||
// ASSERT_NE(mr, nullptr);
|
||||
// ASSERT_NE(mr, map[dev->id()]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestRDMABuf, Subrange) {
|
||||
const auto buf = RDMABuf::allocate(4 << 20);
|
||||
ASSERT_TRUE(buf);
|
||||
ASSERT_EQ(buf.size(), 4 << 20);
|
||||
ASSERT_EQ(buf.capacity(), buf.size());
|
||||
|
||||
auto buf1 = buf;
|
||||
ASSERT_EQ(buf1, buf);
|
||||
ASSERT_EQ(buf1.advance(1 << 20), true);
|
||||
ASSERT_EQ(buf1.capacity(), 4 << 20);
|
||||
ASSERT_EQ(buf1.size(), 3 << 20);
|
||||
ASSERT_EQ(buf1.subtract(1 << 20), true);
|
||||
ASSERT_EQ(buf1.capacity(), 4 << 20);
|
||||
ASSERT_EQ(buf1.size(), 2 << 20);
|
||||
ASSERT_FALSE(buf1.advance(buf1.size() + 1));
|
||||
ASSERT_FALSE(buf1.subtract(buf1.size() + 1));
|
||||
|
||||
ASSERT_NE(buf, buf1);
|
||||
ASSERT_EQ(buf.subrange(1 << 20, 2 << 20), buf1);
|
||||
ASSERT_EQ(buf.subrange(2 << 20, 1 << 20), buf1.subrange(1 << 20, 1 << 20));
|
||||
|
||||
auto buf2 = buf;
|
||||
auto buf3 = buf2.first(1 << 20);
|
||||
auto buf4 = buf2.takeFirst(1 << 20);
|
||||
ASSERT_EQ(buf3, buf4);
|
||||
ASSERT_EQ(buf2, buf.subrange(1 << 20, 3 << 20));
|
||||
ASSERT_EQ(buf3.ptr(), buf.ptr());
|
||||
ASSERT_EQ(buf2.size(), 3 << 20);
|
||||
ASSERT_EQ(buf2.ptr(), buf.ptr() + (1 << 20));
|
||||
|
||||
auto buf5 = buf;
|
||||
auto buf6 = buf.last(1 << 20);
|
||||
auto buf7 = buf5.takeLast(1 << 20);
|
||||
ASSERT_EQ(buf6, buf7);
|
||||
ASSERT_EQ(buf6, buf.subrange(3 << 20, 1 << 20));
|
||||
ASSERT_EQ(buf5.size(), 3 << 20);
|
||||
ASSERT_EQ(buf5.ptr(), buf.ptr());
|
||||
|
||||
ASSERT_TRUE(buf.subrange(1 << 20, 3 << 20));
|
||||
ASSERT_FALSE(buf.subrange(4 << 20, 1));
|
||||
ASSERT_TRUE(buf.subrange(4 << 20, 0));
|
||||
ASSERT_TRUE(buf.first(buf.size()));
|
||||
ASSERT_TRUE(buf.last(buf.size()));
|
||||
ASSERT_FALSE(buf.first(buf.size() + 1));
|
||||
ASSERT_FALSE(buf.last(buf.size() + 1));
|
||||
|
||||
auto buf8 = buf;
|
||||
ASSERT_FALSE(buf8.advance(buf.size() + 1));
|
||||
ASSERT_FALSE(buf8.subtract(buf.size() + 1));
|
||||
}
|
||||
|
||||
class TestRDMABufPool : public test::SetupIB {};
|
||||
|
||||
TEST_F(TestRDMABufPool, Basic) {
|
||||
constexpr size_t kBufSize = 4 << 10L;
|
||||
constexpr size_t kBufNum = 10;
|
||||
auto pool = RDMABufPool::create(kBufSize, kBufNum);
|
||||
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
std::queue<RDMABuf> bufs;
|
||||
for (size_t i = 0; i < kBufNum; i++) {
|
||||
auto buf = co_await pool->allocate();
|
||||
CO_ASSERT_TRUE(buf);
|
||||
bufs.push(buf);
|
||||
}
|
||||
|
||||
auto buf = co_await pool->allocate(std::chrono::milliseconds(10));
|
||||
CO_ASSERT_FALSE(buf);
|
||||
|
||||
auto firstPtr = bufs.front().ptr();
|
||||
std::atomic_bool allocated(false);
|
||||
auto firstBuf = bufs.front();
|
||||
bufs.pop();
|
||||
std::jthread proc([&allocated, first = std::move(firstBuf)]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
EXPECT_EQ(allocated.load(), false);
|
||||
});
|
||||
buf = co_await pool->allocate();
|
||||
CO_ASSERT_EQ(firstPtr, buf.ptr());
|
||||
bufs.push(buf);
|
||||
|
||||
while (!bufs.empty()) {
|
||||
auto buf = bufs.front();
|
||||
bufs.pop();
|
||||
CO_ASSERT_EQ(buf.capacity(), kBufSize);
|
||||
CO_ASSERT_NE(buf.ptr(), nullptr);
|
||||
std::map<int, RDMABufMR> map;
|
||||
for (auto &dev : IBDevice::all()) {
|
||||
auto mr = buf.getMR(dev->id());
|
||||
map[dev->id()] = mr;
|
||||
CO_ASSERT_NE(mr, nullptr);
|
||||
}
|
||||
buf.toRemoteBuf();
|
||||
// CO_ASSERT_EQ(buf.reregMR(), 0);
|
||||
// for (auto &dev : IBDevice::all()) {
|
||||
// auto mr = buf.getMR(dev->id());
|
||||
// CO_ASSERT_NE(mr, nullptr);
|
||||
// CO_ASSERT_NE(mr, map[dev->id()]);
|
||||
// }
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::net
|
||||
24
tests/common/serde/TestBigEndian.cc
Normal file
24
tests/common/serde/TestBigEndian.cc
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "common/serde/BigEndian.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::serde::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestSerde, BigEndian) {
|
||||
{
|
||||
using BigSize = BigEndian<std::size_t>;
|
||||
BigSize size{0x123456789ABCDEF0ul};
|
||||
auto out = serde::serialize(size);
|
||||
ASSERT_EQ(out.size(), sizeof(std::size_t));
|
||||
ASSERT_EQ(out.front(), 0x12);
|
||||
ASSERT_EQ(out.back(), (char)0xF0);
|
||||
ASSERT_EQ(serde::toJsonString(size), std::to_string(size));
|
||||
|
||||
ASSERT_OK(serde::fromJsonString(size, "233"));
|
||||
ASSERT_EQ(size, 233);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::serde::test
|
||||
34
tests/common/serde/TestMessagePacket.cc
Normal file
34
tests/common/serde/TestMessagePacket.cc
Normal file
@@ -0,0 +1,34 @@
|
||||
#include "common/serde/MessagePacket.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::serde::test {
|
||||
namespace {
|
||||
|
||||
struct Demo {
|
||||
bool operator==(const Demo &) const = default;
|
||||
|
||||
SERDE_STRUCT_FIELD(foo, int{}, nullptr);
|
||||
SERDE_STRUCT_FIELD(bar, std::string{}, nullptr);
|
||||
};
|
||||
|
||||
TEST(TestMessagePacket, Normal) {
|
||||
// send.
|
||||
Demo req;
|
||||
req.foo = 100;
|
||||
req.bar = "Good";
|
||||
MessagePacket send(req);
|
||||
auto bytes = serde::serialize(send);
|
||||
XLOGF(INFO, "send json: {}, binary: {:02X}", send, fmt::join(bytes, "-"));
|
||||
|
||||
// recv.
|
||||
MessagePacket recv;
|
||||
ASSERT_OK(serde::deserialize(recv, bytes));
|
||||
Demo rsp;
|
||||
ASSERT_OK(serde::deserialize(rsp, recv.payload));
|
||||
ASSERT_EQ(req, rsp);
|
||||
XLOGF(INFO, "rsp json: {}", rsp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::serde::test
|
||||
946
tests/common/serde/TestSerde.cc
Normal file
946
tests/common/serde/TestSerde.cc
Normal file
@@ -0,0 +1,946 @@
|
||||
#include <folly/json.h>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/DownwardBytes.h"
|
||||
#include "common/utils/Reflection.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/RobinHood.h"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "fbs/mgmtd/RoutingInfo.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
bool checkValue(float b) { return b > 10.0; }
|
||||
|
||||
struct CheckString {
|
||||
constexpr CheckString() = default;
|
||||
constexpr bool operator()(const std::string &str) const { return !str.empty(); }
|
||||
};
|
||||
|
||||
struct Demo {
|
||||
SERDE_CLASS_FIELD(a, short{10}, [](short a) { return a > 0; }); // checker is lambda expr.
|
||||
SERDE_CLASS_FIELD(b, 20.0f, checkValue); // checker is function pointer.
|
||||
SERDE_CLASS_FIELD(c, std::string{"ok"}, CheckString{}); // checker is function object.
|
||||
};
|
||||
static_assert(serde::count<Demo>() == 3);
|
||||
static_assert(serde::name<Demo, 0>() == "a");
|
||||
static_assert(serde::name<Demo, 1>() == "b");
|
||||
static_assert(serde::name<Demo, 2>() == "c");
|
||||
static_assert(serde::SerdeType<Demo>);
|
||||
static_assert(!serde::SerdeType<int>);
|
||||
|
||||
TEST(TestSerde, Normal) {
|
||||
const Demo demo;
|
||||
ASSERT_EQ(serde::name<0>(demo), "a");
|
||||
ASSERT_EQ(serde::name<1>(demo), "b");
|
||||
ASSERT_EQ(serde::name<2>(demo), "c");
|
||||
ASSERT_EQ(serde::value<0>(demo), 10);
|
||||
ASSERT_EQ(serde::value<1>(demo), 20.0);
|
||||
ASSERT_EQ(serde::value<2>(demo), "ok");
|
||||
ASSERT_EQ(&serde::value<0>(demo), &demo.a());
|
||||
ASSERT_EQ(&serde::value<1>(demo), &demo.b());
|
||||
ASSERT_EQ(&serde::value<2>(demo), &demo.c());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Serialize) {
|
||||
Demo demo;
|
||||
demo.a() = 100;
|
||||
demo.b() = 200.f;
|
||||
demo.c() = "hello";
|
||||
|
||||
const Demo &d = demo;
|
||||
auto out = serde::serialize(d);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(d));
|
||||
constexpr auto size = 1 + 2 + 4 + 1 + 5; // struct size + short + float + str length + str content.
|
||||
ASSERT_EQ(out.length(), size);
|
||||
|
||||
XLOGF(INFO, "out is {:02X}", fmt::join(out, ","));
|
||||
|
||||
Demo des;
|
||||
ASSERT_EQ(des.a(), 10);
|
||||
ASSERT_EQ(des.b(), 20.f);
|
||||
ASSERT_EQ(des.c(), "ok");
|
||||
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des.a(), 100);
|
||||
ASSERT_EQ(des.b(), 200.f);
|
||||
ASSERT_EQ(des.c(), "hello");
|
||||
}
|
||||
|
||||
struct Nested {
|
||||
SERDE_CLASS_FIELD(a, Demo{});
|
||||
SERDE_CLASS_FIELD(b, Demo{});
|
||||
};
|
||||
|
||||
TEST(TestSerde, Serialize2) {
|
||||
Nested nested;
|
||||
nested.b().a() = 100;
|
||||
nested.b().b() = 200.f;
|
||||
nested.b().c() = "hello";
|
||||
|
||||
auto out = serde::serialize(nested);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(nested));
|
||||
constexpr auto size = 1 + (1 + 2 + 4 + 1 + 2) + (1 + 2 + 4 + 1 + 5);
|
||||
ASSERT_EQ(out.length(), size);
|
||||
|
||||
Nested des;
|
||||
ASSERT_EQ(des.a().a(), des.b().a());
|
||||
ASSERT_EQ(des.a().b(), des.b().b());
|
||||
ASSERT_EQ(des.a().c(), des.b().c());
|
||||
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des.a().a(), 10);
|
||||
ASSERT_EQ(des.a().b(), 20.f);
|
||||
ASSERT_EQ(des.a().c(), "ok");
|
||||
ASSERT_EQ(des.b().a(), 100);
|
||||
ASSERT_EQ(des.b().b(), 200.f);
|
||||
ASSERT_EQ(des.b().c(), "hello");
|
||||
}
|
||||
|
||||
TEST(TestSerde, Vector) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, (std::vector<int>{0, 1, 2}));
|
||||
SERDE_CLASS_FIELD(b, (std::vector<std::string>{"x", "y"}));
|
||||
};
|
||||
|
||||
Test a;
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 4 * 3 + 1 + 2 * (1 + 1)); // struct size + vsize + int * 3 + vsize + 2 * (ssize + c)
|
||||
|
||||
a.a() = std::vector<int>{0, 1, 0, 1};
|
||||
a.b() = std::vector<std::string>{"a", "b", "c", "d"};
|
||||
out = serde::serialize(a);
|
||||
|
||||
Test b;
|
||||
ASSERT_NE(b.a(), a.a());
|
||||
ASSERT_NE(b.b(), a.b());
|
||||
ASSERT_OK(serde::deserialize(b, out));
|
||||
ASSERT_EQ(b.a(), a.a());
|
||||
ASSERT_EQ(b.b(), a.b());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Vector2) {
|
||||
struct Item {
|
||||
SERDE_STRUCT_FIELD(a, uint32_t{});
|
||||
SERDE_STRUCT_FIELD(b, std::string{});
|
||||
};
|
||||
struct Vector {
|
||||
SERDE_STRUCT_FIELD(vec, std::vector<Item>{});
|
||||
};
|
||||
|
||||
Vector vec;
|
||||
vec.vec.push_back({1, "one"});
|
||||
vec.vec.push_back({2, "two"});
|
||||
XLOGF(INFO, "vec is {}", vec);
|
||||
}
|
||||
|
||||
TEST(TestSerde, Vector3) {
|
||||
struct Item {
|
||||
SERDE_STRUCT_FIELD(a, std::vector<uint32_t>{});
|
||||
};
|
||||
|
||||
Item item;
|
||||
item.a.push_back(2);
|
||||
item.a.push_back(0);
|
||||
item.a.push_back(2);
|
||||
item.a.push_back(3);
|
||||
auto out = serde::serialize(item);
|
||||
|
||||
ASSERT_EQ(out.size(), 18);
|
||||
ASSERT_EQ(out[0], 17);
|
||||
ASSERT_EQ(out[1], 4);
|
||||
|
||||
Item des;
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(item.a, des.a);
|
||||
}
|
||||
|
||||
TEST(TestSerde, Set) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, (std::set<int>{0, 1, 2}));
|
||||
SERDE_CLASS_FIELD(b, (std::set<std::string>{"x", "y"}));
|
||||
};
|
||||
|
||||
Test a;
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 4 * 3 + 1 + 2 * (1 + 1));
|
||||
|
||||
a.a() = std::set<int>{0, 1, 0, 1};
|
||||
a.b() = std::set<std::string>{"a", "b", "c", "d"};
|
||||
out = serde::serialize(a);
|
||||
|
||||
Test b;
|
||||
ASSERT_NE(b.a(), a.a());
|
||||
ASSERT_NE(b.b(), a.b());
|
||||
ASSERT_OK(serde::deserialize(b, out));
|
||||
ASSERT_EQ(b.a(), a.a());
|
||||
ASSERT_EQ(b.b(), a.b());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Map) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, (std::map<int, int>{}));
|
||||
SERDE_CLASS_FIELD(b, (robin_hood::unordered_map<std::string, std::string>{}));
|
||||
};
|
||||
|
||||
Test a;
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 1);
|
||||
|
||||
a.a()[1] = 2;
|
||||
a.a()[3] = 4;
|
||||
a.b()["hello"] = "world";
|
||||
a.b()["nice to"] = "meet you";
|
||||
out = serde::serialize(a);
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 4 * 4 + 1 + 4 * 1 + 5 + 5 + 7 + 8);
|
||||
|
||||
Test b;
|
||||
ASSERT_NE(b.a(), a.a());
|
||||
ASSERT_NE(b.b(), a.b());
|
||||
ASSERT_OK(serde::deserialize(b, out));
|
||||
ASSERT_EQ(b.a(), a.a());
|
||||
ASSERT_EQ(b.b(), a.b());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Optional) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, std::optional<std::string>{});
|
||||
SERDE_CLASS_FIELD(b, std::optional<std::string>{"OK"});
|
||||
};
|
||||
|
||||
Test a;
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 1 + 1 + 2);
|
||||
|
||||
a.a() = "hello";
|
||||
a.b() = std::nullopt;
|
||||
out = serde::serialize(a);
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 1 + 5 + 1);
|
||||
|
||||
Test b;
|
||||
ASSERT_NE(b.a(), a.a());
|
||||
ASSERT_NE(b.b(), a.b());
|
||||
ASSERT_OK(serde::deserialize(b, out));
|
||||
ASSERT_EQ(b.a(), a.a());
|
||||
ASSERT_EQ(b.b(), a.b());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Variant) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, (std::variant<int, float, std::string>{}));
|
||||
};
|
||||
|
||||
Test a;
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 3 + 4);
|
||||
|
||||
a.a() = "hello";
|
||||
out = serde::serialize(a);
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 12 + 1 + 5);
|
||||
|
||||
Test b;
|
||||
ASSERT_NE(b.a(), a.a());
|
||||
ASSERT_OK(serde::deserialize(b, out));
|
||||
ASSERT_EQ(b.a(), a.a());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Compatibility) {
|
||||
struct Test {
|
||||
SERDE_CLASS_FIELD(a, std::optional<std::string>{});
|
||||
SERDE_CLASS_FIELD(b, std::vector<int>{});
|
||||
};
|
||||
|
||||
Test a;
|
||||
a.a() = "ok";
|
||||
a.b() = {1, 2, 3};
|
||||
auto out = serde::serialize(a);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(a));
|
||||
ASSERT_EQ(out.length(), 1 + 1 + 1 + 2 + 1 + 4 * 3);
|
||||
XLOGF(INFO, "out is {:02x}", fmt::join(out, ","));
|
||||
|
||||
size_t size = out.length();
|
||||
for (size_t i = 1; i <= size; ++i) {
|
||||
std::string_view view = out;
|
||||
*reinterpret_cast<uint8_t *>(out.data()) = i - 1;
|
||||
view = view.substr(0, i);
|
||||
|
||||
Test t;
|
||||
if (i == 1) {
|
||||
ASSERT_OK(serde::deserialize(t, view));
|
||||
ASSERT_FALSE(t.a().has_value());
|
||||
ASSERT_TRUE(t.b().empty());
|
||||
} else if (i == 5) {
|
||||
ASSERT_OK(serde::deserialize(t, view));
|
||||
ASSERT_TRUE(t.a().has_value());
|
||||
ASSERT_EQ(t.a().value(), "ok");
|
||||
ASSERT_TRUE(t.b().empty());
|
||||
} else if (i == size) {
|
||||
ASSERT_OK(serde::deserialize(t, view));
|
||||
ASSERT_TRUE(t.a().has_value());
|
||||
ASSERT_EQ(t.a().value(), "ok");
|
||||
ASSERT_FALSE(t.b().empty());
|
||||
} else {
|
||||
XLOGF(INFO, "length is {}", i);
|
||||
ASSERT_FALSE(serde::deserialize(a, view));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Base {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
SERDE_STRUCT_FIELD(y, int{});
|
||||
};
|
||||
|
||||
struct Derived : Base {
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
};
|
||||
|
||||
struct Grandson : Derived {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
SERDE_STRUCT_FIELD(y, int{});
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
};
|
||||
|
||||
namespace v1 {
|
||||
struct Base {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
};
|
||||
|
||||
struct Derived : Base {
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
};
|
||||
|
||||
struct Grandson : Derived {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
SERDE_STRUCT_FIELD(y, int{});
|
||||
};
|
||||
}; // namespace v1
|
||||
|
||||
namespace v3 {
|
||||
struct Base {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
SERDE_STRUCT_FIELD(y, int{});
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
};
|
||||
|
||||
struct Derived : Base {
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
SERDE_STRUCT_FIELD(a, int{});
|
||||
};
|
||||
|
||||
struct Grandson : Derived {
|
||||
SERDE_STRUCT_FIELD(x, int{});
|
||||
SERDE_STRUCT_FIELD(y, int{});
|
||||
SERDE_STRUCT_FIELD(z, int{});
|
||||
SERDE_STRUCT_FIELD(a, int{});
|
||||
};
|
||||
}; // namespace v3
|
||||
|
||||
static_assert(serde::count<Base>() == 2, "field count of Base must be 2");
|
||||
static_assert(serde::count<Derived>() == 3, "field count of Derived must be 3");
|
||||
static_assert(serde::count<Grandson>() == 6, "field count of Grandson must be 6");
|
||||
static_assert(serde::count<v1::Grandson>() == 4);
|
||||
|
||||
static_assert(serde::getter<Grandson, 0>() != serde::getter<Grandson, 3>());
|
||||
static_assert(serde::getter<Grandson, 1>() != serde::getter<Grandson, 4>());
|
||||
static_assert(serde::getter<Grandson, 2>() != serde::getter<Grandson, 5>());
|
||||
|
||||
static_assert(serde::name<Base, 0>() == "x");
|
||||
static_assert(serde::name<Base, 1>() == "y");
|
||||
|
||||
static_assert(serde::name<Derived, 0>() == "x");
|
||||
static_assert(serde::name<Derived, 1>() == "y");
|
||||
static_assert(serde::name<Derived, 2>() == "z");
|
||||
|
||||
static_assert(serde::name<Grandson, 0>() == "x");
|
||||
static_assert(serde::name<Grandson, 1>() == "y");
|
||||
static_assert(serde::name<Grandson, 2>() == "z");
|
||||
static_assert(serde::name<Grandson, 3>() == "x");
|
||||
static_assert(serde::name<Grandson, 4>() == "y");
|
||||
static_assert(serde::name<Grandson, 5>() == "z");
|
||||
|
||||
TEST(TestSerde, Inherit) {
|
||||
Grandson x;
|
||||
x.Base::x = 10;
|
||||
x.Base::y = 12;
|
||||
x.Derived::z = 16;
|
||||
x.Grandson::x = 20;
|
||||
x.Grandson::y = 24;
|
||||
x.Grandson::z = 28;
|
||||
ASSERT_EQ(serde::count(x), 6);
|
||||
ASSERT_EQ(serde::value<0>(x), 10);
|
||||
ASSERT_EQ(serde::value<3>(x), 20);
|
||||
|
||||
int typeCount = 1;
|
||||
refl::Helper::iterate<Grandson, true>([](auto t) { XLOGF(INFO, "field name: {}", t.name); },
|
||||
[&]() {
|
||||
++typeCount;
|
||||
XLOGF(INFO, "type changed");
|
||||
});
|
||||
ASSERT_EQ(typeCount, 3);
|
||||
|
||||
auto out = serde::serialize(x);
|
||||
Grandson o;
|
||||
ASSERT_OK(serde::deserialize(o, out));
|
||||
ASSERT_EQ(x.Base::x, o.Base::x);
|
||||
ASSERT_EQ(x.Base::y, o.Base::y);
|
||||
ASSERT_EQ(x.Derived::z, o.Derived::z);
|
||||
ASSERT_EQ(x.Grandson::x, o.Grandson::x);
|
||||
ASSERT_EQ(x.Grandson::y, o.Grandson::y);
|
||||
ASSERT_EQ(x.Grandson::z, o.Grandson::z);
|
||||
|
||||
v1::Grandson p;
|
||||
ASSERT_OK(serde::deserialize(p, out));
|
||||
ASSERT_EQ(x.Base::x, p.Base::x);
|
||||
ASSERT_EQ(x.Derived::z, p.Derived::z);
|
||||
ASSERT_EQ(x.Grandson::x, p.Grandson::x);
|
||||
ASSERT_EQ(x.Grandson::y, p.Grandson::y);
|
||||
|
||||
v3::Grandson q;
|
||||
ASSERT_OK(serde::deserialize(q, out));
|
||||
ASSERT_EQ(x.Base::x, q.Base::x);
|
||||
ASSERT_EQ(x.Base::y, q.Base::y);
|
||||
ASSERT_EQ(q.Base::z, 0);
|
||||
ASSERT_EQ(x.Derived::z, q.Derived::z);
|
||||
ASSERT_EQ(q.Derived::a, 0);
|
||||
ASSERT_EQ(x.Grandson::x, q.Grandson::x);
|
||||
ASSERT_EQ(x.Grandson::y, q.Grandson::y);
|
||||
ASSERT_EQ(x.Grandson::z, q.Grandson::z);
|
||||
ASSERT_EQ(q.Grandson::a, 0);
|
||||
}
|
||||
|
||||
struct WithStringView {
|
||||
SERDE_STRUCT_FIELD(msg, std::string_view{});
|
||||
SERDE_STRUCT_FIELD(test, std::string_view{});
|
||||
};
|
||||
|
||||
TEST(TestSerde, StringView) {
|
||||
WithStringView view;
|
||||
view.msg = "OK";
|
||||
view.test = "test";
|
||||
auto out = serde::serialize(view);
|
||||
ASSERT_EQ(out.length(), serde::serializeLength(view));
|
||||
|
||||
WithStringView des;
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(view.msg, des.msg);
|
||||
ASSERT_EQ(view.test, des.test);
|
||||
ASSERT_LT(out.data(), des.msg.data());
|
||||
ASSERT_LT(des.msg.data() + des.msg.length(), des.test.data());
|
||||
ASSERT_LE(des.test.data() + des.test.length(), out.data() + out.length());
|
||||
}
|
||||
|
||||
TEST(TestSerde, Result) {
|
||||
{
|
||||
Result<std::string> ser = "OK";
|
||||
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
|
||||
|
||||
auto out = serde::serialize(ser);
|
||||
ASSERT_EQ(out.size(), 1 + 1 + 2);
|
||||
|
||||
Result<std::string> des = makeError(Status::OK);
|
||||
ASSERT_FALSE(des);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_OK(des);
|
||||
ASSERT_EQ(des, ser);
|
||||
}
|
||||
|
||||
{
|
||||
Result<std::string> ser = makeError(StatusCode::kInvalidArg);
|
||||
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
|
||||
|
||||
auto out = serde::serialize(ser);
|
||||
ASSERT_EQ(out.size(), 1 + 2 + 1);
|
||||
|
||||
Result<std::string> des = makeError(Status::OK);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
|
||||
ASSERT_TRUE(des.error().message().empty());
|
||||
}
|
||||
|
||||
{
|
||||
Result<std::string> ser = makeError(StatusCode::kInvalidArg, "TT");
|
||||
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
|
||||
|
||||
auto out = serde::serialize(ser);
|
||||
ASSERT_EQ(out.size(), 1 + 2 + 1 + 1 + 2);
|
||||
|
||||
Result<std::string> des = makeError(Status::OK);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
|
||||
ASSERT_EQ(des.error().message(), "TT");
|
||||
}
|
||||
|
||||
{
|
||||
Result<Void> ser = Void{};
|
||||
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
|
||||
|
||||
auto out = serde::serialize(ser);
|
||||
ASSERT_EQ(out.size(), 1);
|
||||
|
||||
Result<Void> des = makeError(Status::OK);
|
||||
ASSERT_FALSE(des);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_OK(des);
|
||||
ASSERT_EQ(des, ser);
|
||||
}
|
||||
|
||||
{
|
||||
Result<Void> ser = makeError(StatusCode::kInvalidArg);
|
||||
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
|
||||
|
||||
auto out = serde::serialize(ser);
|
||||
ASSERT_EQ(out.size(), 1 + 2 + 1);
|
||||
|
||||
Result<Void> des = makeError(Status::OK);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
|
||||
ASSERT_TRUE(des.error().message().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDes) {
|
||||
Demo ser;
|
||||
ser.a() = 1;
|
||||
ser.b() = 2;
|
||||
ser.c() = "test";
|
||||
auto out = serde::serialize(ser);
|
||||
|
||||
Demo des;
|
||||
serde::In<std::string_view> in(out);
|
||||
ASSERT_OK(serde::deserialize(des, in));
|
||||
ASSERT_EQ(des.a(), 1);
|
||||
ASSERT_EQ(des.b(), 2);
|
||||
ASSERT_EQ(des.c(), "test");
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesJson) {
|
||||
auto json = R"({
|
||||
"a": 1,
|
||||
"b": 2.0,
|
||||
"c": "test"
|
||||
})";
|
||||
|
||||
Demo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, json));
|
||||
ASSERT_EQ(des.a(), 1);
|
||||
ASSERT_EQ(des.b(), 2);
|
||||
ASSERT_EQ(des.c(), "test");
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesToml) {
|
||||
auto toml = R"(
|
||||
a = 1
|
||||
b = 2.0
|
||||
c = "test"
|
||||
)";
|
||||
|
||||
Demo des;
|
||||
ASSERT_OK(serde::fromTomlString(des, toml));
|
||||
ASSERT_EQ(des.a(), 1);
|
||||
ASSERT_EQ(des.b(), 2);
|
||||
ASSERT_EQ(des.c(), "test");
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesOptional) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(num, std::optional<float>{});
|
||||
SERDE_STRUCT_FIELD(str, std::optional<std::string>{});
|
||||
};
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
auto out = serde::serialize(ser);
|
||||
|
||||
Foo des;
|
||||
serde::In<std::string_view> in(out);
|
||||
ASSERT_OK(serde::deserialize(des, in));
|
||||
ASSERT_FALSE(des.num.has_value());
|
||||
ASSERT_FALSE(des.str.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
auto out = serde::toJsonString(ser);
|
||||
XLOGF(INFO, "json is {}", out);
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, out));
|
||||
ASSERT_FALSE(des.num.has_value());
|
||||
ASSERT_FALSE(des.str.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
auto out = serde::toTomlString(ser);
|
||||
XLOGF(INFO, "toml is {}", out);
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromTomlString(des, out));
|
||||
ASSERT_FALSE(des.num.has_value());
|
||||
ASSERT_FALSE(des.str.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
Foo des;
|
||||
des.num = 20;
|
||||
des.str = "Test";
|
||||
ASSERT_OK(serde::fromJsonString(des, "{}"));
|
||||
ASSERT_FALSE(des.num.has_value());
|
||||
ASSERT_FALSE(des.str.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
Foo des;
|
||||
des.num = 20;
|
||||
des.str = "Test";
|
||||
ASSERT_OK(serde::fromTomlString(des, ""));
|
||||
ASSERT_FALSE(des.num.has_value());
|
||||
ASSERT_FALSE(des.str.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
ser.num = 27;
|
||||
ser.str = "OK";
|
||||
auto out = serde::serialize(ser);
|
||||
|
||||
Foo des;
|
||||
serde::In<std::string_view> in(out);
|
||||
ASSERT_OK(serde::deserialize(des, in));
|
||||
ASSERT_TRUE(des.num.has_value());
|
||||
ASSERT_EQ(ser.num, des.num);
|
||||
ASSERT_TRUE(des.str.has_value());
|
||||
ASSERT_EQ(ser.str, des.str);
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
ser.num = 27;
|
||||
ser.str = "OK";
|
||||
auto out = serde::toJsonString(ser);
|
||||
XLOGF(INFO, "json is {}", out);
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, out));
|
||||
ASSERT_TRUE(des.num.has_value());
|
||||
ASSERT_EQ(ser.num, des.num);
|
||||
ASSERT_TRUE(des.str.has_value());
|
||||
ASSERT_EQ(ser.str, des.str);
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
ser.num = 27;
|
||||
ser.str = "OK";
|
||||
auto out = serde::toTomlString(ser);
|
||||
XLOGF(INFO, "toml is {}", out);
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromTomlString(des, out));
|
||||
ASSERT_TRUE(des.num.has_value());
|
||||
ASSERT_EQ(ser.num, des.num);
|
||||
ASSERT_TRUE(des.str.has_value());
|
||||
ASSERT_EQ(ser.str, des.str);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesVariant) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(num, (std::variant<int, double>{}));
|
||||
SERDE_STRUCT_FIELD(val, (std::variant<std::string, Demo>{}));
|
||||
};
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
auto out = serde::serialize(ser);
|
||||
Foo des;
|
||||
serde::In<std::string_view> in(out);
|
||||
ASSERT_OK(serde::deserialize(des, in));
|
||||
ASSERT_EQ(des.num.index(), 0);
|
||||
ASSERT_EQ(des.val.index(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
ser.num = 20.0;
|
||||
ser.val = Demo{};
|
||||
auto out = serde::serialize(ser);
|
||||
Foo des;
|
||||
ASSERT_EQ(des.num.index(), 0);
|
||||
ASSERT_EQ(des.val.index(), 0);
|
||||
serde::In<std::string_view> in(out);
|
||||
ASSERT_OK(serde::deserialize(des, in));
|
||||
ASSERT_EQ(des.num.index(), 1);
|
||||
ASSERT_EQ(des.val.index(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto json = R"({
|
||||
"num": {
|
||||
"type": "double",
|
||||
"value": 70
|
||||
},
|
||||
"val": {
|
||||
"type": "basic_string",
|
||||
"value": "Test"
|
||||
}
|
||||
})";
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, json));
|
||||
ASSERT_EQ(des.num.index(), 1);
|
||||
ASSERT_EQ(std::get<1>(des.num), 70.0);
|
||||
ASSERT_EQ(des.val.index(), 0);
|
||||
ASSERT_EQ(std::get<0>(des.val), "Test");
|
||||
}
|
||||
|
||||
{
|
||||
Foo ser;
|
||||
ser.num = 70.0;
|
||||
Demo demo;
|
||||
demo.c() = "OK";
|
||||
ser.val = demo;
|
||||
auto out = serde::toJsonString(ser);
|
||||
XLOGF(INFO, "json is {}", out);
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, out));
|
||||
ASSERT_EQ(des.num.index(), 1);
|
||||
ASSERT_EQ(std::get<1>(des.num), 70.0);
|
||||
ASSERT_EQ(des.val.index(), 1);
|
||||
ASSERT_EQ(std::get<1>(des.val).c(), "OK");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesContainer) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(num, (std::vector<int>{}));
|
||||
SERDE_STRUCT_FIELD(val, (std::set<std::string>{}));
|
||||
SERDE_STRUCT_FIELD(map, (std::map<std::string, std::string>{}));
|
||||
SERDE_STRUCT_FIELD(map2, (std::map<int, std::string>{}));
|
||||
};
|
||||
|
||||
{
|
||||
auto json = R"({
|
||||
"num": [1, 2, 3, 4],
|
||||
"val": ["A", "B"],
|
||||
"map": {
|
||||
"A": "1",
|
||||
"B": "2"
|
||||
},
|
||||
"map2": {
|
||||
"1": "A",
|
||||
"2": "B"
|
||||
}
|
||||
})";
|
||||
|
||||
Foo des;
|
||||
ASSERT_OK(serde::fromJsonString(des, json));
|
||||
ASSERT_EQ(des.num.size(), 4);
|
||||
ASSERT_EQ(des.val.size(), 2);
|
||||
ASSERT_EQ(des.map.size(), 2);
|
||||
ASSERT_EQ(des.map["A"], "1");
|
||||
ASSERT_EQ(des.map["B"], "2");
|
||||
ASSERT_EQ(des.map2.size(), 2);
|
||||
ASSERT_EQ(des.map2[1], "A");
|
||||
ASSERT_EQ(des.map2[2], "B");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, NewDesEnum) {
|
||||
enum class Foo { A, B, C };
|
||||
|
||||
Foo foo;
|
||||
{
|
||||
auto json = serde::toJsonString(Foo::B);
|
||||
ASSERT_OK(serde::fromJsonString(foo, json));
|
||||
ASSERT_EQ(foo, Foo::B);
|
||||
}
|
||||
|
||||
ASSERT_FALSE(serde::fromJsonString(foo, "\"D\""));
|
||||
}
|
||||
|
||||
TEST(TestSerde, UniquePtr) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(value, std::unique_ptr<std::string>{});
|
||||
};
|
||||
struct Bar {
|
||||
SERDE_STRUCT_FIELD(value, std::unique_ptr<Foo>{});
|
||||
};
|
||||
|
||||
Bar bar;
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
|
||||
bar.value = std::make_unique<Foo>();
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
|
||||
bar.value->value = std::make_unique<std::string>("hello");
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, SharedPtr) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(value, std::shared_ptr<std::string>{});
|
||||
};
|
||||
struct Bar {
|
||||
SERDE_STRUCT_FIELD(value, std::shared_ptr<Foo>{});
|
||||
};
|
||||
|
||||
Bar bar;
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_EQ(des.value, nullptr);
|
||||
}
|
||||
|
||||
bar.value = std::make_unique<Foo>();
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_EQ(des.value->value, nullptr);
|
||||
}
|
||||
|
||||
bar.value->value = std::make_unique<std::string>("hello");
|
||||
XLOGF(INFO, "{}", bar);
|
||||
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
{
|
||||
Bar des;
|
||||
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
|
||||
ASSERT_NE(des.value, nullptr);
|
||||
ASSERT_NE(des.value->value, nullptr);
|
||||
ASSERT_EQ(*des.value->value, "hello");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSerde, AutoFallbackVariant) {
|
||||
std::variant<String, Status> src = Status(StatusCode::kUnknown, "Unknown");
|
||||
static_assert(!serde::is_auto_fallback_variant_v<decltype(src)>);
|
||||
auto s = serde::serialize(src);
|
||||
serde::AutoFallbackVariant<String> dst;
|
||||
static_assert(serde::is_auto_fallback_variant_v<decltype(dst)>);
|
||||
ASSERT_OK(serde::deserialize(dst, s));
|
||||
auto *uvt = std::get_if<serde::UnknownVariantType>(&dst);
|
||||
ASSERT_TRUE(uvt != nullptr);
|
||||
ASSERT_EQ(uvt->type, "Status");
|
||||
}
|
||||
|
||||
TEST(TestSerde, UserBufferAllocator) {
|
||||
std::string ser = "hello world!";
|
||||
auto len = serde::serializeLength(ser);
|
||||
|
||||
std::string buf(len, '\0');
|
||||
serde::serializeToUserBuffer(ser, (uint8_t *)buf.data(), buf.size());
|
||||
|
||||
std::string der;
|
||||
serde::deserialize(der, buf);
|
||||
ASSERT_EQ(ser, der);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
146
tests/common/serde/TestSerdeTo.cc
Normal file
146
tests/common/serde/TestSerdeTo.cc
Normal file
@@ -0,0 +1,146 @@
|
||||
#include <folly/container/OrderedMap.h>
|
||||
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/Path.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/Size.h"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
STRONG_TYPEDEF(uint32_t, ID);
|
||||
enum class Type { A, B, C, D };
|
||||
|
||||
struct Demo {
|
||||
bool operator==(const Demo &) const = default;
|
||||
|
||||
SERDE_STRUCT_FIELD(name, std::string{"Test"}, nullptr);
|
||||
SERDE_STRUCT_FIELD(size, 1_MB, nullptr);
|
||||
SERDE_STRUCT_FIELD(addr, net::Address{}, nullptr);
|
||||
SERDE_STRUCT_FIELD(id, ID{12138}, nullptr);
|
||||
SERDE_STRUCT_FIELD(type, Type::C, nullptr);
|
||||
SERDE_STRUCT_FIELD(values, (std::set<ID>{ID{1}, ID{2}}), nullptr);
|
||||
SERDE_STRUCT_FIELD(map, (std::map<std::string, Size>{{"a", 1_MB}, {"b", 2_MB}}), nullptr);
|
||||
SERDE_STRUCT_FIELD(option, std::optional<std::string>("OK"), nullptr);
|
||||
SERDE_STRUCT_FIELD(variant, (std::variant<int, std::string>("OK")), nullptr);
|
||||
SERDE_STRUCT_FIELD(path, Path{"/a/b/c"}, nullptr);
|
||||
};
|
||||
|
||||
TEST(TestSerde, ToToml) {
|
||||
Demo demo;
|
||||
XLOGF(INFO, "Toml: {}", serde::toTomlString(demo));
|
||||
|
||||
struct Demo2 {
|
||||
SERDE_STRUCT_FIELD(one, Demo{});
|
||||
SERDE_STRUCT_FIELD(two, Demo{});
|
||||
};
|
||||
XLOGF(INFO, "Toml: {}", serde::toTomlString(Demo2{}));
|
||||
}
|
||||
|
||||
TEST(TestSerde, ToJson) {
|
||||
Demo demo;
|
||||
demo.option = std::nullopt; // count - 1
|
||||
demo.variant = 100;
|
||||
demo.path = "/x/y/z";
|
||||
XLOGF(INFO, "Json: {}", serde::toJsonString(demo));
|
||||
|
||||
auto out = serde::serialize(demo);
|
||||
Demo des;
|
||||
ASSERT_NE(des.path, demo.path);
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des, demo);
|
||||
ASSERT_EQ(des.path, demo.path);
|
||||
}
|
||||
|
||||
TEST(TestOrderedMap, Normal) {
|
||||
folly::OrderedMap<int, int> map;
|
||||
constexpr auto N = 1000;
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
map[i] = i;
|
||||
}
|
||||
|
||||
auto idx = 0;
|
||||
for (auto [k, v] : map) {
|
||||
ASSERT_EQ(k, idx);
|
||||
ASSERT_EQ(v, idx);
|
||||
++idx;
|
||||
}
|
||||
ASSERT_EQ(idx, N);
|
||||
|
||||
for (auto i = 0; i < N; i += 2) {
|
||||
map.erase(i);
|
||||
}
|
||||
|
||||
idx = 1;
|
||||
for (auto [k, v] : map) {
|
||||
ASSERT_EQ(k, idx);
|
||||
ASSERT_EQ(v, idx);
|
||||
idx += 2;
|
||||
}
|
||||
ASSERT_EQ(idx, N + 1);
|
||||
|
||||
for (auto i = 0; i < N; i += 2) {
|
||||
map[i] = i;
|
||||
}
|
||||
ASSERT_EQ(map.size(), N);
|
||||
|
||||
idx = 1;
|
||||
for (auto [k, v] : map) {
|
||||
ASSERT_EQ(k, idx);
|
||||
ASSERT_EQ(v, idx);
|
||||
idx += 2;
|
||||
if (idx == N + 1) {
|
||||
idx = 0;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(idx, N);
|
||||
|
||||
auto item = std::move(map);
|
||||
ASSERT_EQ(item.size(), N);
|
||||
ASSERT_EQ(map.size(), 0);
|
||||
item.clear();
|
||||
ASSERT_TRUE(item.empty());
|
||||
}
|
||||
|
||||
TEST(TestSerde, RDMARemoteBuf) {
|
||||
net::RDMARemoteBuf buf;
|
||||
XLOGF(INFO, "buf is {}", serde::toJsonString(buf));
|
||||
|
||||
auto rkeys = buf.rkeys();
|
||||
rkeys[0].devId = 1;
|
||||
rkeys[0].rkey = 100;
|
||||
rkeys[1].devId = 2;
|
||||
rkeys[1].rkey = 200;
|
||||
|
||||
buf = net::RDMARemoteBuf(0x1000, 1024, rkeys);
|
||||
XLOGF(INFO, "buf is {}", serde::toJsonString(buf));
|
||||
|
||||
auto out = serde::serialize(buf);
|
||||
ASSERT_EQ(out.size(), 8 + 8 + 1 + 8 * 2); // addr + size + len + rkey * 2
|
||||
|
||||
net::RDMARemoteBuf des;
|
||||
ASSERT_OK(serde::deserialize(des, out));
|
||||
ASSERT_EQ(des, buf);
|
||||
}
|
||||
|
||||
TEST(TestSerde, DoubleFormat) {
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(value, 0ul);
|
||||
};
|
||||
|
||||
Foo foo;
|
||||
foo.value = ~0ul / 2;
|
||||
XLOGF(INFO, "foo is {}", foo);
|
||||
|
||||
auto json = serde::toJsonString(foo);
|
||||
Foo bar;
|
||||
ASSERT_OK(serde::fromJsonString(bar, json));
|
||||
ASSERT_EQ(bar.value, foo.value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
425
tests/common/serde/TestService.cc
Normal file
425
tests/common/serde/TestService.cc
Normal file
@@ -0,0 +1,425 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/Server.h"
|
||||
#include "common/net/Transport.h"
|
||||
#include "common/net/WriteItem.h"
|
||||
#include "common/net/sync/Client.h"
|
||||
#include "common/serde/CallContext.h"
|
||||
#include "common/serde/ClientMockContext.h"
|
||||
#include "common/serde/MessagePacket.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/serde/Service.h"
|
||||
#include "common/serde/Services.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Reflection.h"
|
||||
#include "common/utils/Thief.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "tests/common/net/ib/SetupIB.h"
|
||||
|
||||
namespace hf3fs::serde::test {
|
||||
namespace {
|
||||
|
||||
struct EchoReq {
|
||||
SERDE_STRUCT_FIELD(value, std::string{});
|
||||
};
|
||||
struct EchoRsp {
|
||||
SERDE_STRUCT_FIELD(value, std::string{});
|
||||
};
|
||||
struct FooReq {
|
||||
SERDE_STRUCT_FIELD(value, 0);
|
||||
};
|
||||
struct FooRsp {
|
||||
SERDE_STRUCT_FIELD(value, 0.0);
|
||||
};
|
||||
struct FourReq {
|
||||
SERDE_STRUCT_FIELD(value, 0);
|
||||
};
|
||||
struct FourRsp {
|
||||
SERDE_STRUCT_FIELD(value, 0);
|
||||
};
|
||||
|
||||
SERDE_SERVICE(DemoService, 1) {
|
||||
SERDE_SERVICE_METHOD(echo, 1, EchoReq, EchoRsp);
|
||||
SERDE_SERVICE_METHOD(foo, 2, FooReq, FooRsp);
|
||||
SERDE_SERVICE_METHOD(three, 3, int, int);
|
||||
SERDE_SERVICE_METHOD(four, 4, FourReq, FourRsp);
|
||||
};
|
||||
|
||||
struct ServiceImpl : public serde::ServiceWrapper<ServiceImpl, DemoService> {
|
||||
CoTryTask<EchoRsp> echo(const EchoReq &req) {
|
||||
cnt += 1;
|
||||
EchoRsp rsp;
|
||||
rsp.value = req.value;
|
||||
co_return rsp;
|
||||
}
|
||||
CoTryTask<FooRsp> foo(const FooReq &req) {
|
||||
cnt += 2;
|
||||
FooRsp rsp;
|
||||
rsp.value = req.value + 1;
|
||||
co_return rsp;
|
||||
}
|
||||
CoTryTask<int> three(int req) {
|
||||
cnt += 4;
|
||||
XLOG(ERR, "req is {}", req);
|
||||
co_return req * 2;
|
||||
}
|
||||
CoTryTask<FourRsp> four(const FourReq &req) {
|
||||
cnt += 8;
|
||||
FourRsp rsp;
|
||||
rsp.value = req.value * req.value;
|
||||
co_return rsp;
|
||||
}
|
||||
int cnt = 0;
|
||||
};
|
||||
|
||||
static_assert(DemoService<>::kServiceName == "DemoService");
|
||||
static_assert(DemoService<>::kServiceID == 1);
|
||||
static_assert(refl::Helper::Size<ServiceImpl> == 4);
|
||||
static_assert(!serde::SerdeType<ServiceImpl>);
|
||||
|
||||
struct FakeClient {
|
||||
template <NameWrapper kServiceName,
|
||||
NameWrapper kMethodName,
|
||||
class Req,
|
||||
class Rsp,
|
||||
uint16_t ServiceID,
|
||||
uint16_t MethodID>
|
||||
CoTryTask<Rsp> call(const Req &, auto...) {
|
||||
XLOGF(INFO, "call method {}:{}", ServiceID, MethodID);
|
||||
co_return Rsp{};
|
||||
}
|
||||
};
|
||||
|
||||
class TestService : public net::test::SetupIB {};
|
||||
|
||||
TEST_F(TestService, Client) {
|
||||
FakeClient ctx;
|
||||
DemoService a;
|
||||
folly::coro::blockingWait(a.echo(ctx, EchoReq{}));
|
||||
folly::coro::blockingWait(a.foo(ctx, FooReq{}));
|
||||
}
|
||||
|
||||
TEST_F(TestService, Server) {
|
||||
ServiceImpl impl;
|
||||
ASSERT_EQ(impl.cnt, 0);
|
||||
folly::coro::blockingWait((impl.*refl::Helper::FieldInfo<ServiceImpl, 0>::method)(EchoReq{}));
|
||||
ASSERT_EQ(impl.cnt, 1);
|
||||
folly::coro::blockingWait((impl.*refl::Helper::FieldInfo<ServiceImpl, 1>::method)(FooReq{}));
|
||||
ASSERT_EQ(impl.cnt, 3);
|
||||
|
||||
auto call = [&impl](auto type) {
|
||||
using T = std::decay_t<decltype(type)>;
|
||||
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||
XLOGF(INFO, "not found!");
|
||||
} else {
|
||||
folly::coro::blockingWait((impl.*T::method)(typename T::ReqType{}));
|
||||
}
|
||||
};
|
||||
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 0);
|
||||
ASSERT_EQ(impl.cnt, 4);
|
||||
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 1);
|
||||
ASSERT_EQ(impl.cnt, 6);
|
||||
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 4);
|
||||
ASSERT_EQ(impl.cnt, 6);
|
||||
}
|
||||
|
||||
struct FakeCallContext {
|
||||
Result<std::string> handle(const std::string &str) {
|
||||
RETURN_AND_LOG_ON_ERROR(serde::deserialize(recv_, str));
|
||||
auto methodPointer = MethodExtractor<ServiceImpl, FakeCallContext>::get(recv_.methodId);
|
||||
if (LIKELY(methodPointer != nullptr)) {
|
||||
return (this->*methodPointer)();
|
||||
} else {
|
||||
XLOGF(INFO, "method not found!");
|
||||
return makeError(RPCCode::kInvalidMethodID);
|
||||
}
|
||||
}
|
||||
|
||||
template <class FieldInfo>
|
||||
Result<std::string> call() {
|
||||
typename FieldInfo::ReqType req;
|
||||
RETURN_AND_LOG_ON_ERROR(serde::deserialize(req, recv_.payload));
|
||||
auto rsp = folly::coro::blockingWait((impl_.*FieldInfo::method)(req));
|
||||
RETURN_AND_LOG_ON_ERROR(rsp);
|
||||
MessagePacket send(*rsp);
|
||||
return serde::serialize(send);
|
||||
}
|
||||
|
||||
ServiceImpl impl_;
|
||||
MessagePacket<> recv_;
|
||||
};
|
||||
|
||||
class FakeServer {
|
||||
public:
|
||||
template <NameWrapper kServiceName,
|
||||
NameWrapper kMethodName,
|
||||
class Req,
|
||||
class Rsp,
|
||||
uint16_t ServiceID,
|
||||
uint16_t MethodID>
|
||||
CoTryTask<Rsp> call(const Req &req, auto...) {
|
||||
XLOGF(INFO, "call method {}:{}", ServiceID, MethodID);
|
||||
MessagePacket send(req);
|
||||
send.serviceId = ServiceID;
|
||||
send.methodId = MethodID;
|
||||
auto bytes = serde::serialize(send);
|
||||
|
||||
FakeCallContext ctx;
|
||||
auto result = ctx.handle(bytes);
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
MessagePacket recv;
|
||||
CO_RETURN_ON_ERROR(serde::deserialize(recv, *result));
|
||||
Rsp rsp;
|
||||
CO_RETURN_ON_ERROR(serde::deserialize(rsp, recv.payload));
|
||||
co_return rsp;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TestService, ClientToServer) {
|
||||
FakeServer ctx;
|
||||
DemoService a;
|
||||
|
||||
{
|
||||
EchoReq req;
|
||||
req.value = "hello";
|
||||
auto result = folly::coro::blockingWait(a.echo(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(req.value, result->value);
|
||||
}
|
||||
|
||||
{
|
||||
FooReq req;
|
||||
req.value = 100;
|
||||
auto result = folly::coro::blockingWait(a.foo(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(req.value + 1, result->value);
|
||||
}
|
||||
|
||||
{
|
||||
auto req = 100;
|
||||
auto result = folly::coro::blockingWait(a.three(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(req * 2, *result);
|
||||
}
|
||||
|
||||
{
|
||||
FourReq req;
|
||||
req.value = 100;
|
||||
auto result = folly::coro::blockingWait(a.four(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(req.value * req.value, result->value);
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(sizeof(CallContext::MethodType) == 16);
|
||||
|
||||
template <class Context = serde::CallContext>
|
||||
struct RealService : public serde::ServiceWrapper<RealService<Context>, DemoService> {
|
||||
CoTryTask<EchoRsp> echo(Context &, const EchoReq &req) {
|
||||
cnt += 1;
|
||||
EchoRsp rsp;
|
||||
rsp.value = req.value;
|
||||
co_return rsp;
|
||||
}
|
||||
CoTryTask<FooRsp> foo(Context &, const FooReq &req) {
|
||||
cnt += 2;
|
||||
FooRsp rsp;
|
||||
rsp.value = req.value + 1;
|
||||
co_return rsp;
|
||||
}
|
||||
CoTryTask<int> three(Context &, const int &req) {
|
||||
cnt += 4;
|
||||
co_return req * 2;
|
||||
}
|
||||
CoTryTask<FourRsp> four(Context &, const FourReq &) {
|
||||
cnt += 8;
|
||||
co_await folly::coro::sleep(std::chrono::milliseconds(10));
|
||||
co_return makeError(StatusCode::kInvalidConfig);
|
||||
}
|
||||
std::atomic<int> cnt = 0;
|
||||
};
|
||||
|
||||
TEST_F(TestService, Normal) {
|
||||
Services services;
|
||||
ASSERT_OK(services.addService(std::make_unique<RealService<>>(), false));
|
||||
|
||||
for (auto o = 0; o < 2; ++o) {
|
||||
auto &service = services.getServiceById(RealService<>::kServiceID + o, false);
|
||||
ASSERT_NE(service.getter, nullptr);
|
||||
|
||||
for (auto i = 0; i < 100; ++i) {
|
||||
if (o == 0 && 1 <= i && i <= 4) {
|
||||
ASSERT_NE(service.getter(i), nullptr);
|
||||
} else {
|
||||
ASSERT_EQ(service.getter(i), &CallContext::invalidId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestService, AddSerdeService) {
|
||||
net::Server::Config config;
|
||||
net::Server server(config);
|
||||
ASSERT_OK(server.setup());
|
||||
ASSERT_OK(server.addSerdeService(std::make_unique<RealService<>>()));
|
||||
|
||||
ASSERT_OK(server.start());
|
||||
server.stopAndJoin();
|
||||
}
|
||||
|
||||
TEST_F(TestService, CallContext) {
|
||||
auto service = std::make_unique<RealService<>>();
|
||||
auto pointer = service.get();
|
||||
|
||||
Services services;
|
||||
ASSERT_OK(services.addService(std::move(service), false));
|
||||
|
||||
EchoReq req;
|
||||
req.value = "hello";
|
||||
MessagePacket send(req);
|
||||
send.serviceId = 1;
|
||||
send.methodId = 1;
|
||||
auto bytes = serde::serialize(send);
|
||||
|
||||
MessagePacket<> recv;
|
||||
ASSERT_OK(serde::deserialize(recv, bytes));
|
||||
|
||||
net::Server::Config config;
|
||||
net::Server server(config);
|
||||
ASSERT_OK(server.setup());
|
||||
ASSERT_OK(server.start());
|
||||
auto tr = net::Transport::create(net::Address{}, server.groups().front()->ioWorker());
|
||||
|
||||
{
|
||||
ASSERT_EQ(pointer->cnt, 0);
|
||||
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
|
||||
folly::coro::blockingWait(ctx.handle());
|
||||
ASSERT_EQ(pointer->cnt, 1);
|
||||
}
|
||||
|
||||
{
|
||||
recv.methodId = 0;
|
||||
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
|
||||
folly::coro::blockingWait(ctx.handle());
|
||||
ASSERT_EQ(pointer->cnt, 1);
|
||||
}
|
||||
|
||||
{
|
||||
recv.serviceId = 0;
|
||||
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
|
||||
folly::coro::blockingWait(ctx.handle());
|
||||
ASSERT_EQ(pointer->cnt, 1);
|
||||
}
|
||||
|
||||
server.stopAndJoin();
|
||||
}
|
||||
|
||||
TEST_F(TestService, ClientContext) {
|
||||
// 1. create service.
|
||||
auto service = std::make_unique<RealService<>>();
|
||||
auto pointer = service.get();
|
||||
|
||||
// 2. start server.
|
||||
auto ctx = serde::ClientMockContext::create(std::move(service), net::Address::RDMA);
|
||||
|
||||
// 4. call client ctx.
|
||||
{
|
||||
DemoService stub;
|
||||
EchoReq req{"hello"};
|
||||
auto result = folly::coro::blockingWait(stub.echo(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(result->value, req.value);
|
||||
ASSERT_EQ(pointer->cnt, 1);
|
||||
}
|
||||
|
||||
{
|
||||
DemoService stub;
|
||||
FooReq req{100};
|
||||
auto result = folly::coro::blockingWait(stub.foo(ctx, req));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(result->value, req.value + 1);
|
||||
ASSERT_EQ(pointer->cnt, 3);
|
||||
}
|
||||
|
||||
{
|
||||
DemoService stub;
|
||||
FooReq req{100};
|
||||
auto result = stub.fooSync(ctx, req);
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(result->value, req.value + 1);
|
||||
ASSERT_EQ(pointer->cnt, 5);
|
||||
}
|
||||
|
||||
{
|
||||
DemoService stub;
|
||||
Timestamp timestamp{};
|
||||
auto result = folly::coro::blockingWait(stub.three(ctx, 100, nullptr, ×tamp));
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(*result, 200);
|
||||
ASSERT_EQ(pointer->cnt, 9);
|
||||
serde::iterate(
|
||||
[](std::string_view name, auto &value) {
|
||||
if (value == 0) {
|
||||
XLOGF(ERR, "{} is zero!", name);
|
||||
ASSERT_NE(value, 0);
|
||||
}
|
||||
},
|
||||
timestamp);
|
||||
XLOGF(INFO, "timestamp is {}", timestamp);
|
||||
}
|
||||
|
||||
{
|
||||
DemoService stub;
|
||||
FourReq req{100};
|
||||
auto result = folly::coro::blockingWait(stub.four(ctx, req));
|
||||
ASSERT_FALSE(result);
|
||||
XLOGF(INFO, "result is error: {}", result.error());
|
||||
}
|
||||
|
||||
{
|
||||
DemoService stub;
|
||||
FourReq req{100};
|
||||
net::UserRequestOptions options;
|
||||
options.timeout = 1_us;
|
||||
auto result = folly::coro::blockingWait(stub.four(ctx, req, &options));
|
||||
ASSERT_FALSE(result);
|
||||
XLOGF(INFO, "result is error: {}", result.error());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestService, CallSync) {
|
||||
using namespace hf3fs::net;
|
||||
|
||||
// note: No IPoIB device in gitlab ci docker runner.
|
||||
std::array<Address::Type, 4> networks{Address::LOCAL, Address::TCP /*, Address::IPoIB */};
|
||||
for (auto network : networks) {
|
||||
Server::Config serverConfig;
|
||||
serverConfig.groups(0).set_network_type(network);
|
||||
Server server_{serverConfig};
|
||||
|
||||
ASSERT_OK(server_.addSerdeService(std::make_unique<RealService<>>()));
|
||||
ASSERT_OK(server_.setup());
|
||||
ASSERT_OK(server_.start());
|
||||
auto serverAddr = server_.groups().front()->addressList().front();
|
||||
|
||||
net::sync::Client::Config clientConfig;
|
||||
net::sync::Client client_{clientConfig};
|
||||
auto ctx = client_.serdeCtx(serverAddr);
|
||||
|
||||
EchoReq req{"hello"};
|
||||
auto result = DemoService<>::echoSync(ctx, req);
|
||||
ASSERT_OK(result);
|
||||
ASSERT_EQ(result->value, req.value);
|
||||
|
||||
server_.stopAndJoin();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::serde::test
|
||||
32
tests/common/serde/TestTypeName.cc
Normal file
32
tests/common/serde/TestTypeName.cc
Normal file
@@ -0,0 +1,32 @@
|
||||
#include <boost/intrusive/intrusive_fwd.hpp>
|
||||
|
||||
#include "common/serde/TypeName.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
static_assert(serde::type_name_v<int> == "int");
|
||||
static_assert(serde::type_name_v<float> == "float");
|
||||
static_assert(serde::type_name_v<double> == "double");
|
||||
static_assert(serde::type_name_v<std::string> == "basic_string"); // template arguments are ignored
|
||||
static_assert(serde::type_name_v<std::vector<int>> == "vector"); // template arguments are ignored
|
||||
|
||||
struct Foo {};
|
||||
static_assert(serde::type_name_v<Foo> == "Foo");
|
||||
|
||||
struct Bar {
|
||||
static constexpr std::string_view kTypeNameForSerde = "BAR";
|
||||
};
|
||||
static_assert(serde::type_name_v<Bar> == "BAR");
|
||||
|
||||
static_assert(serde::variant_type_names_v<std::variant<int, float>>[0] == "int");
|
||||
static_assert(serde::variant_type_names_v<std::variant<int, float>>[1] == "float");
|
||||
static_assert(serde::variant_type_names_v<std::variant<std::string, std::vector<int>>>[0] == "basic_string");
|
||||
static_assert(serde::variant_type_names_v<std::variant<std::string, std::vector<int>>>[1] == "vector");
|
||||
static_assert(serde::variant_type_names_v<std::variant<Foo, Bar>>[0] == "Foo");
|
||||
static_assert(serde::variant_type_names_v<std::variant<Foo, Bar>>[1] == "BAR");
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
43
tests/common/serde/TestVisit.cc
Normal file
43
tests/common/serde/TestVisit.cc
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <folly/json.h>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/serde/Visit.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
template <>
|
||||
class hf3fs::serde::VisitOut<void> {
|
||||
public:
|
||||
auto tableBegin() { XLOGF(INFO, "start a table"); }
|
||||
void tableEnd() { XLOGF(INFO, "end a table"); }
|
||||
void arrayBegin() { XLOGF(INFO, "start an array"); }
|
||||
void arrayEnd() { XLOGF(INFO, "end an array"); }
|
||||
void variantBegin() { XLOGF(INFO, "start a variant"); }
|
||||
void variantEnd() { XLOGF(INFO, "end a variant"); }
|
||||
void key(std::string_view key) { XLOGF(INFO, "key {}", key); }
|
||||
void value(auto &&value) { XLOGF(INFO, "value {}", nameof::nameof_full_type<std::decay_t<decltype(value)>>()); }
|
||||
};
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
enum class Type {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
};
|
||||
|
||||
struct Foo {
|
||||
SERDE_STRUCT_FIELD(val, double{});
|
||||
SERDE_STRUCT_FIELD(type, Type::A);
|
||||
SERDE_STRUCT_FIELD(num, (std::variant<int, double>{}));
|
||||
SERDE_STRUCT_FIELD(vec, std::vector<std::string>{});
|
||||
};
|
||||
|
||||
TEST(TestVist, Normal) {
|
||||
serde::VisitOut<void> out;
|
||||
serde::visit(Foo{}, out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
35
tests/common/utils/TestAddress.cc
Normal file
35
tests/common/utils/TestAddress.cc
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "common/utils/Address.h"
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestAddress, Normal) {
|
||||
constexpr std::string_view str = "IPoIB://192.168.1.1:8000";
|
||||
auto addr = Address::fromString(str);
|
||||
ASSERT_TRUE(addr.isTCP());
|
||||
ASSERT_EQ(addr.ipStr(), "192.168.1.1");
|
||||
ASSERT_EQ(addr.port, 8000);
|
||||
ASSERT_EQ(addr.type, Address::IPoIB);
|
||||
ASSERT_EQ(addr.str(), str);
|
||||
|
||||
addr = Address::fromString("tcp://127.0.0.1:8888");
|
||||
ASSERT_TRUE(addr);
|
||||
ASSERT_EQ(addr.type, Address::TCP);
|
||||
ASSERT_EQ(addr.port, 8888);
|
||||
}
|
||||
|
||||
static_assert(config::IsPrimitive<Address>, "address is not primitive");
|
||||
auto kDefaultAddr = Address::from("tcp://192.168.1.1:8000").value();
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_ITEM(addr, Address{kDefaultAddr});
|
||||
};
|
||||
|
||||
TEST(TestAddress, Config) {
|
||||
Config cfg;
|
||||
ASSERT_EQ(cfg.toString(), "addr = 'TCP://192.168.1.1:8000'");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
22
tests/common/utils/TestAtomicValue.cc
Normal file
22
tests/common/utils/TestAtomicValue.cc
Normal file
@@ -0,0 +1,22 @@
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
|
||||
#include "common/utils/AtomicValue.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestAtomicValue, Normal) {
|
||||
AtomicValue<uint64_t> a;
|
||||
ASSERT_EQ(a, 0);
|
||||
|
||||
a = 1;
|
||||
ASSERT_EQ(a, 1);
|
||||
|
||||
AtomicValue<uint64_t> b = a;
|
||||
ASSERT_EQ(b, 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
79
tests/common/utils/TestBoundedQueue.cc
Normal file
79
tests/common/utils/TestBoundedQueue.cc
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/BoundedQueue.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestBoundedQueue, Normal) {
|
||||
BoundedQueue<int> queue(4);
|
||||
ASSERT_TRUE(queue.empty());
|
||||
|
||||
queue.enqueue(1);
|
||||
queue.enqueue(2);
|
||||
queue.enqueue(3);
|
||||
queue.enqueue(4);
|
||||
ASSERT_TRUE(queue.full());
|
||||
ASSERT_FALSE(queue.try_enqueue(5));
|
||||
|
||||
// 1. sync.
|
||||
{
|
||||
std::jthread dequeue([&] {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
ASSERT_EQ(queue.dequeue(), 1);
|
||||
});
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
queue.enqueue(5);
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
ASSERT_LE(50ms, elapsed);
|
||||
ASSERT_LE(elapsed, 150ms);
|
||||
}
|
||||
|
||||
// 2. async.
|
||||
{
|
||||
std::jthread dequeue([&] {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
ASSERT_EQ(folly::coro::blockingWait(queue.co_dequeue()), 2);
|
||||
});
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
folly::coro::blockingWait(queue.co_enqueue(6));
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
ASSERT_LE(50ms, elapsed);
|
||||
ASSERT_LE(elapsed, 150ms);
|
||||
}
|
||||
|
||||
// 3. try enqueue/dequeue.
|
||||
{
|
||||
ASSERT_FALSE(queue.try_enqueue(7));
|
||||
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{3});
|
||||
ASSERT_TRUE(queue.try_enqueue(7));
|
||||
|
||||
int value;
|
||||
ASSERT_TRUE(queue.try_dequeue(value));
|
||||
ASSERT_EQ(value, 4);
|
||||
|
||||
ASSERT_EQ(queue.size(), 3);
|
||||
|
||||
queue.dequeue(value);
|
||||
ASSERT_EQ(value, 5);
|
||||
|
||||
folly::coro::blockingWait(queue.co_dequeue(value));
|
||||
ASSERT_EQ(value, 6);
|
||||
|
||||
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{7});
|
||||
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{});
|
||||
ASSERT_FALSE(queue.try_dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
165
tests/common/utils/TestCoLockManager.cc
Normal file
165
tests/common/utils/TestCoLockManager.cc
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/Baton.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/experimental/coro/Timeout.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/CoLockManager.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestCoLockManager, Normal) {
|
||||
CoLockManager lockManager;
|
||||
{
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.lock(baton, "A", "no wait");
|
||||
folly::coro::blockingWait(guard.lock());
|
||||
}
|
||||
{
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.lock(baton, "A", "no wait");
|
||||
folly::coro::blockingWait(guard.lock());
|
||||
}
|
||||
|
||||
folly::CPUThreadPoolExecutor executor(1);
|
||||
folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.lock(baton, "B", "first");
|
||||
CO_ASSERT_TRUE(guard.locked());
|
||||
co_await guard.lock();
|
||||
co_await folly::coro::sleep(200ms);
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&executor)
|
||||
.start();
|
||||
|
||||
folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
co_await folly::coro::sleep(100ms);
|
||||
folly::coro::Baton baton;
|
||||
auto start = RelativeTime::now();
|
||||
auto guard = lockManager.lock(baton, "B", "second");
|
||||
CO_ASSERT_FALSE(guard.locked());
|
||||
co_await guard.lock();
|
||||
auto elapsed = RelativeTime::now() - start;
|
||||
CO_ASSERT_TRUE(70_ms <= elapsed);
|
||||
CO_ASSERT_TRUE(elapsed <= 130_ms);
|
||||
co_await folly::coro::sleep(200ms);
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&executor)
|
||||
.start();
|
||||
|
||||
{
|
||||
std::this_thread::sleep_for(300ms);
|
||||
folly::coro::Baton baton;
|
||||
auto start = RelativeTime::now();
|
||||
auto guard = lockManager.lock(baton, "B", "third");
|
||||
ASSERT_FALSE(guard.locked());
|
||||
folly::coro::blockingWait(guard.lock());
|
||||
auto elapsed = RelativeTime::now() - start;
|
||||
ASSERT_NEAR(elapsed.asMs().count(), 100, 30);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestCoLockManager, Concurrent) {
|
||||
CoLockManager lockManager;
|
||||
folly::CPUThreadPoolExecutor executor(16);
|
||||
constexpr auto N = 20000;
|
||||
auto cnt = 0;
|
||||
auto current = 0;
|
||||
folly::coro::Baton finish;
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.lock(baton, "A");
|
||||
co_await guard.lock();
|
||||
++current;
|
||||
CO_ASSERT_EQ(current, 1);
|
||||
if (++cnt == N) {
|
||||
finish.post();
|
||||
}
|
||||
--current;
|
||||
})
|
||||
.scheduleOn(&executor)
|
||||
.start();
|
||||
}
|
||||
auto result = folly::coro::blockingWait(
|
||||
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
|
||||
ASSERT_FALSE(result.hasException());
|
||||
}
|
||||
|
||||
TEST(TestCoLockManager, Concurrent2) {
|
||||
CoLockManager lockManager;
|
||||
constexpr auto N = 20000;
|
||||
auto cnt = 0;
|
||||
auto current = 0;
|
||||
folly::coro::Baton finish;
|
||||
std::vector<std::thread> threads(16);
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&] {
|
||||
for (auto i = 0; i < N / 16; ++i) {
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.lock(baton, "A");
|
||||
if (!guard.locked()) {
|
||||
folly::coro::blockingWait(guard.lock());
|
||||
}
|
||||
++current;
|
||||
ASSERT_EQ(current, 1);
|
||||
if (++cnt == N) {
|
||||
finish.post();
|
||||
}
|
||||
--current;
|
||||
}
|
||||
});
|
||||
}
|
||||
auto result = folly::coro::blockingWait(
|
||||
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
|
||||
ASSERT_FALSE(result.hasException());
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestCoLockManager, Concurrent3) {
|
||||
CoLockManager lockManager;
|
||||
constexpr auto N = 20000;
|
||||
auto cnt = 0;
|
||||
auto current = 0;
|
||||
folly::coro::Baton finish;
|
||||
std::vector<std::thread> threads(16);
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&] {
|
||||
for (auto i = 0; i < N / 16;) {
|
||||
folly::coro::Baton baton;
|
||||
auto guard = lockManager.tryLock(baton, "A");
|
||||
if (!guard.locked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++current;
|
||||
ASSERT_EQ(current, 1);
|
||||
if (++cnt == N) {
|
||||
finish.post();
|
||||
}
|
||||
--current;
|
||||
++i;
|
||||
}
|
||||
});
|
||||
}
|
||||
auto result = folly::coro::blockingWait(
|
||||
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
|
||||
ASSERT_FALSE(result.hasException());
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
44
tests/common/utils/TestConcurrencyLimiter.cc
Normal file
44
tests/common/utils/TestConcurrencyLimiter.cc
Normal file
@@ -0,0 +1,44 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Timeout.h>
|
||||
|
||||
#include "common/utils/ConcurrencyLimiter.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::net::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestConcurrencyLimiter, Normal) {
|
||||
ConcurrencyLimiterConfig config;
|
||||
ConcurrencyLimiter<std::string> limiter(config);
|
||||
for (auto c = 1; c <= 4; c *= 2) {
|
||||
config.set_max_concurrency(c);
|
||||
constexpr auto N = 20000;
|
||||
std::atomic<size_t> cnt = 0;
|
||||
std::atomic<size_t> current = 0;
|
||||
folly::coro::Baton finish;
|
||||
std::vector<std::thread> threads(16);
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&] {
|
||||
for (auto i = 0; i < N / 16; ++i) {
|
||||
auto guard = folly::coro::blockingWait(limiter.lock("A"));
|
||||
++current;
|
||||
ASSERT_LE(current, c);
|
||||
if (++cnt == N) {
|
||||
finish.post();
|
||||
}
|
||||
--current;
|
||||
}
|
||||
});
|
||||
}
|
||||
auto result = folly::coro::blockingWait(
|
||||
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5_s)));
|
||||
ASSERT_FALSE(result.hasException());
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::net::test
|
||||
522
tests/common/utils/TestConfig.cc
Normal file
522
tests/common/utils/TestConfig.cc
Normal file
@@ -0,0 +1,522 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/TestUtil.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <type_traits>
|
||||
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
enum class Channel { Red, Green, Blue };
|
||||
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_HOT_UPDATED_ITEM(string, "ing");
|
||||
CONFIG_HOT_UPDATED_ITEM(channel, Channel::Red);
|
||||
CONFIG_HOT_UPDATED_ITEM(optional, std::optional<std::string>{});
|
||||
CONFIG_SECT(sect, {
|
||||
CONFIG_HOT_UPDATED_ITEM(val, 100l, [](const int64_t &val) { return val < 200; });
|
||||
CONFIG_HOT_UPDATED_ITEM(foo, "foo");
|
||||
CONFIG_HOT_UPDATED_ITEM(score, 0.0);
|
||||
CONFIG_HOT_UPDATED_ITEM(ok, false);
|
||||
CONFIG_HOT_UPDATED_ITEM(succ, true);
|
||||
CONFIG_SECT(sub, {
|
||||
CONFIG_HOT_UPDATED_ITEM(val, 100l);
|
||||
CONFIG_HOT_UPDATED_ITEM(foo, "foo");
|
||||
});
|
||||
});
|
||||
CONFIG_SECT(vec, {
|
||||
CONFIG_HOT_UPDATED_ITEM(int_vec, std::vector<int64_t>({1, 2}), [](const std::vector<int64_t> &v) {
|
||||
return v.size() <= 2;
|
||||
});
|
||||
CONFIG_HOT_UPDATED_ITEM(str_vec, std::vector<std::string>({"foo", "foo"}));
|
||||
CONFIG_HOT_UPDATED_ITEM(double_vec, std::vector<double>({1.0, 2.0}));
|
||||
CONFIG_HOT_UPDATED_ITEM(bool_vec, std::vector<bool>({true, false}));
|
||||
CONFIG_HOT_UPDATED_ITEM(enum_vec, std::vector<Channel>({Channel::Red, Channel::Green}));
|
||||
});
|
||||
};
|
||||
static_assert(std::is_same_v<decltype(Config{}.sect().foo()), const std::string &>, "ok");
|
||||
static_assert(std::is_same_v<decltype(Config{}.sect().val()), int64_t>, "ok");
|
||||
static_assert(std::is_same_v<decltype(Config{}.vec().int_vec()), const std::vector<int64_t> &>, "ok");
|
||||
|
||||
class BigConfig : public ConfigBase<BigConfig> {
|
||||
CONFIG_OBJ(a, Config);
|
||||
CONFIG_OBJ(b, Config, [](Config &c) { c.set_string("replaced"); });
|
||||
};
|
||||
|
||||
class ArrayConfig : public ConfigBase<ArrayConfig> {
|
||||
CONFIG_OBJ_ARRAY(cfgs, Config, 4);
|
||||
} array;
|
||||
|
||||
TEST(TestConfig, Normal) {
|
||||
Config cfg;
|
||||
ASSERT_EQ(cfg.string(), "ing");
|
||||
ASSERT_FALSE(cfg.optional().has_value());
|
||||
ASSERT_EQ(cfg.sect().foo(), "foo");
|
||||
ASSERT_EQ(cfg.sect().val(), 100);
|
||||
ASSERT_EQ(cfg.sect().ok(), false);
|
||||
ASSERT_EQ(cfg.sect().succ(), true);
|
||||
ASSERT_EQ(cfg.sect().sub().val(), 100);
|
||||
|
||||
toml::table result = toml::parse(R"(
|
||||
optional = "has value"
|
||||
|
||||
[sect]
|
||||
foo = "bar"
|
||||
val = 123
|
||||
score = 1
|
||||
ok = true
|
||||
|
||||
[sect.sub]
|
||||
val = 200
|
||||
)");
|
||||
ASSERT_EQ(cfg.sect().foo(), "foo");
|
||||
ASSERT_EQ(cfg.sect().val(), 100);
|
||||
ASSERT_EQ(cfg.sect().ok(), false);
|
||||
ASSERT_EQ(cfg.sect().sub().val(), 100);
|
||||
|
||||
ASSERT_TRUE(cfg.update(result));
|
||||
ASSERT_TRUE(cfg.optional().has_value());
|
||||
ASSERT_EQ(cfg.optional().value(), "has value");
|
||||
ASSERT_EQ(cfg.sect().foo(), "bar");
|
||||
ASSERT_EQ(cfg.sect().val(), 123);
|
||||
ASSERT_EQ(cfg.sect().ok(), true);
|
||||
ASSERT_EQ(cfg.sect().sub().val(), 200);
|
||||
|
||||
ASSERT_TRUE(cfg.sect().set_val(150));
|
||||
ASSERT_FALSE(cfg.sect().set_val(200));
|
||||
|
||||
// support copy.
|
||||
auto other = cfg;
|
||||
ASSERT_EQ(other.sect().foo(), "bar");
|
||||
ASSERT_EQ(other.sect().val(), 150);
|
||||
|
||||
ASSERT_TRUE(cfg.update(result));
|
||||
ASSERT_EQ(cfg.sect().val(), 123);
|
||||
ASSERT_EQ(other.sect().val(), 150);
|
||||
}
|
||||
|
||||
TEST(TestConfig, Vector) {
|
||||
Config cfg;
|
||||
ASSERT_TRUE(cfg.vec().enum_vec() == std::vector({Channel::Red, Channel::Green}));
|
||||
|
||||
toml::table result = toml::parse(R"(
|
||||
[vec]
|
||||
int_vec = [1, 2]
|
||||
str_vec = ["foo", "bar"]
|
||||
enum_vec = ["Red", "Blue"]
|
||||
)");
|
||||
ASSERT_TRUE(cfg.update(result));
|
||||
std::vector<int64_t> expected_int_vec{1, 2};
|
||||
ASSERT_TRUE(cfg.vec().int_vec() == expected_int_vec);
|
||||
std::vector<std::string> expected_str_vec{"foo", "bar"};
|
||||
ASSERT_TRUE(cfg.vec().str_vec() == expected_str_vec);
|
||||
std::vector expected_enum_vec{Channel::Red, Channel::Blue};
|
||||
ASSERT_TRUE(cfg.vec().enum_vec() == expected_enum_vec);
|
||||
|
||||
result = toml::parse(R"(
|
||||
[vec]
|
||||
int_vec = [1, 2, 3]
|
||||
)");
|
||||
ASSERT_FALSE(cfg.update(result));
|
||||
|
||||
result = toml::parse(R"(
|
||||
[vec]
|
||||
enum_vec = ["Yellow"]
|
||||
)");
|
||||
ASSERT_FALSE(cfg.update(result));
|
||||
}
|
||||
|
||||
TEST(TestConfig, ParseValue) {
|
||||
Config cfg;
|
||||
|
||||
// 1. empty config.
|
||||
ASSERT_TRUE(cfg.update(toml::parse("")));
|
||||
|
||||
// 2. redundant entries.
|
||||
ASSERT_FALSE(cfg.update(toml::parse(R"(
|
||||
something = "else"
|
||||
)")));
|
||||
ASSERT_FALSE(cfg.update(toml::parse(R"(
|
||||
[sect]
|
||||
something = "else"
|
||||
)")));
|
||||
ASSERT_FALSE(cfg.update(toml::parse(R"(
|
||||
[some]
|
||||
thing = "else"
|
||||
)")));
|
||||
|
||||
// 3. invalid values.
|
||||
ASSERT_FALSE(cfg.update(toml::parse(R"(
|
||||
[sect]
|
||||
val = 200
|
||||
)")));
|
||||
|
||||
// 4. invalid enum.
|
||||
ASSERT_FALSE(cfg.update(toml::parse(R"(
|
||||
channel = "Yellow"
|
||||
)")));
|
||||
}
|
||||
|
||||
TEST(TestConfig, Nested) {
|
||||
BigConfig cfg;
|
||||
ASSERT_EQ(cfg.a().channel(), Channel::Red);
|
||||
ASSERT_EQ(cfg.a().string(), "ing");
|
||||
ASSERT_EQ(cfg.b().string(), "replaced");
|
||||
|
||||
cfg.b().set_channel(Channel::Green);
|
||||
ASSERT_EQ(cfg.a().channel(), Channel::Red);
|
||||
ASSERT_EQ(cfg.b().channel(), Channel::Green);
|
||||
|
||||
BigConfig other;
|
||||
ASSERT_TRUE(other.update(toml::parse(cfg.toString())));
|
||||
ASSERT_EQ(other.a().channel(), Channel::Red);
|
||||
ASSERT_EQ(other.b().channel(), Channel::Green);
|
||||
}
|
||||
|
||||
TEST(TestConfig, ToToml) {
|
||||
Config cfg;
|
||||
cfg.set_string("set string with \"quotes\"");
|
||||
cfg.sect().set_ok(false);
|
||||
cfg.vec().set_str_vec({"1", "2", "3"});
|
||||
cfg.set_channel(Channel::Blue);
|
||||
|
||||
XLOGF(INFO, "toString: {}", cfg.toString());
|
||||
|
||||
toml::table result = toml::parse(cfg.toString());
|
||||
Config other;
|
||||
ASSERT_TRUE(other.update(result));
|
||||
ASSERT_EQ(cfg.string(), other.string());
|
||||
ASSERT_EQ(cfg.sect().ok(), other.sect().ok());
|
||||
ASSERT_EQ(cfg.vec().str_vec(), other.vec().str_vec());
|
||||
ASSERT_EQ(cfg.toString(), other.toString());
|
||||
ASSERT_EQ(cfg.channel(), other.channel());
|
||||
}
|
||||
|
||||
// TEST(TestConfig, Init) {
|
||||
// char *argvArr[] = {
|
||||
// const_cast<char *>("./test"),
|
||||
// const_cast<char *>("--config.channel=Blue"),
|
||||
// const_cast<char *>("--config.string"),
|
||||
// const_cast<char *>("a long string"),
|
||||
// const_cast<char *>("--config.sect.val"),
|
||||
// const_cast<char *>("123"),
|
||||
// };
|
||||
// int argc = ARRAY_SIZE(argvArr);
|
||||
|
||||
// Config cfg;
|
||||
// auto *argv = argvArr; // decay char *[] to char **
|
||||
// ASSERT_TRUE(cfg.init(&argc, &argv, false));
|
||||
|
||||
// ASSERT_EQ(cfg.channel(), Channel::Blue);
|
||||
// ASSERT_EQ(cfg.string(), "a long string");
|
||||
// ASSERT_EQ(cfg.sect().val(), 123);
|
||||
// }
|
||||
|
||||
TEST(TestConfig, ArrayOfTable) {
|
||||
ASSERT_EQ(array.cfgs_length(), 1u);
|
||||
ASSERT_EQ(array.cfgs(0).channel(), Channel::Red);
|
||||
|
||||
toml::table table = toml::parse(R"(
|
||||
[[cfgs]]
|
||||
string = "A"
|
||||
channel = "Blue"
|
||||
|
||||
[[cfgs]]
|
||||
string = "B"
|
||||
channel = "Red"
|
||||
|
||||
[cfgs.sect]
|
||||
val = 121
|
||||
)");
|
||||
|
||||
ASSERT_TRUE(array.update(table));
|
||||
ASSERT_EQ(array.cfgs_length(), 2u);
|
||||
ASSERT_EQ(array.cfgs(0).string(), "A");
|
||||
ASSERT_EQ(array.cfgs(0).channel(), Channel::Blue);
|
||||
ASSERT_EQ(array.cfgs(0).sect().val(), 100);
|
||||
|
||||
ASSERT_EQ(array.cfgs(1).string(), "B");
|
||||
ASSERT_EQ(array.cfgs(1).channel(), Channel::Red);
|
||||
ASSERT_EQ(array.cfgs(1).sect().val(), 121);
|
||||
|
||||
XLOGF(INFO, "toString: {}", array.toString());
|
||||
}
|
||||
|
||||
TEST(TestConfig, CheckIsParsedFromString) {
|
||||
Config cfg;
|
||||
|
||||
ASSERT_TRUE(cfg.find("string"));
|
||||
ASSERT_TRUE(cfg.find("string").value()->isParsedFromString());
|
||||
|
||||
ASSERT_TRUE(cfg.find("channel"));
|
||||
ASSERT_TRUE(cfg.find("channel").value()->isParsedFromString());
|
||||
|
||||
ASSERT_TRUE(cfg.find("optional"));
|
||||
ASSERT_TRUE(cfg.find("optional").value()->isParsedFromString());
|
||||
|
||||
ASSERT_FALSE(cfg.find("sect"));
|
||||
ASSERT_FALSE(cfg.find("not_found"));
|
||||
|
||||
ASSERT_TRUE(cfg.find("sect.foo"));
|
||||
ASSERT_TRUE(cfg.find("sect.foo").value()->isParsedFromString());
|
||||
|
||||
ASSERT_TRUE(cfg.find("sect.ok"));
|
||||
ASSERT_FALSE(cfg.find("sect.ok").value()->isParsedFromString());
|
||||
}
|
||||
|
||||
TEST(TestConfig, HotUpdated) {
|
||||
Config cfg;
|
||||
auto now = std::chrono::steady_clock::now;
|
||||
cfg.set_string(cfg.sect().foo());
|
||||
|
||||
std::jthread read([&] {
|
||||
auto start = now();
|
||||
while (now() - start <= std::chrono::milliseconds(100)) {
|
||||
auto clone = cfg.clone();
|
||||
ASSERT_EQ(clone.string(), clone.sect().foo());
|
||||
|
||||
clone.copy(cfg);
|
||||
ASSERT_EQ(clone.string(), clone.sect().foo());
|
||||
}
|
||||
});
|
||||
|
||||
std::jthread update([&] {
|
||||
auto start = now();
|
||||
while (now() - start <= std::chrono::milliseconds(100)) {
|
||||
ASSERT_TRUE(cfg.update(toml::parse(fmt::format(R"(
|
||||
string = "{0}"
|
||||
[sect]
|
||||
foo = "{0}")",
|
||||
now().time_since_epoch().count()))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TEST(TestConfig, InlineTable) {
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_HOT_UPDATED_ITEM(map, (std::map<std::string, int>{}));
|
||||
CONFIG_HOT_UPDATED_ITEM(string_to_string, (std::map<std::string, std::string>{}));
|
||||
} cfg;
|
||||
ASSERT_TRUE(cfg.map().empty());
|
||||
ASSERT_TRUE(cfg.string_to_string().empty());
|
||||
|
||||
toml::table table = toml::parse(R"(
|
||||
map = { one = 1, two = 2 }
|
||||
|
||||
[string_to_string]
|
||||
hello = 'world'
|
||||
language = 'C++'
|
||||
)");
|
||||
ASSERT_TRUE(cfg.update(table));
|
||||
ASSERT_EQ(cfg.map().at("one"), 1);
|
||||
ASSERT_EQ(cfg.map().at("two"), 2);
|
||||
ASSERT_EQ(cfg.string_to_string().at("hello"), "world");
|
||||
ASSERT_EQ(cfg.string_to_string().at("language"), "C++");
|
||||
|
||||
XLOGF(INFO, "toString: {}", cfg.toString());
|
||||
}
|
||||
|
||||
TEST(TestConfig, Set) {
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_HOT_UPDATED_ITEM(set, std::set<std::string>{});
|
||||
} cfg;
|
||||
ASSERT_TRUE(cfg.set().empty());
|
||||
|
||||
toml::table table = toml::parse(R"(
|
||||
set = ["one", "two"]
|
||||
)");
|
||||
ASSERT_TRUE(cfg.update(table));
|
||||
ASSERT_EQ(cfg.set().count("one"), 1);
|
||||
ASSERT_EQ(cfg.set().count("two"), 1);
|
||||
ASSERT_EQ(cfg.set().count("three"), 0);
|
||||
|
||||
XLOGF(INFO, "toString: {}", cfg.toString());
|
||||
ASSERT_EQ(cfg.toString(), "set = [ 'one', 'two' ]");
|
||||
}
|
||||
|
||||
TEST(TestConfig, AtomicallyUpdate) {
|
||||
std::string_view str = R"(
|
||||
[sect]
|
||||
val = 123
|
||||
)";
|
||||
|
||||
Config cfg;
|
||||
ASSERT_TRUE(cfg.atomicallyUpdate(str));
|
||||
ASSERT_EQ(cfg.sect().val(), 123);
|
||||
cfg.sect().set_val(100);
|
||||
|
||||
folly::test::TemporaryFile file;
|
||||
::write(file.fd(), str.data(), str.length());
|
||||
ASSERT_TRUE(cfg.atomicallyUpdate(file.path()));
|
||||
ASSERT_EQ(cfg.sect().val(), 123);
|
||||
}
|
||||
|
||||
TEST(TestConfig, VariantType) {
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_SECT(type1, {
|
||||
CONFIG_HOT_UPDATED_ITEM(a, 0);
|
||||
CONFIG_HOT_UPDATED_ITEM(b, 0);
|
||||
CONFIG_HOT_UPDATED_ITEM(c, 0);
|
||||
});
|
||||
CONFIG_SECT(type2, {
|
||||
CONFIG_HOT_UPDATED_ITEM(x, 0);
|
||||
CONFIG_HOT_UPDATED_ITEM(y, 0);
|
||||
CONFIG_HOT_UPDATED_ITEM(z, 0);
|
||||
});
|
||||
CONFIG_VARIANT_TYPE("type1");
|
||||
};
|
||||
|
||||
Config config;
|
||||
XLOGF(INFO, "{}", config.toString());
|
||||
ASSERT_OK(config.validate());
|
||||
|
||||
ASSERT_TRUE(config.set_type("type2"));
|
||||
XLOGF(INFO, "{}", config.toString());
|
||||
ASSERT_OK(config.validate());
|
||||
|
||||
ASSERT_FALSE(config.set_type("type3"));
|
||||
}
|
||||
|
||||
TEST(TestConfig, UpdateCallback) {
|
||||
Config config;
|
||||
|
||||
auto cnt = 0;
|
||||
auto guard = config.addCallbackGuard([&] { ++cnt; });
|
||||
|
||||
ASSERT_EQ(cnt, 0);
|
||||
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
|
||||
ASSERT_EQ(cnt, 1);
|
||||
|
||||
guard->dismiss();
|
||||
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
|
||||
ASSERT_EQ(cnt, 1);
|
||||
|
||||
guard = config.addCallbackGuard();
|
||||
guard->setCallback([&] { cnt += 7; });
|
||||
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
|
||||
ASSERT_EQ(cnt, 8);
|
||||
}
|
||||
|
||||
TEST(TestConfig, HotUpdatedMacro) {
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_SECT(cold, {
|
||||
CONFIG_ITEM(a, 0);
|
||||
CONFIG_ITEM(b, (std::vector<int>{1, 2, 3}));
|
||||
CONFIG_ITEM(c, (std::map<std::string, int>{{"one", 1}, {"two", 2}}));
|
||||
});
|
||||
CONFIG_SECT(hot, {
|
||||
CONFIG_HOT_UPDATED_ITEM(a, 0);
|
||||
CONFIG_HOT_UPDATED_ITEM(b, (std::vector<int>{1, 2, 3}));
|
||||
CONFIG_HOT_UPDATED_ITEM(c, (std::map<std::string, int>{{"one", 1}, {"two", 2}}));
|
||||
});
|
||||
};
|
||||
|
||||
Config config;
|
||||
|
||||
// hot update but keep value, succ.
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
a = 0
|
||||
b = [1, 2, 3]
|
||||
c = { one = 1, two = 2 }
|
||||
)")));
|
||||
|
||||
// hot update and change value, fail.
|
||||
ASSERT_FALSE(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
a = 1
|
||||
)")));
|
||||
ASSERT_FALSE(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
b = [1, 2]
|
||||
)")));
|
||||
ASSERT_FALSE(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
c = { one = 1 }
|
||||
)")));
|
||||
|
||||
// not a hot update. succ.
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
a = 1
|
||||
)"),
|
||||
false));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
b = [1, 2]
|
||||
)"),
|
||||
false));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[cold]
|
||||
c = { one = 1 }
|
||||
)"),
|
||||
false));
|
||||
|
||||
ASSERT_EQ(config.cold().a(), 1);
|
||||
ASSERT_EQ(config.cold().b(), (std::vector<int>{1, 2}));
|
||||
ASSERT_EQ(config.cold().c(), (std::map<std::string, int>{{"one", 1}}));
|
||||
|
||||
// support hot updated. succ.
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
a = 0
|
||||
b = [1, 2, 3]
|
||||
c = { one = 1, two = 2 }
|
||||
)")));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
a = 1
|
||||
)")));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
b = [1, 2]
|
||||
)")));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
c = { one = 1 }
|
||||
)")));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
a = 1
|
||||
)"),
|
||||
false));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
b = [1, 2]
|
||||
)"),
|
||||
false));
|
||||
ASSERT_OK(config.update(toml::parse(R"(
|
||||
[hot]
|
||||
c = { one = 1 }
|
||||
)"),
|
||||
false));
|
||||
}
|
||||
|
||||
TEST(TestConfig, DiffWith) {
|
||||
auto stringfy = [](const config::IConfig::ItemDiff &diff) {
|
||||
return fmt::format("{}: {} -> {}", diff.key, diff.left, diff.right);
|
||||
};
|
||||
|
||||
config::IConfig::ItemDiff diffs[3];
|
||||
Config c0;
|
||||
Config c1;
|
||||
ASSERT_EQ(c0.diffWith(c1, std::span(diffs)), 0);
|
||||
|
||||
c1.set_optional("nonnull");
|
||||
c1.sect().set_foo("abc");
|
||||
c1.sect().sub().set_val(101);
|
||||
auto diffCnt = c0.diffWith(c1, std::span(diffs));
|
||||
ASSERT_EQ(diffCnt, 3);
|
||||
ASSERT_EQ(stringfy(diffs[0]), "optional: nullopt -> 'nonnull'");
|
||||
ASSERT_EQ(stringfy(diffs[1]), "sect.foo: 'foo' -> 'abc'");
|
||||
ASSERT_EQ(stringfy(diffs[2]), "sect.sub.val: 100 -> 101");
|
||||
|
||||
c1.sect().sub().set_foo("");
|
||||
ASSERT_EQ(c0.diffWith(c1, std::span(diffs)), 3);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
42
tests/common/utils/TestCoroSynchronized.cc
Normal file
42
tests/common/utils/TestCoroSynchronized.cc
Normal file
@@ -0,0 +1,42 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
|
||||
#include "common/utils/CoroSynchronized.h"
|
||||
#include "common/utils/CountDownLatch.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
folly::CPUThreadPoolExecutor &getExecutor() {
|
||||
static folly::CPUThreadPoolExecutor executor(4, std::make_shared<folly::NamedThreadFactory>("Test"));
|
||||
return executor;
|
||||
}
|
||||
|
||||
TEST(CoroSynchronizedTest, testInc) {
|
||||
CoroSynchronized<int64_t> count(0);
|
||||
constexpr int64_t countPerAdder = 10000;
|
||||
constexpr int64_t adderCount = 10;
|
||||
|
||||
CountDownLatch latch(adderCount);
|
||||
|
||||
auto adder = [&]() -> CoTask<void> {
|
||||
for (int64_t i = 0; i < countPerAdder; ++i) {
|
||||
auto ptr = co_await count.coLock();
|
||||
++*ptr;
|
||||
}
|
||||
latch.countDown();
|
||||
};
|
||||
|
||||
for (int64_t i = 0; i < adderCount; ++i) {
|
||||
adder().scheduleOn(&getExecutor()).start();
|
||||
}
|
||||
|
||||
auto result = folly::coro::blockingWait([&]() -> CoTask<int64_t> {
|
||||
co_await latch.wait();
|
||||
auto ptr = co_await count.coSharedLock();
|
||||
co_return *ptr;
|
||||
}());
|
||||
|
||||
ASSERT_EQ(result, adderCount * countPerAdder);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
49
tests/common/utils/TestCoroutinesPool.cc
Normal file
49
tests/common/utils/TestCoroutinesPool.cc
Normal file
@@ -0,0 +1,49 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
|
||||
#include "common/utils/CoroutinesPool.h"
|
||||
#include "common/utils/CountDownLatch.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
void testCoroutinesPool(bool enableWorkStealing) {
|
||||
struct Job {
|
||||
int cnt = 0;
|
||||
};
|
||||
|
||||
folly::CPUThreadPoolExecutor executor(4);
|
||||
|
||||
CoroutinesPool<Job>::Config config;
|
||||
config.set_enable_work_stealing(enableWorkStealing);
|
||||
CoroutinesPool<Job> pool(config, &executor);
|
||||
|
||||
SCOPE_EXIT { pool.stopAndJoin(); };
|
||||
|
||||
int n = 100;
|
||||
CountDownLatch latch(2 * n);
|
||||
|
||||
std::atomic<int> sum = 0;
|
||||
pool.start([&sum, &latch](Job job) -> CoTask<void> {
|
||||
sum += job.cnt;
|
||||
latch.countDown();
|
||||
co_return;
|
||||
});
|
||||
|
||||
for (auto i = 0; i < n; ++i) {
|
||||
pool.enqueueSync(Job{i + 1});
|
||||
folly::coro::blockingWait(pool.enqueue(Job{i}));
|
||||
}
|
||||
|
||||
folly::coro::blockingWait(latch.wait());
|
||||
|
||||
ASSERT_EQ(sum, 10000);
|
||||
}
|
||||
|
||||
TEST(TestCoroutinesPool, Normal) { testCoroutinesPool(false); }
|
||||
|
||||
TEST(TestCoroutinesPool, WorkStealing) { testCoroutinesPool(true); }
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
52
tests/common/utils/TestDefaultRetryStrategy.cc
Normal file
52
tests/common/utils/TestDefaultRetryStrategy.cc
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/DefaultRetryStrategy.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
struct MockSleeper {
|
||||
CoTask<void> operator()(std::chrono::milliseconds d) {
|
||||
records.push_back(d);
|
||||
co_return;
|
||||
}
|
||||
|
||||
std::vector<std::chrono::milliseconds> records;
|
||||
};
|
||||
|
||||
DefaultRetryStrategy<MockSleeper> getDefaultRetryStrategy() {
|
||||
return DefaultRetryStrategy<MockSleeper>(RetryConfig{10ms, 100ms, 5});
|
||||
}
|
||||
|
||||
TEST(DefaultRetryStrategy, basic) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
{
|
||||
auto retryStrategy = getDefaultRetryStrategy();
|
||||
// kDataCorruption is unretryable
|
||||
auto r = co_await retryStrategy.onError(Status(StatusCode::kDataCorruption));
|
||||
EXPECT_TRUE(r.hasError());
|
||||
EXPECT_EQ(r.error().code(), StatusCode::kDataCorruption);
|
||||
EXPECT_EQ(retryStrategy.getSleeper().records.size(), 0);
|
||||
}
|
||||
{
|
||||
auto retryStrategy = getDefaultRetryStrategy();
|
||||
// kThrottled is retryable
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
auto r = co_await retryStrategy.onError(Status(TransactionCode::kThrottled));
|
||||
EXPECT_TRUE(!r.hasError());
|
||||
}
|
||||
{
|
||||
auto r = co_await retryStrategy.onError(Status(TransactionCode::kThrottled));
|
||||
EXPECT_TRUE(r.hasError());
|
||||
EXPECT_EQ(r.error().code(), TransactionCode::kThrottled);
|
||||
}
|
||||
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
|
||||
EXPECT_EQ(retryStrategy.getSleeper().records, expected);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
121
tests/common/utils/TestDownwardBytes.cc
Normal file
121
tests/common/utils/TestDownwardBytes.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "common/net/Allocator.h"
|
||||
#include "common/utils/DownwardBytes.h"
|
||||
#include "common/utils/Size.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs {
|
||||
namespace {
|
||||
|
||||
std::vector<std::pair<size_t, bool>> logger;
|
||||
|
||||
struct Allocator {
|
||||
public:
|
||||
static constexpr size_t kDefaultSize = 1_MB;
|
||||
static std::uint8_t *allocate(size_t size) {
|
||||
size = std::max(size, kDefaultSize);
|
||||
logger.emplace_back(size, true);
|
||||
return new uint8_t[size];
|
||||
}
|
||||
|
||||
static void deallocate(const uint8_t *buf, size_t size) {
|
||||
size = std::max(size, kDefaultSize);
|
||||
logger.emplace_back(size, false);
|
||||
delete[] buf;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(TestDownwardBytes, Normal) {
|
||||
ASSERT_EQ(logger.size(), 0);
|
||||
|
||||
{
|
||||
DownwardBytes<Allocator> bytes;
|
||||
ASSERT_EQ(bytes.capacity(), 1_MB);
|
||||
ASSERT_EQ(logger.size(), 1);
|
||||
ASSERT_EQ(logger.back().first, 1_MB);
|
||||
ASSERT_TRUE(logger.back().second);
|
||||
}
|
||||
ASSERT_EQ(logger.size(), 2);
|
||||
ASSERT_EQ(logger.back().first, 1_MB);
|
||||
ASSERT_FALSE(logger.back().second);
|
||||
|
||||
{
|
||||
DownwardBytes<Allocator> bytes;
|
||||
auto old = bytes.data();
|
||||
ASSERT_EQ(bytes.capacity(), 1_MB);
|
||||
ASSERT_EQ(logger.size(), 3);
|
||||
ASSERT_EQ(logger.back().first, 1_MB);
|
||||
ASSERT_TRUE(logger.back().second);
|
||||
|
||||
std::vector<uint8_t> empty(512_KB, 'A');
|
||||
bytes.append(empty.data(), empty.size());
|
||||
ASSERT_EQ(bytes.data() + 512_KB, old);
|
||||
ASSERT_EQ(bytes.data()[0], 'A');
|
||||
ASSERT_EQ(bytes.data()[512_KB - 1], 'A');
|
||||
ASSERT_EQ(bytes.capacity(), 1_MB);
|
||||
ASSERT_EQ(bytes.size(), 512_KB);
|
||||
ASSERT_EQ(logger.size(), 3);
|
||||
|
||||
std::fill(empty.begin(), empty.end(), 'B');
|
||||
bytes.append(empty.data(), empty.size());
|
||||
ASSERT_EQ(bytes.data() + 1_MB, old);
|
||||
ASSERT_EQ(bytes.data()[0], 'B');
|
||||
ASSERT_EQ(bytes.data()[512_KB - 1], 'B');
|
||||
ASSERT_EQ(bytes.data()[512_KB], 'A');
|
||||
ASSERT_EQ(bytes.data()[1_MB - 1], 'A');
|
||||
ASSERT_EQ(bytes.capacity(), 1_MB);
|
||||
ASSERT_EQ(bytes.size(), 1_MB);
|
||||
ASSERT_EQ(logger.size(), 3);
|
||||
|
||||
std::fill(empty.begin(), empty.end(), 'C');
|
||||
bytes.append(empty.data(), 1);
|
||||
ASSERT_NE(bytes.data() + 1_MB + 1, old);
|
||||
ASSERT_EQ(bytes.data()[0], 'C');
|
||||
ASSERT_EQ(bytes.data()[1], 'B');
|
||||
ASSERT_EQ(bytes.data()[512_KB], 'B');
|
||||
ASSERT_EQ(bytes.data()[512_KB + 1], 'A');
|
||||
ASSERT_EQ(bytes.data()[1_MB], 'A');
|
||||
ASSERT_EQ(bytes.capacity(), 2_MB);
|
||||
ASSERT_EQ(bytes.size(), 1_MB + 1);
|
||||
ASSERT_EQ(logger.size(), 5);
|
||||
ASSERT_EQ(logger[3].first, 2_MB);
|
||||
ASSERT_TRUE(logger[3].second);
|
||||
ASSERT_EQ(logger[4].first, 1_MB);
|
||||
ASSERT_FALSE(logger[4].second);
|
||||
|
||||
uint32_t offset, capacity;
|
||||
auto buf = bytes.release(offset, capacity);
|
||||
ASSERT_EQ(logger.size(), 5);
|
||||
ASSERT_EQ(bytes.data(), nullptr);
|
||||
ASSERT_EQ(bytes.size(), 0);
|
||||
ASSERT_EQ(bytes.capacity(), 0);
|
||||
ASSERT_EQ(offset, 1_MB - 1);
|
||||
ASSERT_EQ(capacity, 2_MB);
|
||||
ASSERT_EQ(buf[offset], 'C');
|
||||
Allocator::deallocate(buf, capacity);
|
||||
}
|
||||
ASSERT_EQ(logger.size(), 6);
|
||||
ASSERT_EQ(logger.back().first, 2_MB);
|
||||
ASSERT_FALSE(logger.back().second);
|
||||
|
||||
{
|
||||
DownwardBytes<Allocator> bytes;
|
||||
ASSERT_EQ(bytes.capacity(), 1_MB);
|
||||
ASSERT_EQ(logger.size(), 7);
|
||||
ASSERT_EQ(logger.back().first, 1_MB);
|
||||
ASSERT_TRUE(logger.back().second);
|
||||
|
||||
std::vector<uint8_t> empty(2_MB, 'A');
|
||||
bytes.append(empty.data(), empty.size());
|
||||
ASSERT_EQ(logger.size(), 9);
|
||||
ASSERT_EQ(logger.back().first, 1_MB);
|
||||
ASSERT_FALSE(logger.back().second);
|
||||
ASSERT_EQ(logger[7].first, 2_MB);
|
||||
ASSERT_TRUE(logger[7].second);
|
||||
}
|
||||
ASSERT_EQ(logger.size(), 10);
|
||||
ASSERT_EQ(logger.back().first, 2_MB);
|
||||
ASSERT_FALSE(logger.back().second);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs
|
||||
76
tests/common/utils/TestDuration.cc
Normal file
76
tests/common/utils/TestDuration.cc
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
static_assert(config::IsPrimitive<Duration>, "duration is not primitive");
|
||||
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_HOT_UPDATED_ITEM(c0, Duration::from("0s").value());
|
||||
CONFIG_HOT_UPDATED_ITEM(c1, 0_ns);
|
||||
};
|
||||
|
||||
TEST(Duration, testDefaultConstruct) {
|
||||
Duration config;
|
||||
ASSERT_EQ(config.asMs(), std::chrono::milliseconds{0});
|
||||
}
|
||||
|
||||
TEST(Duration, testParse) {
|
||||
Duration c0 = Duration::from("100ns").value();
|
||||
ASSERT_EQ(c0.toString(), "100ns");
|
||||
Duration c1 = Duration::from("2us").value();
|
||||
ASSERT_EQ(c1.toString(), "2us");
|
||||
Duration c2 = Duration::from("1001ms").value();
|
||||
ASSERT_EQ(c2.toString(), "1s 1ms");
|
||||
Duration c3 = Duration::from("45s ").value();
|
||||
ASSERT_EQ(c3.toString(), "45s");
|
||||
Duration c5 = Duration::from("08h").value();
|
||||
ASSERT_EQ(c5.toString(), "8h");
|
||||
Duration c6 = Duration::from("1day").value();
|
||||
ASSERT_EQ(c6.toString(), "1day");
|
||||
ASSERT_FALSE(Duration::from("10mon"));
|
||||
ASSERT_FALSE(Duration::from(""));
|
||||
ASSERT_FALSE(Duration::from("100"));
|
||||
ASSERT_FALSE(Duration::from("ns"));
|
||||
ASSERT_FALSE(Duration::from("1.1s"));
|
||||
|
||||
ASSERT_ERROR(Duration::from("1ns2ms"), StatusCode::kInvalidConfig);
|
||||
ASSERT_ERROR(Duration::from("1ns2ns"), StatusCode::kInvalidConfig);
|
||||
ASSERT_ERROR(Duration::from("1ns2k"), StatusCode::kInvalidConfig);
|
||||
ASSERT_ERROR(Duration::from("1ms2.1us"), StatusCode::kInvalidConfig);
|
||||
ASSERT_ERROR(Duration::from("ms2us"), StatusCode::kInvalidConfig);
|
||||
ASSERT_ERROR(Duration::from("1day2sec"), StatusCode::kInvalidConfig);
|
||||
|
||||
ASSERT_EQ(Duration::from("1day 2s")->toString(), "1day 2s");
|
||||
ASSERT_EQ(Duration::from("1day 2h 2001 ms")->toString(), "1day 2h 2s 1ms");
|
||||
}
|
||||
|
||||
TEST(Duration, testConfig) {
|
||||
Config cfg;
|
||||
ASSERT_EQ(cfg.c0(), Duration::from("0s").value());
|
||||
ASSERT_EQ(cfg.c0(), cfg.c1());
|
||||
cfg.set_c0(Duration::from("10ms").value());
|
||||
ASSERT_EQ(cfg.c0(), Duration::from("10ms").value());
|
||||
|
||||
toml::table result = toml::parse(R"(
|
||||
c0 = "5s"
|
||||
c1 = "10min"
|
||||
)");
|
||||
ASSERT_TRUE(cfg.update(result));
|
||||
ASSERT_EQ(cfg.c0(), Duration::from("5s").value());
|
||||
ASSERT_EQ(cfg.c1(), Duration::from("10min").value());
|
||||
|
||||
XLOGF(INFO, "toString: {}", cfg.toString());
|
||||
|
||||
result = toml::parse(cfg.toString());
|
||||
Config other;
|
||||
ASSERT_TRUE(other.update(result));
|
||||
ASSERT_EQ(cfg.c0(), other.c0());
|
||||
ASSERT_EQ(cfg.c1(), other.c1());
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
77
tests/common/utils/TestDynamicCoroutinesPool.cc
Normal file
77
tests/common/utils/TestDynamicCoroutinesPool.cc
Normal file
@@ -0,0 +1,77 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/Baton.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/DynamicCoroutinesPool.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestDynamicCoroutinesPool, Normal) {
|
||||
DynamicCoroutinesPool::Config config;
|
||||
config.set_coroutines_num(8);
|
||||
DynamicCoroutinesPool pool(config);
|
||||
ASSERT_OK(pool.start());
|
||||
ASSERT_OK(pool.stopAndJoin());
|
||||
|
||||
DynamicCoroutinesPool{config};
|
||||
}
|
||||
|
||||
TEST(TestDynamicCoroutinesPool, ManyTasks) {
|
||||
constexpr auto N = 200;
|
||||
constexpr auto M = 64;
|
||||
DynamicCoroutinesPool::Config config;
|
||||
config.set_coroutines_num(M);
|
||||
DynamicCoroutinesPool pool(config);
|
||||
ASSERT_OK(pool.start());
|
||||
|
||||
folly::coro::Baton baton;
|
||||
std::atomic<uint32_t> cnt{};
|
||||
std::atomic<uint32_t> current{};
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
pool.enqueue(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
auto now = ++current;
|
||||
CO_ASSERT_LE(now, M);
|
||||
co_await folly::coro::sleep(50_ms);
|
||||
if (++cnt == N) {
|
||||
baton.post();
|
||||
}
|
||||
--current;
|
||||
}));
|
||||
}
|
||||
|
||||
folly::coro::blockingWait(baton);
|
||||
}
|
||||
|
||||
TEST(TestDynamicCoroutinesPool, HotUpdated) {
|
||||
constexpr auto N = 64;
|
||||
DynamicCoroutinesPool::Config config;
|
||||
config.set_coroutines_num(1);
|
||||
DynamicCoroutinesPool pool(config);
|
||||
ASSERT_OK(pool.start());
|
||||
|
||||
folly::coro::Baton baton;
|
||||
std::atomic<uint32_t> cnt{};
|
||||
std::atomic<uint32_t> current{};
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
pool.enqueue(folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
auto now = ++current;
|
||||
config.set_coroutines_num(now + 1);
|
||||
config.update(toml::table{}, true);
|
||||
co_await folly::coro::sleep(50_ms);
|
||||
if (++cnt == N) {
|
||||
baton.post();
|
||||
}
|
||||
--current;
|
||||
}));
|
||||
}
|
||||
|
||||
folly::coro::blockingWait(baton);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
225
tests/common/utils/TestFaultInjection.cc
Normal file
225
tests/common/utils/TestFaultInjection.cc
Normal file
@@ -0,0 +1,225 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/concurrency/UnboundedQueue.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/IOThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/AsyncGenerator.h>
|
||||
#include <folly/experimental/coro/Baton.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/BoundedQueue.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/ScopeExit.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/experimental/coro/Task.h>
|
||||
#include <folly/experimental/coro/UnboundedQueue.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <folly/io/async/Request.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/FaultInjection.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestFaultInjection, Basic) {
|
||||
static constexpr size_t kLoops = 100000;
|
||||
|
||||
auto fi = FaultInjection::get();
|
||||
ASSERT_EQ(fi, nullptr);
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
auto inject = FAULT_INJECTION();
|
||||
ASSERT_FALSE(inject);
|
||||
}
|
||||
|
||||
FAULT_INJECTION_SET(1, -1); // 1% probility and unlimited times
|
||||
ASSERT_EQ(FaultInjection::get()->getProbability(), 1);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 1);
|
||||
size_t cnt = 0;
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
if (FAULT_INJECTION()) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(cnt, 0);
|
||||
std::cout << "Inject " << cnt << " faults during " << kLoops << " loops, " << (double)cnt / kLoops * 100
|
||||
<< "%, expect 1%" << std::endl;
|
||||
|
||||
{
|
||||
// disable fault injection in current scope
|
||||
FAULT_INJECTION_SET(0, -1);
|
||||
size_t cnt = 0;
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
if (FAULT_INJECTION()) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(cnt, 0);
|
||||
}
|
||||
ASSERT_EQ(FaultInjection::get()->getProbability(), 1);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 1);
|
||||
|
||||
// test set factor scope.
|
||||
{
|
||||
FAULT_INJECTION_SET_FACTOR(2);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 2);
|
||||
{
|
||||
FAULT_INJECTION_SET_FACTOR(3);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 3);
|
||||
}
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 2);
|
||||
|
||||
FAULT_INJECTION_SET_FACTOR(4);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 4);
|
||||
}
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 1);
|
||||
|
||||
{
|
||||
FAULT_INJECTION_SET_FACTOR(10);
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 10);
|
||||
size_t cnt = 0;
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
if (FAULT_INJECTION()) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(cnt, 0);
|
||||
std::cout << "Set factor of current scope to 10, inject " << cnt << " faults during " << kLoops << " loops, "
|
||||
<< (double)cnt / kLoops * 100 << "%, expect 0.1%" << std::endl;
|
||||
}
|
||||
ASSERT_EQ(FaultInjectionFactor::get(), 1);
|
||||
|
||||
{
|
||||
size_t cnt = 0;
|
||||
FAULT_INJECTION_SET_FACTOR(100); // should be ignored
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
if (FAULT_INJECTION_WITH_FACTOR(5)) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(cnt, 0);
|
||||
std::cout << "Inject with factor 5, inject " << cnt << " faults during " << kLoops << " loops, "
|
||||
<< (double)cnt / kLoops * 100 << "%, expect 0.2%" << std::endl;
|
||||
}
|
||||
|
||||
{
|
||||
FAULT_INJECTION_SET(10, 10);
|
||||
size_t cnt = 0;
|
||||
for (size_t i = 0; i < kLoops; i++) {
|
||||
if (FAULT_INJECTION()) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(cnt, 10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestFaultInjection, Coroutine) {
|
||||
ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
|
||||
folly::coro::blockingWait([]() -> folly::coro::Task<void> {
|
||||
folly::RequestContext::create();
|
||||
FAULT_INJECTION_SET(10, -1);
|
||||
co_return;
|
||||
}());
|
||||
ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
|
||||
FAULT_INJECTION_SET(10, -1);
|
||||
auto fi = FaultInjection::get();
|
||||
ASSERT_NE(fi, nullptr);
|
||||
|
||||
folly::coro::blockingWait([]() -> folly::coro::Task<void> {
|
||||
folly::RequestContext::create();
|
||||
FAULT_INJECTION_SET(10, -1);
|
||||
co_return;
|
||||
}());
|
||||
ASSERT_EQ(FaultInjection::get(), fi);
|
||||
|
||||
{
|
||||
folly::CPUThreadPoolExecutor executor(4);
|
||||
folly::coro::co_invoke([fi]() -> folly::coro::Task<void> {
|
||||
CO_ASSERT_EQ(FaultInjection::get(), fi);
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&executor)
|
||||
.start()
|
||||
.wait();
|
||||
executor.join();
|
||||
}
|
||||
|
||||
{
|
||||
FAULT_INJECTION_SET(10, 1000);
|
||||
folly::CPUThreadPoolExecutor executor(4);
|
||||
std::vector<folly::SemiFuture<size_t>> futures;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto future = folly::coro::co_invoke([&]() -> folly::coro::Task<size_t> {
|
||||
size_t cnts = 0;
|
||||
for (int i = 0; i < 100000; i++) {
|
||||
if (FAULT_INJECTION()) {
|
||||
cnts++;
|
||||
}
|
||||
if (folly::Random::oneIn(100)) co_await folly::coro::co_reschedule_on_current_executor_t();
|
||||
}
|
||||
co_return cnts;
|
||||
})
|
||||
.scheduleOn(&executor)
|
||||
.start();
|
||||
futures.push_back(std::move(future));
|
||||
}
|
||||
auto results = folly::collectAll(futures.begin(), futures.end()).wait().result().value();
|
||||
size_t cnts = 0;
|
||||
for (auto &result : results) {
|
||||
cnts += result.value();
|
||||
}
|
||||
executor.join();
|
||||
ASSERT_EQ(cnts, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestFaultInjection, Coroutine1) {
|
||||
folly::CPUThreadPoolExecutor exec(1);
|
||||
auto coro1 = [](FaultInjection *ptr) -> CoTask<void> {
|
||||
CO_ASSERT_EQ(FaultInjection::get(), ptr);
|
||||
co_return;
|
||||
};
|
||||
auto coro2 = [&]() -> CoTask<void> {
|
||||
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
FAULT_INJECTION_SET(10, 1);
|
||||
co_await coro1(FaultInjection::get()).scheduleOn(&exec).start();
|
||||
co_return;
|
||||
};
|
||||
coro2().scheduleOn(&exec).start().wait();
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
TEST(TestFaultInjection, Coroutine2) {
|
||||
folly::CPUThreadPoolExecutor exec(1);
|
||||
auto coro1 = []() -> CoTask<void> {
|
||||
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
folly::RequestContext::create();
|
||||
FAULT_INJECTION_SET(10, 1);
|
||||
CO_ASSERT_NE(folly::RequestContext::try_get(), nullptr);
|
||||
CO_ASSERT_NE(FaultInjection::get(), nullptr);
|
||||
co_return;
|
||||
};
|
||||
auto coro2 = [&]() -> CoTask<void> {
|
||||
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
co_await coro1().scheduleOn(&exec).start();
|
||||
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
|
||||
co_return;
|
||||
};
|
||||
coro2().scheduleOn(&exec).start().wait();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
24
tests/common/utils/TestFdWrapper.cc
Normal file
24
tests/common/utils/TestFdWrapper.cc
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/FdWrapper.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestFdWrapper, Normal) {
|
||||
int p[2];
|
||||
ASSERT_EQ(::pipe(p), 0);
|
||||
|
||||
FdWrapper fd;
|
||||
ASSERT_EQ(fd, -1);
|
||||
|
||||
fd = p[0];
|
||||
fd = FdWrapper{p[1]}; // p[0] is closed.
|
||||
ASSERT_EQ(fd, p[1]);
|
||||
|
||||
fd = FdWrapper{}; // p[1] is closed.
|
||||
ASSERT_EQ(fd, -1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
14
tests/common/utils/TestFmt.cc
Normal file
14
tests/common/utils/TestFmt.cc
Normal file
@@ -0,0 +1,14 @@
|
||||
#include <fmt/chrono.h>
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
TEST(Fmt, testFormat) {
|
||||
ASSERT_EQ(fmt::format("Hello {}", "world"), "Hello world");
|
||||
ASSERT_EQ(fmt::format("{}", std::vector<std::string>{"\naan"}), "[\"\\naan\"]");
|
||||
ASSERT_EQ(fmt::format("{:%S}", std::chrono::milliseconds(1234)), "01.234");
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
27
tests/common/utils/TestFolly.cc
Normal file
27
tests/common/utils/TestFolly.cc
Normal file
@@ -0,0 +1,27 @@
|
||||
#include <folly/hash/Checksum.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
|
||||
TEST(Folly, CRC32C) {
|
||||
std::string a = "hello";
|
||||
auto crc1 = folly::crc32c(reinterpret_cast<const uint8_t *>(a.data()), a.size(), 0);
|
||||
|
||||
std::string b = "world";
|
||||
auto crc2 = folly::crc32c(reinterpret_cast<const uint8_t *>(b.data()), b.size(), 0);
|
||||
|
||||
auto x = folly::crc32c_combine(crc1, crc2, b.size());
|
||||
auto y = folly::crc32c(reinterpret_cast<const uint8_t *>(b.data()), b.size(), crc1);
|
||||
ASSERT_EQ(x, y);
|
||||
|
||||
auto crc32c1 = ~0x14298C12; // 1MB zero.
|
||||
auto crc32c2 = ~0x527D5351; // one zero.
|
||||
auto out = folly::crc32c_combine(~crc32c1, crc32c2, 1);
|
||||
XLOGF(INFO, "{:08X} {:08X}", out, ~out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
16
tests/common/utils/TestHashCombine.cc
Normal file
16
tests/common/utils/TestHashCombine.cc
Normal file
@@ -0,0 +1,16 @@
|
||||
#include <cstdint>
|
||||
#include <folly/hash/Hash.h>
|
||||
|
||||
#include "common/utils/RobinHoodUtils.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
TEST(HashCombine, testUint64) {
|
||||
for (uint64_t x = 0; x < 64; ++x) {
|
||||
auto h = folly::hash::hash_combine_generic(RobinHoodHasher{}, x);
|
||||
fmt::print("x = {} h = {} h % 256 = {}\n", x, h, h % 256);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
175
tests/common/utils/TestIdAllocator.cc
Normal file
175
tests/common/utils/TestIdAllocator.cc
Normal file
@@ -0,0 +1,175 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/Synchronized.h>
|
||||
#include <folly/Unit.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "common/kv/mem/MemKVEngine.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/FaultInjection.h"
|
||||
#include "common/utils/IdAllocator.h"
|
||||
#include "fdb/FDBRetryStrategy.h"
|
||||
#include "fdb/FDBTransaction.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
namespace hf3fs::test {
|
||||
|
||||
kv::FDBRetryStrategy retryStrategy() { return kv::FDBRetryStrategy({.retryMaybeCommitted = true}); }
|
||||
|
||||
TEST(TestIdAllocator, Basic) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
kv::MemKVEngine engine;
|
||||
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 1);
|
||||
|
||||
for (size_t i = 1; i <= 1000; i++) {
|
||||
auto result = co_await allocator.allocate();
|
||||
CO_ASSERT_OK(result);
|
||||
CO_ASSERT_EQ(result.value(), i);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST(TestIdAllocator, MultiThreads) {
|
||||
kv::MemKVEngine engine;
|
||||
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 1);
|
||||
|
||||
std::atomic<size_t> failed = 0;
|
||||
folly::Synchronized<std::vector<uint64_t>, std::mutex> allocated;
|
||||
std::vector<folly::SemiFuture<folly::Unit>> futures;
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
for (size_t i = 0; i < 128; i++) {
|
||||
auto task = [&]() -> CoTask<void> {
|
||||
for (size_t i = 1; i <= 1000; i++) {
|
||||
auto result = co_await allocator.allocate();
|
||||
if (result.hasError()) {
|
||||
CO_ASSERT_ERROR(result, TransactionCode::kConflict);
|
||||
failed++;
|
||||
} else {
|
||||
allocated.lock()->push_back(result.value());
|
||||
}
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
}
|
||||
co_return;
|
||||
};
|
||||
futures.push_back(folly::coro::co_invoke(task).scheduleOn(&exec).start());
|
||||
}
|
||||
|
||||
for (auto &f : futures) {
|
||||
f.wait();
|
||||
}
|
||||
|
||||
auto lock = allocated.lock();
|
||||
std::sort(lock->begin(), lock->end());
|
||||
fmt::print("success {}, failed {}, first {}, last {}\n",
|
||||
lock->size(),
|
||||
failed.load(),
|
||||
*lock->begin(),
|
||||
*lock->rbegin());
|
||||
for (size_t i = 1; i < lock->size(); i++) {
|
||||
ASSERT_GT(lock->at(i), lock->at(i - 1));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestIdAllocator, Sharded) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
kv::MemKVEngine engine;
|
||||
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 8);
|
||||
|
||||
std::vector<uint64_t> allocated;
|
||||
for (size_t i = 1; i <= 1000; i++) {
|
||||
auto result = co_await allocator.allocate();
|
||||
CO_ASSERT_OK(result);
|
||||
allocated.push_back(result.value());
|
||||
}
|
||||
|
||||
std::sort(allocated.begin(), allocated.end());
|
||||
for (size_t i = 1; i < allocated.size(); i++) {
|
||||
CO_ASSERT_GT(allocated.at(i), allocated.at(i - 1));
|
||||
}
|
||||
|
||||
auto status = co_await allocator.status();
|
||||
CO_ASSERT_OK(status);
|
||||
std::sort(status->begin(), status->end());
|
||||
fmt::print("shard usage: min {}, max {}\n", *status->begin(), *status->rbegin());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST(TestIdAllocator, ShardedMultiThreads) {
|
||||
kv::MemKVEngine engine;
|
||||
std::vector<std::unique_ptr<IdAllocator<kv::FDBRetryStrategy>>> vec;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
vec.emplace_back(std::make_unique<IdAllocator<kv::FDBRetryStrategy>>(engine, retryStrategy(), "test", 32));
|
||||
}
|
||||
|
||||
std::atomic<size_t> failed = 0;
|
||||
folly::Synchronized<std::vector<uint64_t>, std::mutex> allocated;
|
||||
std::vector<folly::SemiFuture<folly::Unit>> futures;
|
||||
folly::CPUThreadPoolExecutor exec(8);
|
||||
for (size_t i = 0; i < 128; i++) {
|
||||
auto task = [&, &allocator = vec[i % vec.size()]]() -> CoTask<void> {
|
||||
for (size_t i = 1; i <= 1000; i++) {
|
||||
auto result = co_await allocator->allocate();
|
||||
if (result.hasError()) {
|
||||
CO_ASSERT_ERROR(result, TransactionCode::kConflict);
|
||||
failed++;
|
||||
} else {
|
||||
allocated.lock()->push_back(result.value());
|
||||
}
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
}
|
||||
co_return;
|
||||
};
|
||||
futures.push_back(folly::coro::co_invoke(task).scheduleOn(&exec).start());
|
||||
}
|
||||
|
||||
for (auto &f : futures) {
|
||||
f.wait();
|
||||
}
|
||||
|
||||
auto lock = allocated.lock();
|
||||
std::sort(lock->begin(), lock->end());
|
||||
fmt::print("success {}, failed {}, min {}, max {}\n", lock->size(), failed.load(), *lock->begin(), *lock->rbegin());
|
||||
for (size_t i = 1; i < lock->size(); i++) {
|
||||
ASSERT_GT(lock->at(i), lock->at(i - 1));
|
||||
}
|
||||
|
||||
auto status = vec[0]->status().scheduleOn(&exec).start().wait().value();
|
||||
ASSERT_OK(status);
|
||||
std::sort(status->begin(), status->end());
|
||||
fmt::print("shard usage: min {}, max {}\n", *status->begin(), *status->rbegin());
|
||||
}
|
||||
|
||||
TEST(TestIdAllocator, FaultInjection) {
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
FAULT_INJECTION_SET(2, -1);
|
||||
kv::MemKVEngine engine;
|
||||
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 8);
|
||||
std::set<uint64_t> set;
|
||||
size_t fail = 0;
|
||||
for (size_t i = 1; i <= 1000; i++) {
|
||||
auto result = co_await allocator.allocate();
|
||||
if (result.hasValue()) {
|
||||
CO_ASSERT_FALSE(set.contains(result.value()));
|
||||
set.insert(result.value());
|
||||
} else {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "success " << set.size() << ", fail " << fail << std::endl;
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace hf3fs::test
|
||||
23
tests/common/utils/TestInt128.cc
Normal file
23
tests/common/utils/TestInt128.cc
Normal file
@@ -0,0 +1,23 @@
|
||||
#include <fmt/format.h>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
|
||||
#include "common/utils/Int128.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestInt128, Normal) {
|
||||
auto value = std::numeric_limits<uint128_t>::max();
|
||||
ASSERT_EQ(fmt::format("{}", value), "340282366920938463463374607431768211455");
|
||||
|
||||
std::set<uint128_t> set;
|
||||
set.insert(value);
|
||||
ASSERT_EQ(set.size(), 1ul);
|
||||
ASSERT_TRUE(set.count(value));
|
||||
ASSERT_FALSE(set.count(0));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
85
tests/common/utils/TestLibUring.cc
Normal file
85
tests/common/utils/TestLibUring.cc
Normal file
@@ -0,0 +1,85 @@
|
||||
#include <fcntl.h>
|
||||
#include <liburing.h>
|
||||
#include <liburing/io_uring.h>
|
||||
|
||||
#include "common/utils/FdWrapper.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestLibUring, Normal) {
|
||||
constexpr auto N = 1024;
|
||||
FdWrapper fd{::open("/dev/zero", O_RDONLY)};
|
||||
ASSERT_TRUE(fd);
|
||||
|
||||
struct io_uring ring;
|
||||
ASSERT_EQ(io_uring_queue_init(N, &ring, 0), 0);
|
||||
|
||||
std::string buf(N, 'A');
|
||||
struct iovec iovec;
|
||||
iovec.iov_base = buf.data();
|
||||
iovec.iov_len = N;
|
||||
|
||||
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
|
||||
ASSERT_NE(sqe, nullptr);
|
||||
io_uring_prep_readv(sqe, fd, &iovec, 1, 0);
|
||||
io_uring_sqe_set_data(sqe, nullptr);
|
||||
|
||||
ASSERT_EQ(io_uring_submit(&ring), 1);
|
||||
|
||||
struct io_uring_cqe *cqe;
|
||||
ASSERT_EQ(::io_uring_wait_cqe(&ring, &cqe), 0);
|
||||
ASSERT_EQ(cqe->res, N);
|
||||
::io_uring_cqe_seen(&ring, cqe);
|
||||
|
||||
ASSERT_EQ(buf, std::string(N, '\0'));
|
||||
|
||||
::io_uring_queue_exit(&ring);
|
||||
}
|
||||
|
||||
TEST(TestLibUring, SQPolling) {
|
||||
if (getuid()) {
|
||||
// skip if non-root.
|
||||
GTEST_SKIP_("Skip SQPolling if non-root");
|
||||
}
|
||||
|
||||
constexpr auto N = 256;
|
||||
FdWrapper fd{::open("/dev/zero", O_RDONLY)};
|
||||
ASSERT_TRUE(fd);
|
||||
|
||||
struct io_uring ring {};
|
||||
struct io_uring_params params {};
|
||||
params.flags = IORING_SETUP_SQPOLL;
|
||||
params.sq_thread_idle = 50;
|
||||
ASSERT_EQ(io_uring_queue_init_params(N, &ring, ¶ms), 0);
|
||||
|
||||
int files[1] = {fd};
|
||||
ASSERT_EQ(io_uring_register_files(&ring, files, 1), 0);
|
||||
|
||||
std::string buf(N, 'A');
|
||||
struct iovec iovec;
|
||||
iovec.iov_base = buf.data();
|
||||
iovec.iov_len = N;
|
||||
ASSERT_EQ(io_uring_register_buffers(&ring, &iovec, 1), 0);
|
||||
|
||||
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
|
||||
ASSERT_NE(sqe, nullptr);
|
||||
io_uring_prep_read_fixed(sqe, 0, iovec.iov_base, N, 0, 0);
|
||||
sqe->flags |= IOSQE_FIXED_FILE;
|
||||
io_uring_sqe_set_data(sqe, nullptr);
|
||||
|
||||
ASSERT_EQ(io_uring_submit(&ring), 1);
|
||||
|
||||
struct io_uring_cqe *cqe;
|
||||
ASSERT_EQ(::io_uring_wait_cqe(&ring, &cqe), 0);
|
||||
ASSERT_EQ(cqe->res, N);
|
||||
::io_uring_cqe_seen(&ring, cqe);
|
||||
|
||||
ASSERT_EQ(buf, std::string(N, '\0'));
|
||||
|
||||
::io_uring_queue_exit(&ring);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
136
tests/common/utils/TestLockManager.cc
Normal file
136
tests/common/utils/TestLockManager.cc
Normal file
@@ -0,0 +1,136 @@
|
||||
#include <chrono>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/LockManager.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestLockManager, Normal) {
|
||||
UniqueLockManager lockManager;
|
||||
|
||||
auto lock1 = lockManager.lock(123);
|
||||
auto lock2 = lockManager.lock(std::string{"yes"});
|
||||
|
||||
auto lock3 = lockManager.try_lock(124);
|
||||
ASSERT_TRUE(lock3.owns_lock());
|
||||
|
||||
auto lock4 = lockManager.try_lock(123);
|
||||
ASSERT_FALSE(lock4.owns_lock());
|
||||
|
||||
lock1.unlock();
|
||||
|
||||
auto lock5 = lockManager.try_lock(123);
|
||||
ASSERT_TRUE(lock5.owns_lock());
|
||||
|
||||
auto lock6 = lockManager.try_lock(std::string{"yes"});
|
||||
ASSERT_FALSE(lock6.owns_lock());
|
||||
}
|
||||
|
||||
TEST(TestLockManager, SharedLock) {
|
||||
SharedLockManager lockManager;
|
||||
|
||||
auto lock = lockManager.lock_shared(123);
|
||||
std::jthread([&] { auto anotherLock = lockManager.lock_shared(123); });
|
||||
}
|
||||
|
||||
TEST(TestLockManager, FairSharedLock) {
|
||||
SharedLockManager lockManager;
|
||||
|
||||
std::jthread read1([&] {
|
||||
// 0ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.lock_shared(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "read1 lock wait {}ms", elapsed.count());
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 200ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 300ms
|
||||
lock.unlock();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 400ms
|
||||
lock.lock();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 500ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 600ms
|
||||
});
|
||||
|
||||
std::jthread read2([&] {
|
||||
// 0ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 200ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.lock_shared(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "read2 lock wait {}ms", elapsed.count());
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 300ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 400ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 500ms
|
||||
lock.unlock();
|
||||
});
|
||||
|
||||
std::jthread write([&] {
|
||||
// 0ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.lock(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "write lock wait {}ms", elapsed.count());
|
||||
|
||||
ASSERT_NEAR(elapsed.count(), 200, 50);
|
||||
});
|
||||
}
|
||||
|
||||
TEST(TestLockManager, CoMutex) {
|
||||
CoUniqueLockManager lockManager;
|
||||
|
||||
folly::coro::blockingWait([&]() -> folly::coro::Task<void> {
|
||||
auto lock1 = co_await lockManager.co_scoped_lock(123);
|
||||
auto lock2 = co_await lockManager.co_scoped_lock(std::string{"yes"});
|
||||
|
||||
auto lock3 = lockManager.try_lock(124);
|
||||
CO_ASSERT_TRUE(lock3.owns_lock());
|
||||
|
||||
auto lock4 = lockManager.try_lock(123);
|
||||
CO_ASSERT_FALSE(lock4.owns_lock());
|
||||
|
||||
lock1.unlock();
|
||||
|
||||
auto lock5 = lockManager.try_lock(123);
|
||||
CO_ASSERT_TRUE(lock5.owns_lock());
|
||||
|
||||
auto lock6 = lockManager.try_lock(std::string{"yes"});
|
||||
CO_ASSERT_FALSE(lock6.owns_lock());
|
||||
}());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
82
tests/common/utils/TestLruCache.cc
Normal file
82
tests/common/utils/TestLruCache.cc
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/LruCache.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestLruCache, Normal) {
|
||||
LruCache<int, int> cache(4);
|
||||
ASSERT_TRUE(cache.empty());
|
||||
ASSERT_EQ(cache.size(), 0);
|
||||
ASSERT_EQ(cache.begin(), cache.end());
|
||||
|
||||
cache[1] = 1;
|
||||
cache[2] = 2;
|
||||
cache[3] = 3;
|
||||
cache[4] = 4;
|
||||
ASSERT_FALSE(cache.empty());
|
||||
ASSERT_EQ(cache.size(), 4);
|
||||
ASSERT_NE(cache.begin(), cache.end());
|
||||
|
||||
auto it = cache.begin();
|
||||
ASSERT_EQ(it->second, 4);
|
||||
++it;
|
||||
ASSERT_EQ(it->second, 3);
|
||||
++it;
|
||||
ASSERT_EQ(it->second, 2);
|
||||
++it;
|
||||
ASSERT_EQ(it->second, 1);
|
||||
++it;
|
||||
ASSERT_EQ(it, cache.end());
|
||||
|
||||
cache[1] = 1;
|
||||
ASSERT_EQ(cache.front().second, 1);
|
||||
ASSERT_EQ(cache.back().second, 2);
|
||||
|
||||
ASSERT_TRUE(cache.emplace(5, 5).second);
|
||||
ASSERT_EQ(cache.front().second, 5);
|
||||
ASSERT_EQ(cache.back().second, 3);
|
||||
ASSERT_EQ(cache.size(), 4);
|
||||
|
||||
cache.promote(cache.find(1));
|
||||
ASSERT_EQ(cache.front().second, 1);
|
||||
ASSERT_EQ(cache.back().second, 3);
|
||||
|
||||
cache.obsolete(cache.find(1));
|
||||
ASSERT_EQ(cache.front().second, 5);
|
||||
ASSERT_EQ(cache.back().second, 1);
|
||||
|
||||
it = cache.erase(5);
|
||||
ASSERT_EQ(it, cache.begin());
|
||||
it = cache.erase(cache.find(1));
|
||||
ASSERT_EQ(it, cache.end());
|
||||
|
||||
ASSERT_EQ(cache.size(), 2);
|
||||
auto idx = 0;
|
||||
for (auto &pair : cache) {
|
||||
ASSERT_EQ(pair.first, 4 - idx++);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestLruCache, ManuallyRemove) {
|
||||
constexpr auto N = 4;
|
||||
constexpr auto M = 8;
|
||||
|
||||
LruCache<int, int> cache(N, false);
|
||||
for (auto i = 0; i < M; ++i) {
|
||||
cache[i] = i;
|
||||
}
|
||||
ASSERT_EQ(cache.size(), M);
|
||||
|
||||
cache.evictObsoletedIf([](int, int value) { return value < 2; });
|
||||
ASSERT_EQ(cache.size(), 6);
|
||||
ASSERT_EQ(cache.back().second, 2);
|
||||
|
||||
cache.evictObsoleted();
|
||||
ASSERT_EQ(cache.size(), N);
|
||||
ASSERT_EQ(cache.back().second, 4);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
31
tests/common/utils/TestMPSCQueue.cc
Normal file
31
tests/common/utils/TestMPSCQueue.cc
Normal file
@@ -0,0 +1,31 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
#include "common/utils/MPSCQueue.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestMPSCQueue, Normal) {
|
||||
constexpr auto N = 4096u;
|
||||
|
||||
MPSCQueue<size_t> que(N);
|
||||
for (auto i = 0u; i < N; ++i) {
|
||||
auto item = std::make_unique<size_t>(i);
|
||||
ASSERT_TRUE(que.push(&item));
|
||||
ASSERT_TRUE(item == nullptr);
|
||||
}
|
||||
auto more = std::make_unique<size_t>(0);
|
||||
ASSERT_EQ(que.push(&more).code(), StatusCode::kQueueFull);
|
||||
ASSERT_TRUE(more != nullptr);
|
||||
|
||||
for (auto i = 0u; i < N; ++i) {
|
||||
std::unique_ptr<size_t> item;
|
||||
ASSERT_TRUE(que.pop(&item));
|
||||
ASSERT_TRUE(item != nullptr);
|
||||
}
|
||||
ASSERT_EQ(que.pop(&more).code(), StatusCode::kQueueEmpty);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
100
tests/common/utils/TestMemoryAllocator.cc
Normal file
100
tests/common/utils/TestMemoryAllocator.cc
Normal file
@@ -0,0 +1,100 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Semaphore.h"
|
||||
#include "common/utils/SemaphoreGuard.h"
|
||||
#include "common/utils/Size.h"
|
||||
#include "memory/common/GlobalMemoryAllocator.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestMemoryAllocator, Basic) {
|
||||
size_t totalSize = 512_MB;
|
||||
size_t blockSize = 16_KB;
|
||||
std::vector<void *> memBlocks;
|
||||
|
||||
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
|
||||
auto mem = memory::allocate(blockSize);
|
||||
memBlocks.push_back(mem);
|
||||
}
|
||||
|
||||
char logbuf[512] = {0};
|
||||
memory::logstatus(logbuf, sizeof(logbuf));
|
||||
XLOGF(INFO, "Memory allocator log: {}", logbuf);
|
||||
|
||||
for (auto mem : memBlocks) {
|
||||
memory::deallocate(mem);
|
||||
}
|
||||
|
||||
memory::logstatus(logbuf, sizeof(logbuf));
|
||||
XLOGF(INFO, "Memory allocator log: {}", logbuf);
|
||||
}
|
||||
|
||||
TEST(TestMemoryAllocator, AlignedAlloc) {
|
||||
static size_t kPageSize = sysconf(_SC_PAGESIZE);
|
||||
size_t totalSize = 512_MB;
|
||||
size_t blockSize = 100_KB;
|
||||
std::vector<void *> memBlocks;
|
||||
|
||||
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
|
||||
auto mem = memory::memalign(kPageSize, blockSize);
|
||||
memBlocks.push_back(mem);
|
||||
}
|
||||
|
||||
char logbuf[512] = {0};
|
||||
memory::logstatus(logbuf, sizeof(logbuf));
|
||||
XLOGF(INFO, "Memory allocator log: {}", logbuf);
|
||||
|
||||
for (auto mem : memBlocks) {
|
||||
memory::deallocate(mem);
|
||||
}
|
||||
|
||||
memory::logstatus(logbuf, sizeof(logbuf));
|
||||
XLOGF(INFO, "Memory allocator log: {}", logbuf);
|
||||
}
|
||||
|
||||
/* Set the following environment variables before run the profiling test
|
||||
export JE_MALLOC_CONF="prof:true,prof_active:false"
|
||||
export MEMORY_ALLOCATOR_LIB_PATH=build/src/memory/jemalloc/libjemalloc_wrapper.so
|
||||
*/
|
||||
TEST(TestMemoryAllocator, Profiling) {
|
||||
size_t totalSize = 512_MB;
|
||||
size_t blockSize = 16_KB;
|
||||
std::vector<void *> memBlocks;
|
||||
|
||||
auto enabled = memory::profiling(true /*active*/, "prof");
|
||||
|
||||
// skip this test if profiling is not supported
|
||||
if (!enabled) return;
|
||||
|
||||
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
|
||||
auto mem = memory::allocate(blockSize);
|
||||
memBlocks.push_back(mem);
|
||||
}
|
||||
|
||||
// re-enable to dump profiling
|
||||
ASSERT_TRUE(memory::profiling(true /*active*/, nullptr));
|
||||
|
||||
// disable and dump profiling
|
||||
ASSERT_TRUE(memory::profiling(false /*active*/, "prof"));
|
||||
|
||||
// enable profiling
|
||||
enabled = memory::profiling(true /*active*/, "prof");
|
||||
|
||||
for (auto mem : memBlocks) {
|
||||
memory::deallocate(mem);
|
||||
}
|
||||
|
||||
// disable and dump profiling
|
||||
ASSERT_TRUE(memory::profiling(false /*active*/, nullptr));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
71
tests/common/utils/TestObjectPool.cc
Normal file
71
tests/common/utils/TestObjectPool.cc
Normal file
@@ -0,0 +1,71 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/ObjectPool.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestObjectPool, Normal) {
|
||||
constexpr auto X = 123;
|
||||
static size_t destructTimes = 0;
|
||||
|
||||
struct A {
|
||||
~A() { ++destructTimes; }
|
||||
int x = X;
|
||||
};
|
||||
A *rawPtr = nullptr;
|
||||
|
||||
{
|
||||
auto a = ObjectPool<A>::get();
|
||||
ASSERT_NE(a.get(), nullptr);
|
||||
ASSERT_EQ(a->x, X);
|
||||
a->x = 0;
|
||||
rawPtr = a.get();
|
||||
ASSERT_EQ(destructTimes, 0);
|
||||
|
||||
a.reset();
|
||||
ASSERT_EQ(destructTimes, 1);
|
||||
}
|
||||
ASSERT_EQ(destructTimes, 1);
|
||||
ASSERT_EQ(ObjectPool<A>::get().get(), rawPtr);
|
||||
ASSERT_EQ(ObjectPool<A>::get()->x, X);
|
||||
}
|
||||
|
||||
TEST(TestObjectPool, Allocate) {
|
||||
constexpr auto N = 1000000;
|
||||
static size_t constructTimes = 0;
|
||||
static size_t destructTimes = 0;
|
||||
struct A {
|
||||
A() { ++constructTimes; }
|
||||
~A() { ++destructTimes; }
|
||||
};
|
||||
|
||||
{
|
||||
std::vector<ObjectPool<A>::Ptr> vec{N};
|
||||
for (auto &item : vec) {
|
||||
item = ObjectPool<A>::get();
|
||||
}
|
||||
ASSERT_EQ(constructTimes, N);
|
||||
ASSERT_EQ(destructTimes, 0);
|
||||
}
|
||||
ASSERT_EQ(destructTimes, N);
|
||||
}
|
||||
|
||||
TEST(TestObjectPool, AllocateAndRelease) {
|
||||
constexpr auto N = 1000000;
|
||||
static size_t constructTimes = 0;
|
||||
static size_t destructTimes = 0;
|
||||
struct A {
|
||||
A() { ++constructTimes; }
|
||||
~A() { ++destructTimes; }
|
||||
};
|
||||
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
ObjectPool<A>::get();
|
||||
}
|
||||
ASSERT_EQ(constructTimes, N);
|
||||
ASSERT_EQ(destructTimes, N);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
47
tests/common/utils/TestPriorityUnboundedQueue.cc
Normal file
47
tests/common/utils/TestPriorityUnboundedQueue.cc
Normal file
@@ -0,0 +1,47 @@
|
||||
#include <chrono>
|
||||
#include <folly/Executor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/PriorityUnboundedQueue.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestPriorityBoundedQueue, Normal) {
|
||||
PriorityUnboundedQueue<int> queue(3);
|
||||
ASSERT_TRUE(queue.size() == 0);
|
||||
|
||||
queue.addWithPriority(1, folly::Executor::LO_PRI);
|
||||
queue.addWithPriority(2, folly::Executor::HI_PRI);
|
||||
queue.addWithPriority(3, folly::Executor::MID_PRI);
|
||||
queue.addWithPriority(4, folly::Executor::HI_PRI);
|
||||
|
||||
ASSERT_EQ(queue.dequeue(), 2);
|
||||
ASSERT_EQ(queue.dequeue(), 4);
|
||||
ASSERT_EQ(queue.dequeue(), 3);
|
||||
ASSERT_EQ(queue.dequeue(), 1);
|
||||
}
|
||||
|
||||
TEST(TestPriorityBoundedQueue, Many) {
|
||||
PriorityUnboundedQueue<int> queue(3);
|
||||
ASSERT_TRUE(queue.size() == 0);
|
||||
|
||||
std::jthread a([&]() {
|
||||
for (size_t i = 0; i < 2000000; i++) {
|
||||
ASSERT_EQ(queue.dequeue(), 1);
|
||||
}
|
||||
});
|
||||
std::jthread b([&]() {
|
||||
for (size_t i = 0; i < 1000000; i++) {
|
||||
queue.addWithPriority(1, folly::Executor::HI_PRI);
|
||||
queue.addWithPriority(1, folly::Executor::LO_PRI);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
115
tests/common/utils/TestReentrantLockManager.cc
Normal file
115
tests/common/utils/TestReentrantLockManager.cc
Normal file
@@ -0,0 +1,115 @@
|
||||
#include <chrono>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/ReentrantLockManager.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(TestReentrantLockManager, Normal) {
|
||||
ReentrantLockManager lockManager;
|
||||
lockManager.init();
|
||||
|
||||
auto lock1 = lockManager.writeLock(123);
|
||||
auto lock2 = lockManager.writeLock(456);
|
||||
|
||||
auto lock3 = lockManager.tryWriteLock(124);
|
||||
ASSERT_TRUE(lock3.writeLocked());
|
||||
|
||||
auto lock4 = lockManager.tryWriteLock(123);
|
||||
ASSERT_FALSE(lock4.writeLocked());
|
||||
|
||||
lock1.unlock();
|
||||
|
||||
auto lock5 = lockManager.tryWriteLock(123);
|
||||
ASSERT_TRUE(lock5.writeLocked());
|
||||
|
||||
auto lock6 = lockManager.tryWriteLock(456);
|
||||
ASSERT_FALSE(lock6.writeLocked());
|
||||
}
|
||||
|
||||
TEST(TestReentrantLockManager, SharedLock) {
|
||||
ReentrantLockManager lockManager;
|
||||
lockManager.init();
|
||||
|
||||
auto lock = lockManager.readLock(123);
|
||||
std::jthread([&] { auto anotherLock = lockManager.readLock(123); });
|
||||
}
|
||||
|
||||
TEST(TestReentrantLockManager, FairSharedLock) {
|
||||
ReentrantLockManager lockManager;
|
||||
lockManager.init();
|
||||
|
||||
std::jthread read1([&] {
|
||||
// 0ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.readLock(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "read1 lock wait {}ms", elapsed.count());
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 200ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 300ms
|
||||
lock.unlock();
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 400ms
|
||||
lock = lockManager.readLock(1);
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 500ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 600ms
|
||||
});
|
||||
|
||||
std::jthread read2([&] {
|
||||
// 0ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 200ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.readLock(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "read2 lock wait {}ms", elapsed.count());
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 300ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 400ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 500ms
|
||||
lock.unlock();
|
||||
});
|
||||
|
||||
std::jthread write([&] {
|
||||
// 0ms
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// 100ms
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto lock = lockManager.writeLock(1);
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
|
||||
XLOGF(INFO, "write lock wait {}ms", elapsed.count());
|
||||
|
||||
ASSERT_NEAR(elapsed.count(), 200, 50);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
27
tests/common/utils/TestReleaseVersion.cc
Normal file
27
tests/common/utils/TestReleaseVersion.cc
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "common/app/AppInfo.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestReleaseVersion, testFormat) {
|
||||
auto rv = flat::ReleaseVersion::fromVersionInfoV0();
|
||||
ASSERT_EQ(fmt::format("Version: v{}", rv), VersionInfo::fullV0());
|
||||
|
||||
rv = flat::ReleaseVersion::fromVersionInfo();
|
||||
ASSERT_EQ(fmt::format("Version: v{}", rv), VersionInfo::full());
|
||||
}
|
||||
|
||||
TEST(TestReleaseVersion, testDeserializeFromV0) {
|
||||
auto rv = flat::ReleaseVersion::fromV0(0, 1, 4, 0xabcdef12, 1688016296, 54321);
|
||||
auto str = serde::serialize(rv);
|
||||
auto unpackRes = flat::ReleaseVersion::unpackFrom(str);
|
||||
ASSERT_TRUE(unpackRes);
|
||||
auto newRv = *unpackRes;
|
||||
ASSERT_EQ(newRv.toString(), "0.1.4-54321-20230629-abcdef12");
|
||||
auto rv2 = flat::ReleaseVersion::fromVersionInfo();
|
||||
ASSERT_TRUE(rv2 > rv);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
187
tests/common/utils/TestRenderConfig.cc
Normal file
187
tests/common/utils/TestRenderConfig.cc
Normal file
@@ -0,0 +1,187 @@
|
||||
#include <cstdlib>
|
||||
|
||||
#include "common/utils/RenderConfig.h"
|
||||
#include "common/utils/Toml.hpp"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
String toPrettyToml(const String &s) {
|
||||
auto t = toml::parse(s);
|
||||
std::stringstream ss;
|
||||
ss << toml::toml_formatter(t, toml::toml_formatter::default_flags & ~toml::format_flags::indentation);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
TEST(RenderConfig, testNormalV0) {
|
||||
flat::AppInfo app;
|
||||
app.nodeId = flat::NodeId(2);
|
||||
app.hostname = "hostname";
|
||||
app.pid = 100;
|
||||
app.tags = {flat::TagPair("k1", "v1"), flat::TagPair("k2"), flat::TagPair("k3", "v3")};
|
||||
app.releaseVersion = flat::ReleaseVersion::fromV0(0, 1, 2, 0, 0, 100);
|
||||
|
||||
::setenv("TEST_RENDER_CONFIG", "abc", 1);
|
||||
|
||||
String rawConfig = R"(
|
||||
[server]
|
||||
{% if app.nodeId in [1, 3, 5] %}
|
||||
queue_size = 10
|
||||
{% elif hasTagValue(app, "k1", "v1") %}
|
||||
queue_size = 20
|
||||
{% else %}
|
||||
queue_size = 30
|
||||
{% endif %}
|
||||
|
||||
{% if app.hostname is startsWith("host") %}
|
||||
zzz = 1
|
||||
{% else %}
|
||||
zzz = 0
|
||||
{% endif %}
|
||||
|
||||
xxx = {{ "\"abc\"" if app is hasTag("k2") else "\"def\""}}
|
||||
|
||||
port0 = {{ 10003 % 2 }}
|
||||
port1 = {{ 10007 % 2 }}
|
||||
|
||||
version_less_than = {{ relaseVersionCompare(app, "<", "0.1.2") }}
|
||||
version_less_than_or_equal_to = {{ relaseVersionCompare(app, "<=", "0.1.2-100") }}
|
||||
version_equal_to = {{ relaseVersionCompare(app, "==", "0.1.2") }}
|
||||
version_greater_than = {{ releaseVersionCompare(app, ">", "0.1.2-100") }}
|
||||
version_greater_than_or_equal_to = {{ releaseVersionCompare(app, ">=", "0.1.2") }}
|
||||
version_not_equal_to = {{ releaseVersionCompare(app, "!=", "0.1.2-100") }}
|
||||
|
||||
yyy = "{{ env.TEST_RENDER_CONFIG }}"
|
||||
|
||||
log_level = '{{ env.HF3FS_LOG_LEVEL if env.HF3FS_LOG_LEVEL else "INFO" }}'
|
||||
|
||||
# TODO: implement startswith
|
||||
{% if app.hostname == "hostname" %}
|
||||
enable_fast_mode = true
|
||||
{% endif %})";
|
||||
|
||||
auto res = renderConfig(rawConfig, &app);
|
||||
ASSERT_OK(res);
|
||||
|
||||
auto resConfig = toPrettyToml(res.value());
|
||||
|
||||
auto expected = toPrettyToml(R"(
|
||||
[server]
|
||||
xxx = 'abc'
|
||||
yyy = 'abc'
|
||||
zzz = 1
|
||||
log_level = 'INFO'
|
||||
version_less_than = false
|
||||
version_less_than_or_equal_to = true
|
||||
version_equal_to = true
|
||||
version_greater_than_or_equal_to = true
|
||||
version_greater_than = false
|
||||
version_not_equal_to = false
|
||||
port0 = 1
|
||||
port1 = 1
|
||||
queue_size = 20 # hit second case
|
||||
enable_fast_mode = true)");
|
||||
|
||||
ASSERT_EQ(resConfig, expected);
|
||||
}
|
||||
|
||||
TEST(RenderConfig, testTemplateNotParsed) {
|
||||
flat::AppInfo app;
|
||||
app.nodeId = flat::NodeId(2);
|
||||
app.hostname = "hostname";
|
||||
app.pid = 100;
|
||||
app.tags = {flat::TagPair("k1", "v1"), flat::TagPair("k2"), flat::TagPair("k3", "v3")};
|
||||
app.releaseVersion = flat::ReleaseVersion::fromV0(0, 1, 2, 0, 0, 0);
|
||||
|
||||
::setenv("TEST_RENDER_CONFIG", "abc", 1);
|
||||
|
||||
String rawConfig = R"(
|
||||
[server]
|
||||
{% if app.nodeId in (1, 3, 5) %}
|
||||
queue_size = 10
|
||||
{% elif app.hostname not in ('hostname') %}
|
||||
queue_size = 10
|
||||
{% elif hasTagValue(app, "k1", "v1") %}
|
||||
queue_size = 20
|
||||
{% else %}
|
||||
queue_size = 30
|
||||
{% endif %}
|
||||
|
||||
xxx = {{ "\"abc\"" if app is hasTag("k2") else "\"def\""}}
|
||||
|
||||
{% if app.releaseVersion.major == 0 and app.releaseVersion.minor == 1 and app.releaseVersion.patch == 0 %}
|
||||
yyy = 1
|
||||
{% endif %}
|
||||
|
||||
version_less_than = {{ relaseVersionCompare(app, "<", "0.1.2") }}
|
||||
version_less_than_or_equal_to = {{ relaseVersionCompare(app, "<=", "0.1.2") }}
|
||||
version_equal_to = {{ relaseVersionCompare(app, "==", "0.1.2") }}
|
||||
version_greater_than = {{ relaseVersionCompare(app, ">", "0.1.2") }}
|
||||
version_greater_than_or_equal_to = {{ relaseVersionCompare(app, ">=", "0.1.2") }}
|
||||
version_not_equal_to = {{ relaseVersionCompare(app, "!=", "0.1.2") }}
|
||||
|
||||
yyy = "{{ env.TEST_RENDER_CONFIG }}"
|
||||
|
||||
log_level = '{{ env.HF3FS_LOG_LEVEL if env.HF3FS_LOG_LEVEL else "INFO" }}'
|
||||
|
||||
# TODO: implement startswith
|
||||
{% if app.hostname == "hostname" %}
|
||||
enable_fast_mode = true
|
||||
{% endif %})";
|
||||
|
||||
auto res = renderConfig(rawConfig, &app);
|
||||
ASSERT_ERROR(res, StatusCode::kInvalidConfig);
|
||||
ASSERT_TRUE(res.error().message().find("app.hostname not in") != std::string_view::npos);
|
||||
}
|
||||
|
||||
#define ASSERT_REDNER_EQ(raw, app, expected) \
|
||||
do { \
|
||||
auto &&_raw = (raw); \
|
||||
auto &&_app = (app); \
|
||||
auto &&_expected = (expected); \
|
||||
auto _res = renderConfig(_raw, _app); \
|
||||
ASSERT_OK(_res); \
|
||||
auto _resConfig = toPrettyToml(_res.value()); \
|
||||
ASSERT_EQ(_resConfig, _expected); \
|
||||
} while (0)
|
||||
|
||||
TEST(RenderConfig, testNormalV1) {
|
||||
String config0 = R"(
|
||||
x = {{ app.nodeId in [1, 2, 3, 4, 5] }}
|
||||
{% set port = app.nodeId % 2 %}
|
||||
{% if port == 0 %}
|
||||
target_paths = ["/storage/data1/hf3fs", "/storage/data2/hf3fs", "/storage/data3/hf3fs", "/storage/data4/hf3fs", "/storage/data5/hf3fs", "/storage/data6/hf3fs", "/storage/data7/hf3fs", "/storage/data8/hf3fs"]
|
||||
{% else %}
|
||||
target_paths = ["/storage/data9/hf3fs", "/storage/data10/hf3fs", "/storage/data11/hf3fs", "/storage/data12/hf3fs", "/storage/data13/hf3fs", "/storage/data14/hf3fs", "/storage/data15/hf3fs", "/storage/data16/hf3fs"]
|
||||
{% endif %}
|
||||
)";
|
||||
|
||||
String config1 = R"(
|
||||
x = {{ app.nodeId in range(1, 6) }}
|
||||
target_paths = [ {% for i in range(8) %} "/storage/data{{i + 8 * (app.nodeId % 2) + 1}}/hf3fs", {% endfor %} ]
|
||||
)";
|
||||
|
||||
flat::AppInfo app0;
|
||||
app0.nodeId = flat::NodeId(0);
|
||||
|
||||
auto expected0 = toPrettyToml(R"(
|
||||
x = false
|
||||
target_paths = ["/storage/data1/hf3fs", "/storage/data2/hf3fs", "/storage/data3/hf3fs", "/storage/data4/hf3fs", "/storage/data5/hf3fs", "/storage/data6/hf3fs", "/storage/data7/hf3fs", "/storage/data8/hf3fs"]
|
||||
)");
|
||||
|
||||
ASSERT_REDNER_EQ(config0, &app0, expected0);
|
||||
ASSERT_REDNER_EQ(config1, &app0, expected0);
|
||||
|
||||
flat::AppInfo app1;
|
||||
app1.nodeId = flat::NodeId(1);
|
||||
|
||||
auto expected1 = toPrettyToml(R"(
|
||||
x = true
|
||||
target_paths = ["/storage/data9/hf3fs", "/storage/data10/hf3fs", "/storage/data11/hf3fs", "/storage/data12/hf3fs", "/storage/data13/hf3fs", "/storage/data14/hf3fs", "/storage/data15/hf3fs", "/storage/data16/hf3fs"]
|
||||
)");
|
||||
|
||||
ASSERT_REDNER_EQ(config0, &app1, expected1);
|
||||
ASSERT_REDNER_EQ(config1, &app1, expected1);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
103
tests/common/utils/TestRequestContext.cc
Normal file
103
tests/common/utils/TestRequestContext.cc
Normal file
@@ -0,0 +1,103 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/concurrency/UnboundedQueue.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/IOThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/AsyncGenerator.h>
|
||||
#include <folly/experimental/coro/Baton.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/BoundedQueue.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/experimental/coro/ScopeExit.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <folly/experimental/coro/Task.h>
|
||||
#include <folly/experimental/coro/UnboundedQueue.h>
|
||||
#include <folly/fibers/Semaphore.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <folly/io/async/Request.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
namespace hf3fs::test {
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(TestRequestContext, FibersBaton) {
|
||||
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1) + 1);
|
||||
folly::fibers::Baton baton;
|
||||
auto producer = folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
co_await folly::coro::sleep(std::chrono::seconds(1));
|
||||
folly::RequestContext::create();
|
||||
baton.post();
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&exec)
|
||||
.start();
|
||||
auto consumer = folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
co_await baton;
|
||||
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
})
|
||||
.scheduleOn(&exec)
|
||||
.start();
|
||||
producer.wait();
|
||||
consumer.wait();
|
||||
}
|
||||
|
||||
TEST(TestRequestContext, CoroBaton) {
|
||||
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1) + 1);
|
||||
folly::coro::Baton baton;
|
||||
auto producer = folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
co_await folly::coro::sleep(std::chrono::seconds(1));
|
||||
folly::RequestContext::create();
|
||||
baton.post();
|
||||
co_return;
|
||||
})
|
||||
.scheduleOn(&exec)
|
||||
.start();
|
||||
auto consumer = folly::coro::co_invoke([&]() -> CoTask<void> {
|
||||
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
co_await baton;
|
||||
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
|
||||
})
|
||||
.scheduleOn(&exec)
|
||||
.start();
|
||||
producer.wait();
|
||||
consumer.wait();
|
||||
}
|
||||
|
||||
TEST(TestRequestContext, Semaphore) {
|
||||
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1, 8));
|
||||
folly::fibers::Semaphore semaphore(folly::Random::rand32(1, 8));
|
||||
auto worker = [&]() -> CoTask<void> {
|
||||
for (size_t i = 0; i < folly::Random::rand32(1000, 2000); i++) {
|
||||
std::optional<folly::ShallowCopyRequestContextScopeGuard> guard;
|
||||
if (folly::Random::oneIn(2)) {
|
||||
guard.emplace();
|
||||
}
|
||||
auto ctx = folly::RequestContext::try_get();
|
||||
co_await semaphore.co_wait();
|
||||
SCOPE_EXIT { semaphore.signal(); };
|
||||
CO_ASSERT_EQ(ctx, folly::RequestContext::try_get());
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<folly::SemiFuture<Void>> tasks;
|
||||
for (size_t i = 0; i < 64; i++) {
|
||||
tasks.push_back(folly::coro::co_invoke(worker).scheduleOn(&exec).start());
|
||||
}
|
||||
for (auto &task : tasks) {
|
||||
task.wait();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
50
tests/common/utils/TestScnLib.cc
Normal file
50
tests/common/utils/TestScnLib.cc
Normal file
@@ -0,0 +1,50 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <scn/scn.h>
|
||||
#include <scn/tuple_return.h>
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
TEST(TestScnlib, ScanString) {
|
||||
std::string word;
|
||||
auto result = scn::scan("Hello world!", "{}", word);
|
||||
|
||||
ASSERT_EQ(word, "Hello");
|
||||
ASSERT_EQ(result.range_as_string(), " world!");
|
||||
}
|
||||
|
||||
TEST(TestScnlib, ScanMultipleValues) {
|
||||
int i, j;
|
||||
auto result = scn::scan("123 456 foo", "{} {}", i, j);
|
||||
|
||||
ASSERT_EQ(i, 123);
|
||||
ASSERT_EQ(j, 456);
|
||||
|
||||
std::string str;
|
||||
auto ret = scn::scan(result.range(), "{}", str);
|
||||
ASSERT_EQ(static_cast<bool>(ret), true);
|
||||
ASSERT_EQ(str, "foo");
|
||||
}
|
||||
|
||||
TEST(TestScnlib, ScanTuple) {
|
||||
{
|
||||
auto [r, i] = scn::scan_tuple<int>("42", "{}");
|
||||
ASSERT_EQ(static_cast<bool>(r), true);
|
||||
ASSERT_EQ(i, 42);
|
||||
}
|
||||
|
||||
{
|
||||
auto [r, i, s] = scn::scan_tuple<int, std::string>("42 foo", "{} {}");
|
||||
ASSERT_EQ(static_cast<bool>(r), true);
|
||||
ASSERT_EQ(i, 42);
|
||||
ASSERT_EQ(s, "foo");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestScnlib, ErrorHandling) {
|
||||
int i;
|
||||
auto result = scn::scan("foo", "{}", i);
|
||||
ASSERT_EQ(static_cast<bool>(result), false);
|
||||
ASSERT_EQ(result.range_as_string(), "foo");
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
73
tests/common/utils/TestSemaphore.cc
Normal file
73
tests/common/utils/TestSemaphore.cc
Normal file
@@ -0,0 +1,73 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/GtestHelpers.h>
|
||||
#include <folly/experimental/coro/Sleep.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Semaphore.h"
|
||||
#include "common/utils/SemaphoreGuard.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
CoTask<size_t> runTask(Semaphore &sema, size_t numLoops) {
|
||||
for (size_t i = 0; i < numLoops; i++) {
|
||||
SemaphoreGuard lock(sema);
|
||||
co_await lock.coWait();
|
||||
co_await folly::coro::sleep(std::chrono::microseconds{0});
|
||||
}
|
||||
|
||||
co_return numLoops;
|
||||
}
|
||||
|
||||
std::vector<folly::SemiFuture<size_t>> startTasks(Semaphore &sema,
|
||||
folly::CPUThreadPoolExecutor &threadPool,
|
||||
size_t numTasks,
|
||||
size_t numLoops) {
|
||||
std::vector<folly::SemiFuture<size_t>> tasks;
|
||||
|
||||
for (size_t t = 0; t < numTasks; t++) {
|
||||
tasks.push_back(runTask(sema, numLoops).scheduleOn(folly::Executor::getKeepAliveToken(threadPool)).start());
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
TEST(TestSemaphore, Basic) {
|
||||
size_t numCpuCores = std::thread::hardware_concurrency();
|
||||
size_t numTasks = numCpuCores * 2;
|
||||
size_t numLoops = 100;
|
||||
|
||||
Semaphore sema(std::max(size_t(1), numCpuCores / 2));
|
||||
folly::CPUThreadPoolExecutor threadPool(numCpuCores / 2);
|
||||
|
||||
std::vector<folly::SemiFuture<size_t>> tasks = startTasks(sema, threadPool, numTasks, numLoops);
|
||||
auto results = folly::coro::blockingWait(folly::coro::collectAllRange(std::move(tasks)));
|
||||
|
||||
size_t sumLoops = 0;
|
||||
for (auto res : results) sumLoops += res;
|
||||
ASSERT_EQ(numTasks * numLoops, sumLoops);
|
||||
}
|
||||
|
||||
TEST(TestSemaphore, ChangeUsableTokens) {
|
||||
size_t numCpuCores = std::thread::hardware_concurrency();
|
||||
size_t numTasks = numCpuCores * 2;
|
||||
size_t numLoops = 100;
|
||||
|
||||
Semaphore sema(std::max(size_t(1), numCpuCores / 2));
|
||||
folly::CPUThreadPoolExecutor threadPool(numCpuCores / 2);
|
||||
|
||||
std::vector<folly::SemiFuture<size_t>> tasks = startTasks(sema, threadPool, numTasks, numLoops);
|
||||
sema.changeUsableTokens(numTasks);
|
||||
auto results = folly::coro::blockingWait(folly::coro::collectAllRange(std::move(tasks)));
|
||||
|
||||
size_t sumLoops = 0;
|
||||
for (auto res : results) sumLoops += res;
|
||||
ASSERT_EQ(numTasks * numLoops, sumLoops);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
130
tests/common/utils/TestSerDeser.cc
Normal file
130
tests/common/utils/TestSerDeser.cc
Normal file
@@ -0,0 +1,130 @@
|
||||
#include <common/utils/SerDeser.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
TEST(Serializer, testPutChar) {
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.putChar('a');
|
||||
ser.putChar('b');
|
||||
ser.putChar('c');
|
||||
ASSERT_EQ(s, "abc");
|
||||
}
|
||||
|
||||
TEST(Serializer, testPutIntAsChar) {
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.putIntAsChar<int16_t>('a');
|
||||
ser.putIntAsChar<int32_t>('b');
|
||||
ser.putIntAsChar<int64_t>('c');
|
||||
ASSERT_EQ(s, "abc");
|
||||
}
|
||||
|
||||
TEST(Serializer, testPutShortString) {
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.putShortString("abc");
|
||||
ASSERT_EQ(s.size(), 4);
|
||||
ASSERT_EQ(s[0], 3);
|
||||
ASSERT_EQ(s.substr(1), "abc");
|
||||
}
|
||||
|
||||
TEST(Deserializer, testGetChar) {
|
||||
String s("abc");
|
||||
Deserializer des(s);
|
||||
String res;
|
||||
for (auto c = des.getChar(); c.hasValue(); c = des.getChar()) {
|
||||
res.push_back(c.value());
|
||||
}
|
||||
ASSERT_TRUE(des.reachEnd());
|
||||
ASSERT_TRUE(!des.getChar().hasValue());
|
||||
ASSERT_EQ(res, "abc");
|
||||
}
|
||||
|
||||
TEST(Deserializer, testGetIntFromChar) {
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.putIntAsChar<int>(100);
|
||||
ser.putIntAsChar<int16_t>(-50);
|
||||
ser.putIntAsChar<uint32_t>(50);
|
||||
|
||||
Deserializer des(s);
|
||||
auto c0 = des.getIntFromChar<int>();
|
||||
ASSERT_TRUE(c0.hasValue());
|
||||
ASSERT_EQ(c0.value(), 100);
|
||||
auto c1 = des.getIntFromChar<int16_t>();
|
||||
ASSERT_TRUE(c1.hasValue());
|
||||
ASSERT_EQ(c1.value(), -50);
|
||||
auto c2 = des.getIntFromChar<uint32_t>();
|
||||
ASSERT_TRUE(c2.hasValue());
|
||||
ASSERT_EQ(c2.value(), 50);
|
||||
|
||||
ASSERT_TRUE(des.reachEnd());
|
||||
ASSERT_TRUE(!des.getIntFromChar<int8_t>().hasValue());
|
||||
}
|
||||
|
||||
TEST(Deserializer, testGetShortString) {
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.putShortString("abc");
|
||||
|
||||
Deserializer des(s);
|
||||
ASSERT_EQ(des.getShortString().value(), "abc");
|
||||
ASSERT_TRUE(des.reachEnd());
|
||||
|
||||
ASSERT_TRUE(!des.getShortString().hasValue());
|
||||
}
|
||||
|
||||
TEST(Deserializer, testGet) {
|
||||
struct T {
|
||||
int x;
|
||||
char y;
|
||||
};
|
||||
T t = {1, 2};
|
||||
String s;
|
||||
Serializer ser(s);
|
||||
ser.put(t);
|
||||
|
||||
Deserializer des(s);
|
||||
auto res = des.get<T>();
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_TRUE(des.reachEnd());
|
||||
ASSERT_EQ(res.value().x, 1);
|
||||
ASSERT_EQ(res.value().y, 2);
|
||||
|
||||
ASSERT_TRUE(!des.get<T>().hasValue());
|
||||
}
|
||||
|
||||
TEST(Deserializer, testGetRaw) {
|
||||
String s = "abcdefg";
|
||||
Deserializer des(s);
|
||||
auto res = des.getRaw(1);
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_EQ(res.value(), "a");
|
||||
|
||||
res = des.getRaw(2);
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_EQ(res.value(), "bc");
|
||||
|
||||
res = des.getRaw(5);
|
||||
ASSERT_TRUE(!res.hasValue());
|
||||
|
||||
res = des.getRaw(1);
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_EQ(res.value(), "d");
|
||||
|
||||
res = des.getRawUntilEnd();
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_EQ(res.value(), "efg");
|
||||
|
||||
res = des.getRawUntilEnd();
|
||||
ASSERT_TRUE(res.hasValue());
|
||||
ASSERT_EQ(res.value(), "");
|
||||
|
||||
ASSERT_TRUE(des.reachEnd());
|
||||
ASSERT_TRUE(!des.getRaw(1).hasValue());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
41
tests/common/utils/TestShards.cc
Normal file
41
tests/common/utils/TestShards.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
#include <folly/Random.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "common/utils/RobinHood.h"
|
||||
#include "common/utils/Shards.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestShards, Normal) {
|
||||
Shards<std::unordered_map<uint32_t, uint32_t>, 64> shards;
|
||||
|
||||
constexpr auto N = 8;
|
||||
constexpr auto M = 100000;
|
||||
std::vector<std::thread> threads(N);
|
||||
for (auto &thread : threads) {
|
||||
thread = std::thread([&shards] {
|
||||
for (auto i = 0; i < M; ++i) {
|
||||
auto r = folly::Random::rand32();
|
||||
shards.withLock([&](std::unordered_map<uint32_t, uint32_t> &map) { ++map[r]; }, r);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
uint64_t sum = 0;
|
||||
shards.iterate([&sum](std::unordered_map<uint32_t, uint32_t> &map) {
|
||||
for (auto &[k, v] : map) {
|
||||
sum += v;
|
||||
}
|
||||
});
|
||||
ASSERT_EQ(sum, M * N);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
121
tests/common/utils/TestSize.cc
Normal file
121
tests/common/utils/TestSize.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include <folly/logging/xlog.h>
|
||||
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "common/utils/Size.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
static_assert(config::IsPrimitive<Size>, "size is not primitive");
|
||||
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_HOT_UPDATED_ITEM(size, 128_KB);
|
||||
};
|
||||
|
||||
TEST(TestSize, Normal) {
|
||||
Size config;
|
||||
ASSERT_EQ(size_t(config), 0);
|
||||
|
||||
ASSERT_EQ(1024_B, 1_KB);
|
||||
ASSERT_EQ(4096_B, 4_KB);
|
||||
ASSERT_EQ(1048576_B, 1_MB);
|
||||
ASSERT_EQ(1024_KB, 1_MB);
|
||||
ASSERT_EQ(1024_MB, 1_GB);
|
||||
ASSERT_EQ(1024_GB, 1_TB);
|
||||
|
||||
ASSERT_EQ(1000_B, 1_K);
|
||||
ASSERT_EQ(1000_K, 1_M);
|
||||
ASSERT_EQ(1000_M, 1_G);
|
||||
ASSERT_EQ(1000_G, 1_T);
|
||||
|
||||
ASSERT_EQ(Size::toString(0), "0");
|
||||
ASSERT_EQ(Size::toString(511), "511");
|
||||
ASSERT_EQ(Size::toString(512), "512");
|
||||
ASSERT_EQ(Size::toString(1013), "1013");
|
||||
ASSERT_EQ(Size::toString(1025), "1025");
|
||||
ASSERT_EQ(Size::toString(1034), "1034");
|
||||
ASSERT_EQ(Size::toString(1048576), "1MB");
|
||||
|
||||
ASSERT_EQ(Size::toString(1000), "1K");
|
||||
ASSERT_EQ(Size::toString(1024), "1KB");
|
||||
ASSERT_EQ(Size::toString(1000_K), "1M");
|
||||
ASSERT_EQ(Size::toString(1024_K), "1024K");
|
||||
ASSERT_EQ(Size::toString(1000_G), "1T");
|
||||
ASSERT_EQ(Size::toString(1024_G), "1024G");
|
||||
|
||||
ASSERT_EQ(Size::from("1K").value(), 1_K);
|
||||
ASSERT_EQ(Size::from("2KB").value(), 2_KB);
|
||||
ASSERT_EQ(Size::from("3M").value(), 3_M);
|
||||
ASSERT_EQ(Size::from("4MB").value(), 4_MB);
|
||||
ASSERT_EQ(Size::from("5G").value(), 5_G);
|
||||
ASSERT_EQ(Size::from("6GB").value(), 6_GB);
|
||||
ASSERT_EQ(Size::from("7T").value(), 7_T);
|
||||
ASSERT_EQ(Size::from("8TB").value(), 8_TB);
|
||||
|
||||
ASSERT_EQ(Size::around(0), "0");
|
||||
ASSERT_EQ(Size::around(511), "511");
|
||||
ASSERT_EQ(Size::around(512), "0.50KB");
|
||||
ASSERT_EQ(Size::around(1013), "0.99KB");
|
||||
ASSERT_EQ(Size::around(1025), "1.00KB");
|
||||
ASSERT_EQ(Size::around(1034), "1.01KB");
|
||||
ASSERT_EQ(Size::around(1048576), "1.00MB");
|
||||
}
|
||||
|
||||
TEST(TestSize, Parse) {
|
||||
Size c0 = Size::from("100").value();
|
||||
ASSERT_EQ(c0.toString(), "100");
|
||||
Size c1 = Size::from("2KB").value();
|
||||
ASSERT_EQ(c1.toString(), "2KB");
|
||||
Size c2 = Size::from("1001MB").value();
|
||||
ASSERT_EQ(c2.toString(), "1001MB");
|
||||
Size c3 = Size::from("45").value();
|
||||
ASSERT_EQ(c3.toString(), "45");
|
||||
Size c5 = Size::from("08GB").value();
|
||||
ASSERT_EQ(c5.toString(), "8GB");
|
||||
Size c6 = Size::from("1TB").value();
|
||||
ASSERT_EQ(c6.toString(), "1TB");
|
||||
ASSERT_FALSE(Size::from("10kb"));
|
||||
ASSERT_FALSE(Size::from(""));
|
||||
ASSERT_FALSE(Size::from("GB"));
|
||||
ASSERT_FALSE(Size::from("1.1MB"));
|
||||
|
||||
ASSERT_EQ(fmt::format("{}", 100_KB), "100KB");
|
||||
}
|
||||
|
||||
TEST(TestSize, testConfig) {
|
||||
Config cfg;
|
||||
ASSERT_EQ(cfg.size(), 128_KB);
|
||||
cfg.set_size(512_KB);
|
||||
ASSERT_EQ(cfg.size(), 512_KB);
|
||||
|
||||
toml::table result = toml::parse(R"(
|
||||
size = "5KB"
|
||||
)");
|
||||
ASSERT_TRUE(cfg.update(result));
|
||||
ASSERT_EQ(cfg.size(), 5_KB);
|
||||
|
||||
XLOGF(INFO, "toString: {}", cfg.toString());
|
||||
|
||||
result = toml::parse(cfg.toString());
|
||||
Config other;
|
||||
ASSERT_TRUE(other.update(result));
|
||||
ASSERT_EQ(cfg.size(), other.size());
|
||||
|
||||
ASSERT_TRUE(cfg.update(toml::parse(R"(
|
||||
size = 4096
|
||||
)")));
|
||||
ASSERT_EQ(cfg.size(), 4_KB);
|
||||
}
|
||||
|
||||
TEST(TestSize, Infinity) {
|
||||
constexpr auto inf = Size::infinity();
|
||||
ASSERT_EQ(inf, std::numeric_limits<uint64_t>::max());
|
||||
ASSERT_EQ(inf.toString(), "infinity");
|
||||
|
||||
ASSERT_OK(Size::from("infinity"));
|
||||
ASSERT_EQ(*Size::from("infinity"), Size::infinity());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
110
tests/common/utils/TestStatus.cc
Normal file
110
tests/common/utils/TestStatus.cc
Normal file
@@ -0,0 +1,110 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
|
||||
#include "common/utils/Status.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
TEST(Status, testCtor) {
|
||||
Status s1(1);
|
||||
ASSERT_EQ(s1.code(), 1);
|
||||
ASSERT_TRUE(s1.message().empty());
|
||||
ASSERT_TRUE(!s1.hasPayload());
|
||||
|
||||
Status s2(1, "test");
|
||||
ASSERT_EQ(s2.code(), 1);
|
||||
ASSERT_EQ(s2.message(), "test");
|
||||
ASSERT_TRUE(!s2.hasPayload());
|
||||
ASSERT_EQ(fmt::format("{}", s2), "NotImplemented(1) test");
|
||||
}
|
||||
|
||||
TEST(Status, testPayload) {
|
||||
Status s0(1);
|
||||
s0.setPayload<int>(5);
|
||||
ASSERT_TRUE(s0.hasPayload());
|
||||
ASSERT_EQ(*s0.payload<int>(), 5);
|
||||
|
||||
s0.setPayload<std::string>("abc");
|
||||
ASSERT_TRUE(s0.hasPayload());
|
||||
ASSERT_EQ(*s0.payload<std::string>(), "abc");
|
||||
|
||||
s0.resetPayload();
|
||||
ASSERT_TRUE(!s0.hasPayload());
|
||||
|
||||
s0.emplacePayload<std::string>(3, 'a');
|
||||
ASSERT_EQ(*s0.payload<std::string>(), "aaa");
|
||||
}
|
||||
|
||||
TEST(Status, testCopy) {
|
||||
Status s2(1, "test");
|
||||
s2.setPayload<std::string>("abc");
|
||||
ASSERT_EQ(s2.code(), 1);
|
||||
ASSERT_EQ(s2.message(), "test");
|
||||
ASSERT_TRUE(s2.hasPayload());
|
||||
ASSERT_EQ(*s2.payload<std::string>(), "abc");
|
||||
|
||||
Status s3 = s2;
|
||||
ASSERT_EQ(s2.code(), 1);
|
||||
ASSERT_EQ(s2.message(), "test");
|
||||
ASSERT_TRUE(s2.hasPayload());
|
||||
ASSERT_EQ(*s2.payload<std::string>(), "abc");
|
||||
ASSERT_EQ(s3.code(), 1);
|
||||
ASSERT_EQ(s3.message(), "test");
|
||||
ASSERT_TRUE(s3.hasPayload());
|
||||
ASSERT_EQ(*s3.payload<std::string>(), "abc");
|
||||
|
||||
auto *ps3 = &s3;
|
||||
s3 = *ps3;
|
||||
ASSERT_EQ(s3.code(), 1);
|
||||
ASSERT_EQ(s3.message(), "test");
|
||||
ASSERT_TRUE(s3.hasPayload());
|
||||
ASSERT_EQ(*s3.payload<std::string>(), "abc");
|
||||
|
||||
Status s4(0);
|
||||
s4 = s3;
|
||||
ASSERT_EQ(s3.code(), 1);
|
||||
ASSERT_EQ(s3.message(), "test");
|
||||
ASSERT_TRUE(s3.hasPayload());
|
||||
ASSERT_EQ(*s3.payload<std::string>(), "abc");
|
||||
ASSERT_EQ(s4.code(), 1);
|
||||
ASSERT_EQ(s4.message(), "test");
|
||||
ASSERT_TRUE(s4.hasPayload());
|
||||
ASSERT_EQ(*s4.payload<std::string>(), "abc");
|
||||
}
|
||||
|
||||
TEST(Status, testMove) {
|
||||
Status s2(1, "test");
|
||||
s2.setPayload<std::string>("abc");
|
||||
ASSERT_EQ(s2.code(), 1);
|
||||
ASSERT_EQ(s2.message(), "test");
|
||||
ASSERT_TRUE(s2.hasPayload());
|
||||
ASSERT_EQ(*s2.payload<std::string>(), "abc");
|
||||
|
||||
Status s3 = std::move(s2);
|
||||
ASSERT_EQ(s2.code(), StatusCode::kOK);
|
||||
ASSERT_TRUE(s2.message().empty());
|
||||
ASSERT_TRUE(!s2.hasPayload());
|
||||
ASSERT_EQ(s3.code(), 1);
|
||||
ASSERT_EQ(s3.message(), "test");
|
||||
ASSERT_TRUE(s3.hasPayload());
|
||||
ASSERT_EQ(*s3.payload<std::string>(), "abc");
|
||||
|
||||
auto *ps3 = &s3;
|
||||
s3 = std::move(*ps3);
|
||||
ASSERT_EQ(s3.code(), 1);
|
||||
ASSERT_EQ(s3.message(), "test");
|
||||
ASSERT_TRUE(s3.hasPayload());
|
||||
ASSERT_EQ(*s3.payload<std::string>(), "abc");
|
||||
|
||||
Status s4(0);
|
||||
s4 = std::move(s3);
|
||||
ASSERT_EQ(s3.code(), StatusCode::kOK);
|
||||
ASSERT_TRUE(s3.message().empty());
|
||||
ASSERT_TRUE(!s3.hasPayload());
|
||||
ASSERT_EQ(s4.code(), 1);
|
||||
ASSERT_EQ(s4.message(), "test");
|
||||
ASSERT_TRUE(s4.hasPayload());
|
||||
ASSERT_EQ(*s4.payload<std::string>(), "abc");
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
14
tests/common/utils/TestStatusCode.cc
Normal file
14
tests/common/utils/TestStatusCode.cc
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "common/utils/StatusCode.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace hf3fs {
|
||||
namespace {
|
||||
|
||||
TEST(StatusCode, toString) {
|
||||
ASSERT_EQ(StatusCode::toString(StatusCode::kOK), "OK");
|
||||
ASSERT_EQ(StatusCode::toString(TransactionCode::kConflict), "Transaction::Conflict");
|
||||
ASSERT_EQ(StatusCode::toString(-1), "UnknownStatusCode");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs
|
||||
28
tests/common/utils/TestStrongTypedef.cc
Normal file
28
tests/common/utils/TestStrongTypedef.cc
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <string>
|
||||
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
STRONG_TYPEDEF(int, TypedInt);
|
||||
STRONG_TYPEDEF(size_t, TypedUint);
|
||||
STRONG_TYPEDEF(std::string, TypedString);
|
||||
|
||||
TEST(StrongTypedefTest, testComparison) {
|
||||
TypedInt ti0;
|
||||
ASSERT_TRUE(ti0 < 1);
|
||||
ASSERT_TRUE(ti0 > -1);
|
||||
ASSERT_EQ(ti0, 0);
|
||||
|
||||
TypedUint tu0;
|
||||
ASSERT_TRUE(tu0.toUnderType() < 1);
|
||||
ASSERT_TRUE(tu0.toUnderType() >= 0);
|
||||
ASSERT_EQ(tu0, 0);
|
||||
|
||||
TypedString ts0, ts1("aa");
|
||||
ASSERT_TRUE(ts0 < ts1);
|
||||
ASSERT_TRUE(ts0 < "a");
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
57
tests/common/utils/TestSysResource.cc
Normal file
57
tests/common/utils/TestSysResource.cc
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <cstdlib>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/SysResource.h"
|
||||
|
||||
namespace hf3fs {
|
||||
|
||||
TEST(TestSysResource, GetHostname) {
|
||||
auto result = SysResource::hostname();
|
||||
ASSERT_TRUE(result);
|
||||
XLOGF(INFO, "hostname is {}", result.value());
|
||||
}
|
||||
|
||||
TEST(TestSysResource, FDLimit) {
|
||||
auto result = SysResource::increaseProcessFDLimit(8192);
|
||||
ASSERT_TRUE(result);
|
||||
}
|
||||
|
||||
// todo: fix fs UUID in docker
|
||||
TEST(TestSysResource, DISABLED_FileSystemUUID) {
|
||||
auto result = SysResource::fileSystemUUID();
|
||||
ASSERT_TRUE(result);
|
||||
auto &map = *result;
|
||||
ASSERT_TRUE(!map.empty());
|
||||
|
||||
for (auto [deviceId, uuid] : map) {
|
||||
XLOGF(INFO, "device: {}, uuid: {}", deviceId, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestSysResource, ScanDiskInfo) {
|
||||
auto result = SysResource::scanDiskInfo();
|
||||
ASSERT_TRUE(result);
|
||||
XLOGF(INFO, "info is {}", serde::toJsonString(*result));
|
||||
}
|
||||
|
||||
TEST(TestSysResource, getAllEnvs) {
|
||||
::setenv("HF3FS_TEST_ONE", "ONE", 1);
|
||||
::setenv("HF3FS_TEST_TWO", "TWO", 1);
|
||||
::setenv("TEST_THREE", "THREE", 1);
|
||||
auto m = SysResource::getAllEnvs("HF3FS_");
|
||||
ASSERT_TRUE(!m.empty());
|
||||
|
||||
for (const auto &[k, v] : m) XLOGF(INFO, "env {} = {}", k, v);
|
||||
|
||||
ASSERT_TRUE(m.contains("HF3FS_TEST_ONE"));
|
||||
ASSERT_EQ(m["HF3FS_TEST_ONE"], "ONE");
|
||||
|
||||
ASSERT_TRUE(m.contains("HF3FS_TEST_TWO"));
|
||||
ASSERT_EQ(m["HF3FS_TEST_TWO"], "TWO");
|
||||
|
||||
ASSERT_TRUE(!m.contains("TEST_THREE"));
|
||||
}
|
||||
|
||||
} // namespace hf3fs
|
||||
32
tests/common/utils/TestThief.cc
Normal file
32
tests/common/utils/TestThief.cc
Normal file
@@ -0,0 +1,32 @@
|
||||
#include <type_traits>
|
||||
|
||||
#include "common/utils/Thief.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
class A {
|
||||
public:
|
||||
auto &secret() { return secret_; }
|
||||
|
||||
private:
|
||||
int secret_ = 0;
|
||||
};
|
||||
|
||||
template <typename Tag, auto Value>
|
||||
struct StoreValue : public std::type_identity<thief::steal<Tag, value_identity<Value>>> {};
|
||||
|
||||
struct Tag;
|
||||
template struct StoreValue<Tag, &A::secret_>;
|
||||
|
||||
TEST(TestThief, AccessPrivateMember) {
|
||||
A a;
|
||||
a.secret() = 20;
|
||||
|
||||
ASSERT_EQ(a.*thief::retrieve<Tag>::value, 20);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
25
tests/common/utils/TestToml.cc
Normal file
25
tests/common/utils/TestToml.cc
Normal file
@@ -0,0 +1,25 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/Toml.hpp"
|
||||
|
||||
TEST(TestToml, Normal) {
|
||||
toml::parse_result config = toml::parse(R"(
|
||||
name = "foo"
|
||||
|
||||
[test]
|
||||
foo = "bar"
|
||||
val = 123
|
||||
)");
|
||||
|
||||
ASSERT_TRUE(config["name"].is_string());
|
||||
ASSERT_EQ(config["name"].value_or(std::string_view{}), std::string_view("foo"));
|
||||
|
||||
ASSERT_TRUE(config["test"].is_table());
|
||||
ASSERT_EQ(config["test"]["foo"].value<std::string_view>(), "bar");
|
||||
ASSERT_EQ(config["test"]["val"].value<int>(), 123);
|
||||
|
||||
ASSERT_FALSE(config.contains("not-exists"));
|
||||
ASSERT_FALSE(config["not-exists"].is_table());
|
||||
ASSERT_FALSE(config["not-exists"]["not-exists"].is_value());
|
||||
ASSERT_EQ(config["not-exists"]["not-exists"].value_or(123), 123);
|
||||
}
|
||||
14
tests/common/utils/TestToml11.cc
Normal file
14
tests/common/utils/TestToml11.cc
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "tests/GtestHelpers.h"
|
||||
#include "toml.hpp"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestToml11, Normal) {
|
||||
toml::value v; // not initialized as a table.
|
||||
v["foo"] = 42; // OK. `v` will be a table.
|
||||
ASSERT_TRUE(v.is_table());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
130
tests/common/utils/TestTracing.cc
Normal file
130
tests/common/utils/TestTracing.cc
Normal file
@@ -0,0 +1,130 @@
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Tracing.h"
|
||||
#include "common/utils/TracingEvent.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
TEST(Tracing, testEventToString) {
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbBeginNewTransaction), "Fdb::BeginNewTransaction");
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbEndNewTransaction), "Fdb::EndNewTransaction");
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbBeginCommitTransaction), "Fdb::BeginCommitTransaction");
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbEndCommitTransaction), "Fdb::EndCommitTransaction");
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbBeginCancelTransaction), "Fdb::BeginCancelTransaction");
|
||||
ASSERT_EQ(tracing::toString(tracing::kFdbEndCancelTransaction), "Fdb::EndCancelTransaction");
|
||||
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaLoadInode), "Meta::LoadInode");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaBeginLoadInode), "Meta::BeginLoadInode");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaEndLoadInode), "Meta::EndLoadInode");
|
||||
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaSnapshotLoadInode), "Meta::SnapshotLoadInode");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaBeginSnapshotLoadInode), "Meta::BeginSnapshotLoadInode");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaEndSnapshotLoadInode), "Meta::EndSnapshotLoadInode");
|
||||
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaLoadDirEntry), "Meta::LoadDirEntry");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaBeginLoadDirEntry), "Meta::BeginLoadDirEntry");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaEndLoadDirEntry), "Meta::EndLoadDirEntry");
|
||||
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaSnapshotLoadDirEntry), "Meta::SnapshotLoadDirEntry");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaBeginSnapshotLoadDirEntry), "Meta::BeginSnapshotLoadDirEntry");
|
||||
ASSERT_EQ(tracing::toString(tracing::kMetaEndSnapshotLoadDirEntry), "Meta::EndSnapshotLoadDirEntry");
|
||||
}
|
||||
|
||||
TEST(Tracing, testScopeGuard) {
|
||||
tracing::Points points1, points2;
|
||||
{
|
||||
SCOPE_SET_TRACING_POINTS(points1);
|
||||
TRACING_ADD_SCOPE_EVENT(tracing::kFdbNewTransaction);
|
||||
|
||||
{
|
||||
SCOPE_SET_TRACING_POINTS(points2);
|
||||
TRACING_ADD_SCOPE_EVENT(tracing::kFdbCommitTransaction);
|
||||
}
|
||||
|
||||
TRACING_ADD_EVENT(tracing::kFdbBeginCancelTransaction);
|
||||
}
|
||||
TRACING_ADD_EVENT(tracing::kFdbEndCancelTransaction);
|
||||
|
||||
auto v1 = points1.extractAll();
|
||||
ASSERT_EQ(v1.size(), 3);
|
||||
ASSERT_EQ(v1[0].event(), tracing::kFdbBeginNewTransaction);
|
||||
ASSERT_EQ(v1[1].event(), tracing::kFdbBeginCancelTransaction);
|
||||
ASSERT_EQ(v1[2].event(), tracing::kFdbEndNewTransaction);
|
||||
|
||||
auto v2 = points2.extractAll();
|
||||
ASSERT_EQ(v2.size(), 2);
|
||||
ASSERT_EQ(v2[0].event(), tracing::kFdbBeginCommitTransaction);
|
||||
ASSERT_EQ(v2[1].event(), tracing::kFdbEndCommitTransaction);
|
||||
}
|
||||
|
||||
TEST(Tracing, testPassThroughCoroutine) {
|
||||
tracing::Points points;
|
||||
folly::CPUThreadPoolExecutor executor(1);
|
||||
{
|
||||
SCOPE_SET_TRACING_POINTS(points);
|
||||
folly::coro::blockingWait([&]() -> CoTask<void> {
|
||||
TRACING_ADD_SCOPE_EVENT(tracing::kFdbNewTransaction);
|
||||
auto f = []() -> CoTask<void> {
|
||||
TRACING_ADD_SCOPE_EVENT(tracing::kFdbCommitTransaction);
|
||||
co_return;
|
||||
};
|
||||
co_await f().scheduleOn(folly::getKeepAliveToken(executor));
|
||||
}());
|
||||
}
|
||||
|
||||
auto v = points.extractAll();
|
||||
ASSERT_EQ(v.size(), 4);
|
||||
ASSERT_EQ(v[0].event(), tracing::kFdbBeginNewTransaction);
|
||||
ASSERT_EQ(v[1].event(), tracing::kFdbBeginCommitTransaction);
|
||||
ASSERT_EQ(v[2].event(), tracing::kFdbEndCommitTransaction);
|
||||
ASSERT_EQ(v[3].event(), tracing::kFdbEndNewTransaction);
|
||||
}
|
||||
|
||||
TEST(Tracing, testPointMessage) {
|
||||
auto t = UtcClock::now();
|
||||
tracing::Point p0(t, 0);
|
||||
ASSERT_EQ(p0.timestamp(), t);
|
||||
ASSERT_EQ(p0.event(), 0);
|
||||
ASSERT_EQ(p0.message(), "");
|
||||
|
||||
std::string_view msg0 = "A very long message which contains more than 16 chars";
|
||||
p0.setMessage(msg0);
|
||||
ASSERT_EQ(p0.message(), msg0);
|
||||
ASSERT_NE(p0.message().data(), msg0.data());
|
||||
|
||||
std::string_view msg1 = "A short message";
|
||||
p0.setMessage(msg1);
|
||||
ASSERT_EQ(p0.message(), msg1);
|
||||
}
|
||||
|
||||
TEST(Tracing, testPointMove) {
|
||||
auto t0 = UtcClock::now();
|
||||
std::string_view msg0 = "A very long message which contains more than 16 chars";
|
||||
tracing::Point p0(t0, 0, msg0);
|
||||
ASSERT_EQ(p0.timestamp(), t0);
|
||||
ASSERT_EQ(p0.event(), 0);
|
||||
ASSERT_EQ(p0.message(), msg0);
|
||||
|
||||
auto t1 = t0 + std::chrono::microseconds(1);
|
||||
std::string_view msg1 = "Yet another very long message but shorter than msg0";
|
||||
{
|
||||
tracing::Point p1(t1, 1, msg1);
|
||||
tracing::Point p2 = std::move(p1);
|
||||
ASSERT_EQ(p1.message(), "");
|
||||
|
||||
ASSERT_EQ(p2.timestamp(), t1);
|
||||
ASSERT_EQ(p2.event(), 1);
|
||||
ASSERT_EQ(p2.message(), msg1);
|
||||
|
||||
p0 = std::move(p2);
|
||||
ASSERT_EQ(p2.message(), "");
|
||||
}
|
||||
ASSERT_EQ(p0.timestamp(), t1);
|
||||
ASSERT_EQ(p0.event(), 1);
|
||||
ASSERT_EQ(p0.message(), msg1);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
34
tests/common/utils/TestTypeTraits.cc
Normal file
34
tests/common/utils/TestTypeTraits.cc
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <vector>
|
||||
|
||||
#include "common/utils/RobinHood.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
|
||||
namespace hf3fs {
|
||||
namespace {
|
||||
|
||||
static_assert(is_vector_v<std::vector<int>>, "is vector");
|
||||
static_assert(!is_vector_v<std::string>, "not vector");
|
||||
|
||||
static_assert(is_set_v<std::set<int>>, "is set");
|
||||
static_assert(!is_set_v<std::string>, "not set");
|
||||
|
||||
static_assert(is_map_v<std::map<int, int>>, "is map");
|
||||
static_assert(!is_map_v<std::vector<int>>, "not map");
|
||||
|
||||
static_assert(GenericPair<std::pair<int, int>>, "is generic pair");
|
||||
static_assert(!GenericPair<std::map<int, int>>, "not generic pair");
|
||||
|
||||
static_assert(GenericPair<robin_hood::pair<int, int>>, "is generic pair");
|
||||
static_assert(Container<robin_hood::unordered_map<int, int>>, "ok");
|
||||
|
||||
struct Base {
|
||||
int x;
|
||||
};
|
||||
struct Inherit : Base {
|
||||
int y;
|
||||
};
|
||||
static_assert(std::is_same_v<member_pointer_to_class_t<&Inherit::x>, Base>);
|
||||
static_assert(std::is_same_v<member_pointer_to_class_t<&Inherit::y>, Inherit>);
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs
|
||||
29
tests/common/utils/TestUnorderedDense.cc
Normal file
29
tests/common/utils/TestUnorderedDense.cc
Normal file
@@ -0,0 +1,29 @@
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/utils/RobinHood.h"
|
||||
#include "common/utils/UnorderedDense.h"
|
||||
#include "tests/GtestHelpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
template <class Map>
|
||||
void insertWhileHashIsZero() {
|
||||
constexpr auto N = 1000;
|
||||
Map map;
|
||||
for (auto i = 0; i < N; ++i) {
|
||||
map[i] = i;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TestUnorderedDense, Normal) {
|
||||
struct Hash {
|
||||
size_t operator()(int) const { return 0; }
|
||||
};
|
||||
|
||||
insertWhileHashIsZero<ankerl::unordered_dense::map<int, int, Hash>>();
|
||||
insertWhileHashIsZero<std::unordered_map<int, int, Hash>>();
|
||||
ASSERT_THROW((insertWhileHashIsZero<robin_hood::unordered_map<int, int, Hash>>()), std::overflow_error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
17
tests/common/utils/TestUtc.cc
Normal file
17
tests/common/utils/TestUtc.cc
Normal file
@@ -0,0 +1,17 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "common/utils/UtcTime.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
|
||||
TEST(UtcTime, testConvert) {
|
||||
UtcTime now = UtcClock::now();
|
||||
int64_t us = now.toMicroseconds();
|
||||
UtcTime utc = UtcTime::fromMicroseconds(us);
|
||||
|
||||
ASSERT_EQ(us, utc.toMicroseconds());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
124
tests/common/utils/TestUuid.cc
Normal file
124
tests/common/utils/TestUuid.cc
Normal file
@@ -0,0 +1,124 @@
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/uuid/random_generator.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <cstdlib>
|
||||
#include <double-conversion/utils.h>
|
||||
#include <fmt/core.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#include "common/utils/SerDeser.h"
|
||||
#include "common/utils/Uuid.h"
|
||||
|
||||
namespace hf3fs::tests {
|
||||
namespace {
|
||||
|
||||
std::pair<boost::uuids::uuid, boost::uuids::uuid> gen(auto &generator) {
|
||||
boost::ignore_unused(generator());
|
||||
int pipefd[2];
|
||||
EXPECT_NE(pipe(pipefd), -1);
|
||||
|
||||
auto pid = fork();
|
||||
EXPECT_NE(pid, -1);
|
||||
auto uuid = generator();
|
||||
fmt::print("{} {}\n", pid == 0 ? "child " : "father", boost::lexical_cast<std::string>(uuid));
|
||||
if (pid == 0) {
|
||||
close(pipefd[0]);
|
||||
write(pipefd[1], uuid.data, sizeof(uuid));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
close(pipefd[1]);
|
||||
boost::uuids::uuid another;
|
||||
read(pipefd[0], another.data, sizeof(another));
|
||||
|
||||
int status;
|
||||
EXPECT_EQ(wait(&status), pid);
|
||||
|
||||
return std::make_pair(uuid, another);
|
||||
}
|
||||
|
||||
TEST(Uuid, DISABLED_fork) {
|
||||
// disable this test by default because it hangs in gitlab CI.
|
||||
static thread_local boost::uuids::random_generator_mt19937 mt19937;
|
||||
static thread_local boost::uuids::random_generator random;
|
||||
|
||||
fmt::print("random_generator_mt19937\n");
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
auto a = gen(mt19937);
|
||||
EXPECT_EQ(a.first, a.second); // shouldn't use mt19937
|
||||
}
|
||||
fmt::print("random_generator\n");
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
auto b = gen(random);
|
||||
EXPECT_NE(b.first, b.second);
|
||||
}
|
||||
fmt::print("Uuid\n");
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
auto c = gen(Uuid::random);
|
||||
EXPECT_NE(c.first, c.second);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Uuid, testZero) {
|
||||
uint8_t buffer[Uuid::static_size()] = {0};
|
||||
Uuid zero = Uuid::zero();
|
||||
Uuid def = Uuid{};
|
||||
|
||||
ASSERT_EQ(memcmp(buffer, zero.data, Uuid::static_size()), 0);
|
||||
ASSERT_EQ(zero, def);
|
||||
|
||||
Uuid zero1 = Uuid::zero();
|
||||
ASSERT_EQ(zero, zero1);
|
||||
}
|
||||
|
||||
TEST(Uuid, testRandom) {
|
||||
Uuid a = Uuid::random();
|
||||
Uuid b = Uuid::random();
|
||||
|
||||
ASSERT_NE(a, b);
|
||||
}
|
||||
|
||||
TEST(Uuid, testToString) {
|
||||
Uuid a = Uuid::random();
|
||||
std::cout << a.toHexString() << std::endl;
|
||||
|
||||
Uuid zero = Uuid::zero();
|
||||
std::cout << zero.toHexString() << std::endl;
|
||||
ASSERT_EQ(zero.toHexString(), "00000000-0000-0000-0000-000000000000");
|
||||
}
|
||||
|
||||
TEST(Uuid, Compare) {
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
Uuid a = Uuid::random();
|
||||
Uuid b = Uuid::random();
|
||||
ASSERT_EQ((a < b), (b > a));
|
||||
ASSERT_NE((a < b), (b < a));
|
||||
ASSERT_TRUE(a > Uuid::zero());
|
||||
ASSERT_TRUE(b > Uuid::zero());
|
||||
ASSERT_TRUE(a < Uuid::max());
|
||||
ASSERT_TRUE(b < Uuid::max());
|
||||
|
||||
auto aser = Serializer::serRawArgs(a);
|
||||
auto bser = Serializer::serRawArgs(b);
|
||||
ASSERT_EQ((a < b), (aser < bser));
|
||||
ASSERT_EQ((a > b), (aser > bser));
|
||||
ASSERT_TRUE(aser > Serializer::serRawArgs(Uuid::zero()));
|
||||
ASSERT_TRUE(bser > Serializer::serRawArgs(Uuid::zero()));
|
||||
ASSERT_TRUE(aser < Serializer::serRawArgs(Uuid::max()));
|
||||
ASSERT_TRUE(bser < Serializer::serRawArgs(Uuid::max()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Uuid, Gen) {
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
fmt::println("{}", Uuid::random());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::tests
|
||||
38
tests/common/utils/TestVarint32.cc
Normal file
38
tests/common/utils/TestVarint32.cc
Normal file
@@ -0,0 +1,38 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <limits>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Varint32.h"
|
||||
|
||||
namespace hf3fs::test {
|
||||
namespace {
|
||||
|
||||
TEST(TestVarint32, Normal) {
|
||||
Varint32 value = 20;
|
||||
value = 30;
|
||||
ASSERT_EQ(value, 30);
|
||||
|
||||
auto out = serde::serialize(value);
|
||||
|
||||
Varint32 de1{};
|
||||
ASSERT_TRUE(serde::deserialize(de1, out).hasValue());
|
||||
ASSERT_EQ(de1, value);
|
||||
|
||||
Varint64 de2{};
|
||||
ASSERT_TRUE(serde::deserialize(de2, out).hasValue());
|
||||
ASSERT_EQ(de2, value);
|
||||
}
|
||||
|
||||
TEST(TestVarint64, Normal) {
|
||||
for (auto value : {0ul, 1ul << 32, std::numeric_limits<uint64_t>::max()}) {
|
||||
Varint64 ser = value;
|
||||
auto out = serde::serialize(ser);
|
||||
|
||||
Varint64 der{};
|
||||
ASSERT_TRUE(serde::deserialize(der, out).hasValue());
|
||||
ASSERT_EQ(ser, der);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace hf3fs::test
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user