mirror of
https://github.com/deepseek-ai/3FS
synced 2025-06-26 18:16:45 +00:00
Initial commit
This commit is contained in:
18
src/CMakeLists.txt
Normal file
18
src/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
add_subdirectory(fbs)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(meta)
|
||||
add_subdirectory(client)
|
||||
add_subdirectory(lib)
|
||||
add_subdirectory(memory)
|
||||
add_subdirectory(kv)
|
||||
add_subdirectory(storage)
|
||||
add_subdirectory(mgmtd)
|
||||
add_subdirectory(fdb)
|
||||
add_subdirectory(stubs)
|
||||
add_subdirectory(monitor_collector)
|
||||
add_subdirectory(analytics)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(tools)
|
||||
add_subdirectory(fuse)
|
||||
add_subdirectory(simple_example)
|
||||
add_subdirectory(migration)
|
||||
1
src/analytics/CMakeLists.txt
Normal file
1
src/analytics/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
target_add_lib(analytics common apache_arrow_static)
|
||||
65
src/analytics/Common.h
Normal file
65
src/analytics/Common.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/StrongType.h"
|
||||
|
||||
namespace hf3fs::serde {
|
||||
|
||||
template <typename T>
|
||||
using SerdeToReadableMemberMethodReturnType = std::invoke_result_t<decltype(&T::serdeToReadable), T>;
|
||||
|
||||
template <typename T>
|
||||
using SerdeToMemberMethodReturnType = std::invoke_result_t<decltype(&T::serdeTo), T>;
|
||||
|
||||
template <typename T>
|
||||
using SerdeToReadableReturnType = std::invoke_result_t<decltype(serde::SerdeMethod<T>::serdeToReadable), const T &>;
|
||||
|
||||
template <typename T>
|
||||
using SerdeToReturnType = std::invoke_result_t<decltype(serde::SerdeMethod<T>::serdeTo), const T &>;
|
||||
|
||||
template <typename T>
|
||||
concept ConvertibleToString = std::is_convertible_v<T, std::string>;
|
||||
|
||||
template <typename T>
|
||||
concept WithReadableSerdeMemberMethod =
|
||||
!StrongTyped<T> && !ConvertibleToString<T> && requires(const T &t, SerdeToReadableMemberMethodReturnType<T> s) {
|
||||
{ t.serdeToReadable() } -> std::convertible_to<SerdeToReadableMemberMethodReturnType<T>>;
|
||||
{ T::serdeFromReadable(s) } -> std::convertible_to<Result<T>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept WithReadableSerdeMethod = !StrongTyped<T> && !ConvertibleToString<T> && !WithReadableSerdeMemberMethod<T> &&
|
||||
requires(const T &t, SerdeToReadableReturnType<T> s) {
|
||||
{ serde::SerdeMethod<T>::serdeToReadable(t) } -> std::convertible_to<SerdeToReadableReturnType<T>>;
|
||||
{ serde::SerdeMethod<T>::serdeFromReadable(s) } -> std::convertible_to<Result<T>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept WithSerdeMemberMethod =
|
||||
!StrongTyped<T> && !ConvertibleToString<T> && !WithReadableSerdeMemberMethod<T> && !WithReadableSerdeMethod<T> &&
|
||||
requires(const T &t, SerdeToMemberMethodReturnType<T> s) {
|
||||
{ t.serdeTo() } -> std::convertible_to<SerdeToMemberMethodReturnType<T>>;
|
||||
{ T::serdeFrom(s) } -> std::convertible_to<Result<T>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept WithSerdeMethod =
|
||||
!StrongTyped<T> && !ConvertibleToString<T> && !WithReadableSerdeMemberMethod<T> && !WithReadableSerdeMethod<T> &&
|
||||
!WithSerdeMemberMethod<T> && requires(const T &t, SerdeToReturnType<T> s) {
|
||||
{ serde::SerdeMethod<T>::serdeTo(t) } -> std::convertible_to<SerdeToReturnType<T>>;
|
||||
{ serde::SerdeMethod<T>::serdeFrom(s) } -> std::convertible_to<Result<T>>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept SerdeTypeWithoutSpecializedSerdeMethod =
|
||||
serde::SerdeType<T> && !WithReadableSerdeMemberMethod<T> && !WithReadableSerdeMethod<T> &&
|
||||
!WithSerdeMemberMethod<T> && !WithSerdeMethod<T>;
|
||||
|
||||
} // namespace hf3fs::serde
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
const std::string kVariantValueIndexColumnSuffix = "ValIdx";
|
||||
const std::string kResultErrorTypeColumnSuffix = "Error";
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
238
src/analytics/SerdeObjectReader.h
Normal file
238
src/analytics/SerdeObjectReader.h
Normal file
@@ -0,0 +1,238 @@
|
||||
#pragma once
|
||||
#include <arrow/io/file.h>
|
||||
#include <optional>
|
||||
#include <parquet/stream_reader.h>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "SerdeObjectVisitor.h"
|
||||
#include "SerdeSchemaBuilder.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
template <serde::SerdeType SerdeType>
|
||||
class SerdeObjectReader : public BaseObjectVisitor<SerdeObjectReader<SerdeType>> {
|
||||
public:
|
||||
SerdeObjectReader(parquet::StreamReader &&reader)
|
||||
: reader_(std::move(reader)) {}
|
||||
|
||||
static std::shared_ptr<SerdeObjectReader> open(const Path path) {
|
||||
// open file
|
||||
auto openStream = arrow::io::ReadableFile::Open(path.string());
|
||||
|
||||
if (!openStream.ok()) {
|
||||
XLOGF(ERR, "Failed to open file input stream: {}, error: {}", path.string(), openStream.status().message());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<arrow::io::ReadableFile> infile;
|
||||
PARQUET_ASSIGN_OR_THROW(infile, openStream);
|
||||
|
||||
try {
|
||||
parquet::StreamReader streamReader{parquet::ParquetFileReader::Open(infile)};
|
||||
return std::make_shared<SerdeObjectReader>(std::move(streamReader));
|
||||
} catch (const std::exception &ex) {
|
||||
XLOGF(ERR, "Failed to create stream reader: {}, error: {}", path.string(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
SerdeObjectReader &operator>>(SerdeType &v) {
|
||||
eof_ = eof();
|
||||
if (!bool(*this)) return *this;
|
||||
|
||||
try {
|
||||
visit("", v);
|
||||
reader_ >> parquet::EndRow;
|
||||
} catch (const parquet::ParquetException &ex) {
|
||||
XLOGF(CRITICAL, "Failed to read from parquet file, error: {}", ex.what());
|
||||
isOk_ = false;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
operator bool() const { return ok() && !eof_; }
|
||||
|
||||
bool ok() const { return isOk_; }
|
||||
|
||||
bool eof() const { return reader_.eof(); }
|
||||
|
||||
size_t numRows() const { return reader_.num_rows(); }
|
||||
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view k, T &v) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
reader_ >> v;
|
||||
XLOGF(DBG3, "arithmetic visit({}): {}", k, v);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
int32_t n;
|
||||
reader_ >> n;
|
||||
XLOGF(DBG3, "enum visit({}): {}", k, n);
|
||||
auto result = magic_enum::enum_cast<T>(n);
|
||||
if (result) {
|
||||
v = *result;
|
||||
} else {
|
||||
XLOGF(CRITICAL, "Failed to parse enum {} from value: {}", nameof::nameof_short_type<T>(), n);
|
||||
}
|
||||
}
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
reader_ >> v;
|
||||
XLOGF(DBG3, "string visit({}): {}", k, v);
|
||||
}
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
BaseObjectVisitor<SerdeObjectReader>::visit(k, v);
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMethod visit({})", k);
|
||||
typename serde::SerdeToReadableReturnType<T> serialized;
|
||||
visit(k, serialized);
|
||||
auto result = serde::SerdeMethod<T>::serdeFromReadable(serialized);
|
||||
if (result) {
|
||||
val = *result;
|
||||
} else {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from value: {}", nameof::nameof_short_type<T>(), serialized);
|
||||
}
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "WithSerdeMethod visit({})", k);
|
||||
typename serde::SerdeToReturnType<T> serialized;
|
||||
visit(k, serialized);
|
||||
auto result = serde::SerdeMethod<T>::serdeFrom(serialized);
|
||||
if (result) {
|
||||
val = *result;
|
||||
} else {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from value: {}", nameof::nameof_short_type<T>(), serialized);
|
||||
}
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMemberMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "WithReadableSerdeMemberMethod visit({})", k);
|
||||
serde::SerdeToReadableMemberMethodReturnType<T> serialized;
|
||||
visit(k, serialized);
|
||||
auto result = T::serdeFromReadable(serialized);
|
||||
if (result) {
|
||||
val = *result;
|
||||
} else {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from value: {}", nameof::nameof_short_type<T>(), serialized);
|
||||
}
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMemberMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "WithSerdeMemberMethod visit({})", k);
|
||||
serde::SerdeToReadableMemberMethodReturnType<T> serialized;
|
||||
visit(k, serialized);
|
||||
auto result = T::serdeFromReadable(serialized);
|
||||
if (result) {
|
||||
val = *result;
|
||||
} else {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from value: {}", nameof::nameof_short_type<T>(), serialized);
|
||||
}
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
BaseObjectVisitor<SerdeObjectReader>::visit(k, val);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_specialization_of_v<T, folly::Expected>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "result visit({})", k);
|
||||
std::string errorColumnName = std::string{k} + kResultErrorTypeColumnSuffix;
|
||||
|
||||
Status status(StatusCode::kOK);
|
||||
typename T::value_type value;
|
||||
visit<typename T::error_type>(errorColumnName, status);
|
||||
visit<typename T::value_type>(k, value);
|
||||
|
||||
if (status.isOK()) {
|
||||
val = std::move(value);
|
||||
} else {
|
||||
val = makeError(status);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
// get the index of value
|
||||
std::string valIdxColumnName = std::string{k} + kVariantValueIndexColumnSuffix;
|
||||
uint32_t valIdx = 0;
|
||||
visit<uint32_t>(valIdxColumnName, valIdx);
|
||||
|
||||
// read and set the value
|
||||
uint32_t altIdx = 0;
|
||||
visitVariant(val, [&](std::string_view typeName, auto &&v) {
|
||||
std::string altTypeName = std::string{k} + std::string{typeName};
|
||||
std::remove_reference_t<decltype(v)> alt;
|
||||
visit(altTypeName, alt);
|
||||
if (altIdx == valIdx) val = std::move(alt);
|
||||
altIdx++;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
std::string str;
|
||||
reader_ >> str;
|
||||
XLOGF(DBG3, "container visit({}): {}", k, str);
|
||||
auto result = serde::fromJsonString(val, str);
|
||||
if (!result) {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from json string: {}", nameof::nameof_short_type<T>(), str);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
std::string str;
|
||||
reader_ >> str;
|
||||
XLOGF(DBG3, "container visit({}): {}", k, str);
|
||||
if (str.empty()) {
|
||||
val = std::nullopt;
|
||||
} else {
|
||||
using ValueType = typename T::value_type;
|
||||
val = ValueType();
|
||||
auto result = serde::fromJsonString(*val, str);
|
||||
if (!result) {
|
||||
XLOGF(CRITICAL, "Failed to parse {} from json string: {}", nameof::nameof_short_type<ValueType>(), str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
parquet::StreamReader reader_;
|
||||
bool isOk_{true};
|
||||
bool eof_{false};
|
||||
};
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
129
src/analytics/SerdeObjectVisitor.h
Normal file
129
src/analytics/SerdeObjectVisitor.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "Common.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
class ObjectVisitor {
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view, T &) = delete;
|
||||
};
|
||||
|
||||
template <size_t I = 0>
|
||||
inline void visitVariant(auto &&t, auto &&func) {
|
||||
using T = std::decay_t<decltype(t)>;
|
||||
using S = std::variant_alternative_t<I, T>;
|
||||
if (t.index() == I) {
|
||||
func(nameof::nameof_short_type<S>(), std::get<I>(t));
|
||||
} else {
|
||||
func(nameof::nameof_short_type<S>(), std::variant_alternative_t<I, T>{});
|
||||
}
|
||||
if constexpr (I + 1 < std::variant_size_v<T>) {
|
||||
visitVariant<I + 1>(t, func);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Derived>
|
||||
class BaseObjectVisitor : public ObjectVisitor {
|
||||
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 &v) {
|
||||
XLOGF(DBG3, "arithmetic visit({})", k);
|
||||
static_cast<Derived *>(this)->template visit<T>(k, v);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k, T &) = delete;
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k, T &) = delete;
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k, T &v) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
static_cast<Derived *>(this)->template visit<typename T::UnderlyingType>(k, v);
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
refl::Helper::iterate<T>(
|
||||
[&](auto type) { static_cast<Derived *>(this)->template visit(type.name, val.*type.getter); });
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
visitVariant(val, [&](std::string_view typeName, auto &&v) {
|
||||
std::string altTypeName = std::string{k} + std::string{typeName};
|
||||
static_cast<Derived *>(this)->template visit(altTypeName, v);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
for (auto item : val) {
|
||||
static_cast<Derived *>(this)->template visit(k, item);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k, T &val) {
|
||||
XLOGF(DBG3, "optional visit({})", k);
|
||||
using ValueType = typename T::value_type;
|
||||
if (val.has_value()) {
|
||||
static_cast<Derived *>(this)->template visit<ValueType>(k, *val);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
241
src/analytics/SerdeObjectWriter.h
Normal file
241
src/analytics/SerdeObjectWriter.h
Normal file
@@ -0,0 +1,241 @@
|
||||
#pragma once
|
||||
#include <arrow/io/file.h>
|
||||
#include <parquet/stream_writer.h>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "SerdeObjectVisitor.h"
|
||||
#include "SerdeSchemaBuilder.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
template <serde::SerdeType SerdeType>
|
||||
class SerdeObjectWriter : public BaseObjectVisitor<SerdeObjectWriter<SerdeType>> {
|
||||
public:
|
||||
SerdeObjectWriter(parquet::StreamWriter &&writer)
|
||||
: writer_(std::move(writer)),
|
||||
createTime_(UtcClock::now()) {}
|
||||
|
||||
static std::shared_ptr<SerdeObjectWriter> open(const Path path,
|
||||
const bool append = false,
|
||||
const size_t maxRowGroupLength = 1'000'000,
|
||||
const std::vector<parquet::SortingColumn> &sortedColumns = {}) {
|
||||
// open file
|
||||
auto openStream = arrow::io::FileOutputStream::Open(path.string(), append);
|
||||
|
||||
if (!openStream.ok()) {
|
||||
XLOGF(ERR, "Failed to open file output stream: {}, error: {}", path.string(), openStream.status().message());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<arrow::io::FileOutputStream> outfile;
|
||||
PARQUET_ASSIGN_OR_THROW(outfile, openStream);
|
||||
|
||||
// generate schema
|
||||
SerdeSchemaBuilder<SerdeType> schemaBuilder;
|
||||
auto schemaNode = schemaBuilder.getSchema();
|
||||
|
||||
if (schemaNode == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
parquet::WriterProperties::Builder writerBuilder;
|
||||
writerBuilder.set_sorting_columns(sortedColumns);
|
||||
writerBuilder.max_row_group_length(maxRowGroupLength);
|
||||
writerBuilder.data_page_version(parquet::ParquetDataPageVersion::V2);
|
||||
|
||||
// set global encoding and compression method
|
||||
// writerBuilder.encoding(parquet::Encoding::DELTA_BINARY_PACKED);
|
||||
writerBuilder.compression(parquet::Compression::ZSTD);
|
||||
|
||||
// set encoding for string columns
|
||||
// for (int fieldIndex = 0; fieldIndex < schemaNode->field_count(); fieldIndex++) {
|
||||
// auto fieldNode = schemaNode->field(fieldIndex);
|
||||
// auto fieldType = fieldNode->logical_type();
|
||||
// if (fieldType->is_string()) writerBuilder.encoding(fieldNode->name(),
|
||||
// parquet::Encoding::DELTA_LENGTH_BYTE_ARRAY);
|
||||
// }
|
||||
|
||||
try {
|
||||
auto fileWriter = parquet::ParquetFileWriter::Open(outfile, schemaNode, writerBuilder.build());
|
||||
if (fileWriter == nullptr) {
|
||||
XLOGF(ERR, "Failed to open file writer: {}", path.string());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
parquet::StreamWriter streamWriter(std::move(fileWriter));
|
||||
return std::make_shared<SerdeObjectWriter>(std::move(streamWriter));
|
||||
|
||||
} catch (const std::exception &ex) {
|
||||
XLOGF(ERR, "Failed to create stream writer: {}, error: {}", path.string(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
SerdeObjectWriter &operator<<(const SerdeType &v) {
|
||||
if (!bool(*this)) return *this;
|
||||
|
||||
try {
|
||||
visit("", v);
|
||||
writer_ << parquet::EndRow;
|
||||
} catch (const parquet::ParquetException &ex) {
|
||||
XLOGF(CRITICAL, "Failed to write to parquet file, error: {}", ex.what());
|
||||
isOk_ = false;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void endRowGroup() { writer_.EndRowGroup(); }
|
||||
|
||||
UtcTime createTime() { return createTime_; }
|
||||
|
||||
operator bool() const { return ok(); }
|
||||
|
||||
bool ok() const { return isOk_; }
|
||||
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view k, const T &v) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
XLOGF(DBG3, "arithmetic visit({})", k);
|
||||
writer_ << v;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
XLOGF(DBG3, "enum visit({})", k);
|
||||
writer_ << (int32_t)v;
|
||||
}
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
XLOGF(DBG3, "string visit({})", k);
|
||||
writer_ << v;
|
||||
}
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
visit<typename T::UnderlyingType>(k, v.toUnderType());
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMethod T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
auto serialized = serde::SerdeMethod<T>::serdeToReadable(val);
|
||||
XLOGF(DBG3,
|
||||
"WithReadableSerdeMethod visit({}), serialized: {} {}",
|
||||
k,
|
||||
nameof::nameof_type<decltype(serialized)>(),
|
||||
serialized);
|
||||
visit<serde::SerdeToReadableReturnType<T>>(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMethod T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
auto serialized = serde::SerdeMethod<T>::serdeTo(val);
|
||||
XLOGF(DBG3,
|
||||
"WithSerdeMethod visit({}), serialized: {} {}",
|
||||
k,
|
||||
nameof::nameof_type<decltype(serialized)>(),
|
||||
serialized);
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithReadableSerdeMemberMethod T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
auto serialized = v.serdeToReadable();
|
||||
XLOGF(DBG3,
|
||||
"WithReadableSerdeMemberMethod visit({}), serialized: {} {}",
|
||||
k,
|
||||
nameof::nameof_type<decltype(serialized)>(),
|
||||
serialized);
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::WithSerdeMemberMethod T>
|
||||
void visit(std::string_view k, const T &v) {
|
||||
auto serialized = v.serdeTo();
|
||||
XLOGF(DBG3,
|
||||
"WithSerdeMemberMethod visit({}), serialized: {} {}",
|
||||
k,
|
||||
nameof::nameof_type<decltype(serialized)>(),
|
||||
serialized);
|
||||
visit(k, serialized);
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
BaseObjectVisitor<SerdeObjectWriter>::visit(k, const_cast<T &>(val));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_specialization_of_v<T, folly::Expected>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
XLOGF(DBG3, "result visit({})", k);
|
||||
std::string errorColumnName = std::string{k} + kResultErrorTypeColumnSuffix;
|
||||
|
||||
if (val.hasValue()) {
|
||||
Status ok(StatusCode::kOK);
|
||||
visit<typename T::error_type>(errorColumnName, ok);
|
||||
visit<typename T::value_type>(k, val.value());
|
||||
} else {
|
||||
typename T::value_type value{};
|
||||
visit<typename T::error_type>(errorColumnName, val.error());
|
||||
visit<typename T::value_type>(k, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
std::string valIdxColumnName = std::string{k} + kVariantValueIndexColumnSuffix;
|
||||
visit<uint32_t>(valIdxColumnName, val.index());
|
||||
BaseObjectVisitor<SerdeObjectWriter>::visit(k, const_cast<T &>(val));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
auto str = serde::toJsonString(val);
|
||||
writer_ << str;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k, const T &val) {
|
||||
XLOGF(DBG3, "optional visit({})", k);
|
||||
if (!val.has_value()) {
|
||||
writer_ << "";
|
||||
} else {
|
||||
auto str = serde::toJsonString(*val);
|
||||
writer_ << str;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
parquet::StreamWriter writer_;
|
||||
UtcTime createTime_;
|
||||
bool isOk_{true};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
SerdeObjectWriter<T> &operator<<(SerdeObjectWriter<T> &writer, parquet::EndRowGroupType) {
|
||||
writer.endRowGroup();
|
||||
return writer;
|
||||
}
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
219
src/analytics/SerdeSchemaBuilder.h
Normal file
219
src/analytics/SerdeSchemaBuilder.h
Normal file
@@ -0,0 +1,219 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <numeric>
|
||||
#include <parquet/exception.h>
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "SerdeStructVisitor.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
template <serde::SerdeType SerdeType>
|
||||
class SerdeSchemaBuilder : public BaseStructVisitor<SerdeSchemaBuilder<SerdeType>> {
|
||||
public:
|
||||
std::shared_ptr<parquet::schema::GroupNode> getSchema() {
|
||||
try {
|
||||
fields_.clear();
|
||||
fieldNameParts_.clear();
|
||||
this->visit<SerdeType>("");
|
||||
return std::static_pointer_cast<parquet::schema::GroupNode>(
|
||||
parquet::schema::GroupNode::Make("schema", parquet::Repetition::REQUIRED, fields_));
|
||||
} catch (const parquet::ParquetException &ex) {
|
||||
XLOGF(CRITICAL, "Failed to build schema of type {}, error: {}", nameof::nameof_full_type<SerdeType>(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view k) = delete;
|
||||
|
||||
template <>
|
||||
void visit<bool>(std::string_view k) {
|
||||
XLOGF(DBG3, "bool visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::None(),
|
||||
parquet::Type::BOOLEAN));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int16_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int16_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(16, true),
|
||||
parquet::Type::INT32));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<uint16_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint16_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(16, false),
|
||||
parquet::Type::INT32));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int32_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int32_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(32, true),
|
||||
parquet::Type::INT32));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<uint32_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint32_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(32, false),
|
||||
parquet::Type::INT32));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<int64_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "int64_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(64, true),
|
||||
parquet::Type::INT64));
|
||||
}
|
||||
|
||||
template <>
|
||||
void visit<uint64_t>(std::string_view k) {
|
||||
XLOGF(DBG3, "uint64_t visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(64, false),
|
||||
parquet::Type::INT64));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "enum visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::Int(32, true),
|
||||
parquet::Type::INT32));
|
||||
}
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "string visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::String(),
|
||||
parquet::Type::BYTE_ARRAY));
|
||||
}
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "strongtyped visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
BaseStructVisitor<SerdeSchemaBuilder>::template 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 <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "serdetype visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
if (!k.empty()) fieldNameParts_.push_back(filterOutInvalidChars(k));
|
||||
BaseStructVisitor<SerdeSchemaBuilder>::template visit<T>(k);
|
||||
if (!k.empty()) fieldNameParts_.pop_back();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_specialization_of_v<T, folly::Expected>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "result visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
std::string errorColumnName = std::string{k} + kResultErrorTypeColumnSuffix;
|
||||
visit<typename T::error_type>(errorColumnName);
|
||||
visit<typename T::value_type>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "variant visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
std::string valIdxColumnName = std::string{k} + kVariantValueIndexColumnSuffix;
|
||||
visit<uint32_t>(valIdxColumnName);
|
||||
BaseStructVisitor<SerdeSchemaBuilder>::template 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({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::String(),
|
||||
parquet::Type::BYTE_ARRAY));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "container visit({}), fullname: '{}'", k, getFieldFullName(k));
|
||||
fields_.push_back(parquet::schema::PrimitiveNode::Make(getFieldFullName(k),
|
||||
parquet::Repetition::REQUIRED,
|
||||
parquet::LogicalType::String(),
|
||||
parquet::Type::BYTE_ARRAY));
|
||||
}
|
||||
|
||||
private:
|
||||
std::string getFieldFullName(std::string_view k) {
|
||||
fieldNameParts_.push_back(filterOutInvalidChars(k));
|
||||
auto fieldFullName = std::accumulate(fieldNameParts_.begin(),
|
||||
fieldNameParts_.end(),
|
||||
std::string{},
|
||||
[](const std::string &a, const std::string &b) {
|
||||
return a + (a.empty() ? std::string{} : std::string("_")) + b;
|
||||
});
|
||||
fieldNameParts_.pop_back();
|
||||
return fieldFullName;
|
||||
}
|
||||
|
||||
std::string filterOutInvalidChars(std::string_view k) {
|
||||
return std::accumulate(k.begin(), k.end(), std::string{}, [](const std::string &a, const char b) {
|
||||
if (('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || ('0' <= b && b <= '9')) return a + b;
|
||||
return a;
|
||||
});
|
||||
};
|
||||
|
||||
private:
|
||||
parquet::schema::NodeVector fields_;
|
||||
std::vector<std::string> fieldNameParts_;
|
||||
};
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
123
src/analytics/SerdeStructVisitor.h
Normal file
123
src/analytics/SerdeStructVisitor.h
Normal file
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "Common.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Nameof.hpp"
|
||||
#include "common/utils/StrongType.h"
|
||||
#include "common/utils/TypeTraits.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
class StructVisitor {
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view) = delete;
|
||||
};
|
||||
|
||||
template <class T, size_t I = 0>
|
||||
inline void visitVariant(auto &&func) {
|
||||
using S = std::variant_alternative_t<I, T>;
|
||||
func(nameof::nameof_short_type<S>(), std::type_identity<S>{});
|
||||
if constexpr (I + 1 < std::variant_size_v<T>) {
|
||||
visitVariant<T, I + 1>(func);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Derived>
|
||||
class BaseStructVisitor : public StructVisitor {
|
||||
public:
|
||||
// default
|
||||
template <typename T>
|
||||
void visit(std::string_view k) = delete;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "arithmetic visit({})", k);
|
||||
static_cast<Derived *>(this)->template visit<T>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_enum_v<T>
|
||||
void visit(std::string_view k) = delete;
|
||||
|
||||
template <serde::ConvertibleToString T>
|
||||
void visit(std::string_view k) = delete;
|
||||
|
||||
template <StrongTyped T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "strongtyped visit({})", k);
|
||||
static_cast<Derived *>(this)->template visit<typename T::UnderlyingType>(k);
|
||||
}
|
||||
|
||||
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "serdetype visit({})", k);
|
||||
refl::Helper::iterate<T>([&](auto field) {
|
||||
using FieldType = std::decay_t<decltype(std::declval<T>().*field.getter)>;
|
||||
static_cast<Derived *>(this)->template visit<FieldType>(field.name);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_variant_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "variant visit({})", k);
|
||||
visitVariant<T>([&](std::string_view typeName, auto &&v) {
|
||||
using AlternativeType = typename std::decay_t<decltype(v)>::type;
|
||||
std::string altTypeName = std::string{k} + std::string{typeName};
|
||||
static_cast<Derived *>(this)->template visit<AlternativeType>(altTypeName);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_vector_v<T> || is_set_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "container visit({})", k);
|
||||
using ElemValueType = typename T::value_type;
|
||||
static_cast<Derived *>(this)->template visit<ElemValueType>(k);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires is_optional_v<T>
|
||||
void visit(std::string_view k) {
|
||||
XLOGF(DBG3, "optional visit({})", k);
|
||||
using ValueType = typename T::value_type;
|
||||
static_cast<Derived *>(this)->template visit<ValueType>(k);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
285
src/analytics/StructuredTraceLog.h
Normal file
285
src/analytics/StructuredTraceLog.h
Normal file
@@ -0,0 +1,285 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/Random.h>
|
||||
#include <folly/concurrency/UnboundedQueue.h>
|
||||
#include <future>
|
||||
|
||||
#include "SerdeObjectWriter.h"
|
||||
#include "common/monitor/Recorder.h"
|
||||
#include "common/monitor/ScopedMetricsWriter.h"
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "common/utils/Path.h"
|
||||
#include "common/utils/SysResource.h"
|
||||
#include "common/utils/UtcTime.h"
|
||||
|
||||
namespace hf3fs::analytics {
|
||||
|
||||
template <serde::SerdeType SerdeType>
|
||||
class StructuredTraceLog : public folly::MoveOnly {
|
||||
struct TraceMeta {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(hostname, String{});
|
||||
};
|
||||
|
||||
struct StructuredTrace {
|
||||
SERDE_STRUCT_FIELD(trace_meta, TraceMeta{});
|
||||
SERDE_STRUCT_FIELD(_, SerdeType{});
|
||||
};
|
||||
|
||||
using WriterType = SerdeObjectWriter<StructuredTrace>;
|
||||
using WriterPtr = std::shared_ptr<WriterType>;
|
||||
|
||||
public:
|
||||
class Config : public hf3fs::ConfigBase<Config> {
|
||||
public:
|
||||
CONFIG_ITEM(trace_file_dir, Path{"."});
|
||||
#ifndef NDEBUG
|
||||
CONFIG_HOT_UPDATED_ITEM(enabled, false);
|
||||
CONFIG_HOT_UPDATED_ITEM(dump_interval, 60_min);
|
||||
#else
|
||||
CONFIG_HOT_UPDATED_ITEM(enabled, true);
|
||||
CONFIG_HOT_UPDATED_ITEM(dump_interval, 30_s);
|
||||
#endif
|
||||
CONFIG_HOT_UPDATED_ITEM(max_num_writers, size_t{1}, ConfigCheckers::checkPositive);
|
||||
CONFIG_HOT_UPDATED_ITEM(max_row_group_length, size_t{100'000});
|
||||
};
|
||||
|
||||
public:
|
||||
StructuredTraceLog(const Config &config)
|
||||
: config_(config),
|
||||
enabled_(config.enabled()),
|
||||
typename_(nameof::nameof_short_type<SerdeType>()),
|
||||
hostname_(SysResource::hostname().value_or("unknown_host")),
|
||||
latencyTagSet_({{"tag", typename_}, {"instance", fmt::to_string(fmt::ptr(this))}}),
|
||||
createLatency_("trace_log.create_latency", latencyTagSet_),
|
||||
appendLatency_("trace_log.append_latency", latencyTagSet_),
|
||||
flushLatency_("trace_log.flush_latency", latencyTagSet_),
|
||||
maxNumWriters_(config.max_num_writers()) {
|
||||
onConfigUpdated_ = config_.addCallbackGuard([this]() {
|
||||
bool enabled = config_.enabled();
|
||||
if (enabled_ != enabled) {
|
||||
enableTraceLog(enabled);
|
||||
if (!enabled) flush(false /*async*/);
|
||||
}
|
||||
|
||||
if (maxNumWriters_ != config_.max_num_writers()) {
|
||||
updateMaxNumWriters(config_.max_num_writers());
|
||||
}
|
||||
});
|
||||
|
||||
uint64_t secsUntilFirstDump =
|
||||
folly::Random::rand64(config_.dump_interval().asSec().count() / 2, config_.dump_interval().asSec().count());
|
||||
nextDumpTime_ = microsecondsSinceEpoch(UtcClock::now() + std::chrono::seconds{secsUntilFirstDump});
|
||||
}
|
||||
|
||||
~StructuredTraceLog() { close(); }
|
||||
|
||||
bool open() {
|
||||
auto writer = getOrCreateWriter();
|
||||
if (!writer) return false;
|
||||
writerPool_.enqueue(writer);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<SerdeType> newEntry(const SerdeType &init = SerdeType{}) {
|
||||
auto ptr = new SerdeType(init);
|
||||
return std::shared_ptr<SerdeType>(ptr, [this](SerdeType *ptr) {
|
||||
this->append(*ptr);
|
||||
delete ptr;
|
||||
});
|
||||
}
|
||||
|
||||
void append(const SerdeType &msg) {
|
||||
if (!enabled_) return;
|
||||
|
||||
{
|
||||
monitor::ScopedLatencyWriter appendLatency(appendLatency_);
|
||||
StructuredTrace trace{
|
||||
.trace_meta = TraceMeta{.timestamp = UtcClock::secondsSinceEpoch(), .hostname = hostname_},
|
||||
._ = msg,
|
||||
};
|
||||
|
||||
WriterPtr writer = getOrCreateWriter();
|
||||
|
||||
if (UNLIKELY(writer == nullptr)) {
|
||||
XLOGF(CRITICAL, "Cannot get a writer of {} trace log in directory {}", typename_, config_.trace_file_dir());
|
||||
enableTraceLog(false);
|
||||
return;
|
||||
}
|
||||
|
||||
*writer << trace;
|
||||
auto writerOk = writer->ok();
|
||||
writerPool_.enqueue(std::move(writer));
|
||||
if (UNLIKELY(!writerOk)) enableTraceLog(false);
|
||||
}
|
||||
|
||||
auto currentTime = microsecondsSinceEpoch(UtcClock::now());
|
||||
|
||||
if (UNLIKELY(currentTime >= nextDumpTime_)) {
|
||||
nextDumpTime_ = currentTime + config_.dump_interval().asUs().count();
|
||||
flush(true /*async*/);
|
||||
}
|
||||
}
|
||||
|
||||
void flush(bool async, bool shutdown = false) {
|
||||
auto running = dumpingTrace_.test_and_set();
|
||||
if (running) return;
|
||||
|
||||
monitor::ScopedLatencyWriter flushLatency(flushLatency_);
|
||||
if (asyncFlush_.valid()) asyncFlush_.wait();
|
||||
|
||||
asyncFlush_ = std::async(
|
||||
std::launch::async,
|
||||
[this](bool shutdown) {
|
||||
size_t numWritersToClose = numWriters_.load();
|
||||
auto now = UtcClock::now();
|
||||
|
||||
XLOGF(INFO,
|
||||
"Flushing {} {} log writers in directory {}",
|
||||
numWritersToClose,
|
||||
typename_,
|
||||
config_.trace_file_dir());
|
||||
|
||||
for (size_t i = 0; numWritersToClose > 0; i++) {
|
||||
// give up flushing old writers after trying for too many loops
|
||||
if (i >= 10 * maxNumWriters_) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto writer = writerPool_.dequeue();
|
||||
if (!writer) continue;
|
||||
|
||||
if (writer->createTime() > now) {
|
||||
writerPool_.enqueue(writer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (writer->ok()) {
|
||||
// add an empty trace at the end of log
|
||||
*writer << StructuredTrace{
|
||||
.trace_meta = {.timestamp = UtcClock::secondsSinceEpoch(), .hostname = hostname_}};
|
||||
}
|
||||
|
||||
try {
|
||||
writer.reset();
|
||||
} catch (const std::exception &ex) {
|
||||
XLOGF(ERR,
|
||||
"Failed to close {} log writer in directory {}, error: {}",
|
||||
typename_,
|
||||
config_.trace_file_dir(),
|
||||
ex.what());
|
||||
}
|
||||
|
||||
if (shutdown)
|
||||
numWriters_--;
|
||||
else
|
||||
writerPool_.enqueue(createNewWriter());
|
||||
|
||||
numWritersToClose--;
|
||||
}
|
||||
|
||||
if (numWritersToClose > 0) {
|
||||
XLOGF(WARN,
|
||||
"Still have {} {} log writers not closed in directory {}",
|
||||
numWritersToClose,
|
||||
typename_,
|
||||
config_.trace_file_dir());
|
||||
} else {
|
||||
XLOGF(INFO, "Flushed {} trace log in directory {}", typename_, config_.trace_file_dir());
|
||||
}
|
||||
},
|
||||
shutdown);
|
||||
|
||||
if (!async) asyncFlush_.wait();
|
||||
dumpingTrace_.clear();
|
||||
}
|
||||
|
||||
void close() {
|
||||
enableTraceLog(false);
|
||||
flush(false /*async*/, true /*shutdown*/);
|
||||
XLOGF(INFO, "Closed {} trace log in directory {}", typename_, config_.trace_file_dir());
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t microsecondsSinceEpoch(const UtcTime &time) const {
|
||||
return std::chrono::duration_cast<std::chrono::microseconds>((time).time_since_epoch()).count();
|
||||
}
|
||||
|
||||
WriterPtr getOrCreateWriter() {
|
||||
WriterPtr writer;
|
||||
if (writerPool_.try_dequeue(writer)) return writer;
|
||||
|
||||
auto currentNumWriters = numWriters_.load();
|
||||
if (currentNumWriters < maxNumWriters_) {
|
||||
bool create = numWriters_.compare_exchange_strong(currentNumWriters, currentNumWriters + 1);
|
||||
if (create) return createNewWriter();
|
||||
}
|
||||
|
||||
return writerPool_.dequeue();
|
||||
}
|
||||
|
||||
WriterPtr createNewWriter() {
|
||||
monitor::ScopedLatencyWriter createLatency(createLatency_);
|
||||
auto timestamp = fmt::localtime(UtcClock::to_time_t(UtcClock::now()));
|
||||
Path logfilePath =
|
||||
config_.trace_file_dir() / Path{fmt::format("{:%Y-%m-%d}", timestamp)} / Path{hostname_} /
|
||||
Path{
|
||||
fmt::format("{}.{}.{:%Y-%m-%d-%H-%M-%S}.{}.parquet", typename_, hostname_, timestamp, nextLogFileIndex_++)};
|
||||
|
||||
if (!boost::filesystem::exists(logfilePath.parent_path())) {
|
||||
boost::system::error_code err{};
|
||||
boost::filesystem::create_directories(logfilePath.parent_path(), err);
|
||||
if (UNLIKELY(err.failed())) {
|
||||
XLOGF(CRITICAL, "Failed to create directory {}, error: {}", logfilePath.parent_path(), err.message());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
XLOGF(INFO, "Opening {} trace log: {}", typename_, logfilePath);
|
||||
return WriterType::open(logfilePath, false /*append*/, config_.max_row_group_length());
|
||||
}
|
||||
|
||||
void enableTraceLog(bool enable) {
|
||||
enabled_ = enable;
|
||||
XLOGF(INFO,
|
||||
"{} {} trace log in directory {}",
|
||||
enable ? "Enabled" : "Disabled",
|
||||
typename_,
|
||||
config_.trace_file_dir());
|
||||
}
|
||||
|
||||
void updateMaxNumWriters(size_t newMaxNumWriters) {
|
||||
XLOGF(INFO,
|
||||
"Update max num of writers from {} to {} for {} trace log in directory {}",
|
||||
maxNumWriters_.load(),
|
||||
newMaxNumWriters,
|
||||
typename_,
|
||||
config_.trace_file_dir());
|
||||
bool doFlush = maxNumWriters_ > newMaxNumWriters;
|
||||
maxNumWriters_ = newMaxNumWriters;
|
||||
if (doFlush) flush(false /*async*/);
|
||||
}
|
||||
|
||||
private:
|
||||
const Config &config_;
|
||||
bool enabled_ = false;
|
||||
const std::string typename_;
|
||||
const std::string hostname_;
|
||||
|
||||
const monitor::TagSet latencyTagSet_;
|
||||
monitor::LatencyRecorder createLatency_;
|
||||
monitor::LatencyRecorder appendLatency_;
|
||||
monitor::LatencyRecorder flushLatency_;
|
||||
|
||||
std::unique_ptr<ConfigCallbackGuard> onConfigUpdated_;
|
||||
std::atomic_size_t maxNumWriters_;
|
||||
std::atomic_size_t numWriters_ = 0;
|
||||
std::atomic_size_t nextLogFileIndex_ = 1;
|
||||
folly::UnboundedQueue<WriterPtr, false, false, true> writerPool_;
|
||||
|
||||
std::atomic_uint64_t nextDumpTime_;
|
||||
std::atomic_flag dumpingTrace_;
|
||||
std::future<void> asyncFlush_;
|
||||
};
|
||||
|
||||
} // namespace hf3fs::analytics
|
||||
6
src/client/CMakeLists.txt
Normal file
6
src/client/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(cli)
|
||||
add_subdirectory(bin)
|
||||
add_subdirectory(mgmtd)
|
||||
add_subdirectory(meta)
|
||||
add_subdirectory(storage)
|
||||
add_subdirectory(core)
|
||||
2
src/client/bin/CMakeLists.txt
Normal file
2
src/client/bin/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
target_add_bin(admin_cli "admin_cli.cc" admin-cli)
|
||||
|
||||
238
src/client/bin/admin_cli.cc
Normal file
238
src/client/bin/admin_cli.cc
Normal file
@@ -0,0 +1,238 @@
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <exception>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/init/Init.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <iostream>
|
||||
|
||||
#include "client/cli/admin/AdminEnv.h"
|
||||
#include "client/cli/admin/registerAdminCommands.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/meta/MetaClient.h"
|
||||
#include "client/mgmtd/MgmtdClientForAdmin.h"
|
||||
#include "common/logging/LogInit.h"
|
||||
#include "common/net/Client.h"
|
||||
#include "common/net/ib/IBDevice.h"
|
||||
#include "common/utils/ConfigBase.h"
|
||||
#include "fdb/FDBContext.h"
|
||||
#include "fdb/FDBKVEngine.h"
|
||||
#include "stubs/MetaService/MetaServiceStub.h"
|
||||
#include "stubs/common/RealStubFactory.h"
|
||||
#include "stubs/core/CoreServiceStub.h"
|
||||
#include "stubs/mgmtd/MgmtdServiceStub.h"
|
||||
|
||||
DECLARE_bool(release_version);
|
||||
|
||||
using namespace hf3fs;
|
||||
using namespace hf3fs::client;
|
||||
using namespace hf3fs::client::cli;
|
||||
using namespace hf3fs::stubs;
|
||||
using hf3fs::client::CoreClient;
|
||||
using hf3fs::meta::client::MetaClient;
|
||||
using hf3fs::storage::client::StorageClient;
|
||||
|
||||
namespace {
|
||||
class UserConfig : public ConfigBase<UserConfig> {
|
||||
CONFIG_ITEM(uid, int64_t{-1});
|
||||
CONFIG_ITEM(gid, int64_t{-1});
|
||||
CONFIG_ITEM(gids, std::vector<uint32_t>{});
|
||||
CONFIG_ITEM(token, "");
|
||||
};
|
||||
|
||||
class Config : public ConfigBase<Config> {
|
||||
CONFIG_OBJ(client, net::Client::Config);
|
||||
CONFIG_OBJ(ib_devices, net::IBDevice::Config);
|
||||
CONFIG_ITEM(cluster_id, "");
|
||||
CONFIG_OBJ(user_info, UserConfig);
|
||||
CONFIG_ITEM(log, "DBG:normal; normal=file:path=cli.log,async=true,sync_level=ERR", ConfigCheckers::checkNotEmpty);
|
||||
CONFIG_ITEM(num_timeout_ms, 1000L);
|
||||
CONFIG_ITEM(verbose, false);
|
||||
CONFIG_ITEM(profile, false);
|
||||
CONFIG_ITEM(break_multi_line_command_on_failure, false);
|
||||
CONFIG_OBJ(fdb, kv::fdb::FDBConfig);
|
||||
CONFIG_OBJ(mgmtd_client, MgmtdClientForAdmin::Config, [&](auto &c) {
|
||||
c.set_enable_auto_refresh(true);
|
||||
c.set_auto_refresh_interval(1_s);
|
||||
c.set_enable_auto_heartbeat(false);
|
||||
c.set_enable_auto_extend_client_session(false);
|
||||
});
|
||||
CONFIG_OBJ(meta_client, MetaClient::Config);
|
||||
CONFIG_OBJ(storage_client, StorageClient::Config);
|
||||
CONFIG_OBJ(monitor, hf3fs::monitor::Monitor::Config);
|
||||
};
|
||||
|
||||
flat::UserInfo generateUserInfo(const UserConfig &cfg) {
|
||||
auto uid = cfg.uid() != -1 ? cfg.uid() : static_cast<int64_t>(geteuid());
|
||||
auto gid = cfg.gid() != -1 ? cfg.gid() : static_cast<int64_t>(getegid());
|
||||
std::vector<flat::Gid> groups;
|
||||
if (cfg.gids().empty()) {
|
||||
auto n = getgroups(0, nullptr);
|
||||
std::vector<gid_t> gs(n);
|
||||
if (getgroups(n, gs.data()) < 0) {
|
||||
XLOGF(ERR, "failed to get supplementary groups for uid {}, going with egid {} only", uid, gid);
|
||||
}
|
||||
for (auto g : gs) groups.emplace_back(g);
|
||||
} else {
|
||||
for (auto g : cfg.gids()) groups.emplace_back(g);
|
||||
}
|
||||
return flat::UserInfo(flat::Uid(uid), flat::Gid(gid), std::move(groups), cfg.token());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
Config config;
|
||||
auto initResult = config.init(&argc, &argv);
|
||||
XLOGF_IF(FATAL, !initResult, "Load config from flags failed with {}", initResult.error().describe());
|
||||
|
||||
if (FLAGS_release_version) {
|
||||
fmt::print("{}\n{}\n", VersionInfo::full(), VersionInfo::commitHashFull());
|
||||
return 0;
|
||||
}
|
||||
|
||||
logging::initOrDie(config.log());
|
||||
hf3fs::SysResource::increaseProcessFDLimit(524288);
|
||||
|
||||
std::shared_ptr<net::Client> client;
|
||||
std::shared_ptr<kv::fdb::FDBContext> fdbContext;
|
||||
std::shared_ptr<kv::IKVEngine> kvEngine;
|
||||
std::shared_ptr<MgmtdClientForAdmin> mgmtdClient;
|
||||
std::shared_ptr<MetaClient> metaClient;
|
||||
std::shared_ptr<StorageClient> storageClient;
|
||||
std::shared_ptr<CoreClient> coreClient;
|
||||
auto clientId = ClientId::random();
|
||||
|
||||
auto ensureIbInited = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
auto ibResult = hf3fs::net::IBManager::start(config.ib_devices());
|
||||
XLOGF_IF(FATAL, !ibResult, "Failed to start IBManager: {}.", ibResult.error().describe());
|
||||
return true;
|
||||
}();
|
||||
};
|
||||
SCOPE_EXIT { hf3fs::net::IBManager::stop(); };
|
||||
|
||||
auto monitorResult = hf3fs::monitor::Monitor::start(config.monitor());
|
||||
XLOGF_IF(CRITICAL, !monitorResult, "Start monitor failed: {}", monitorResult.error().describe());
|
||||
SCOPE_EXIT { hf3fs::monitor::Monitor::stop(); };
|
||||
|
||||
auto ensureClient = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
ensureIbInited();
|
||||
client = std::make_shared<net::Client>(config.client());
|
||||
client->start();
|
||||
return true;
|
||||
}();
|
||||
};
|
||||
|
||||
auto ensureMgmtdClient = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
ensureClient();
|
||||
mgmtdClient =
|
||||
std::make_shared<MgmtdClientForAdmin>(config.cluster_id(),
|
||||
std::make_unique<RealStubFactory<mgmtd::MgmtdServiceStub>>(
|
||||
[&client](net::Address addr) { return client->serdeCtx(addr); }),
|
||||
config.mgmtd_client());
|
||||
|
||||
folly::coro::blockingWait(mgmtdClient->start(&client->tpg().bgThreadPool().randomPick()));
|
||||
folly::coro::blockingWait(mgmtdClient->refreshRoutingInfo(/*force=*/false));
|
||||
return true;
|
||||
}();
|
||||
};
|
||||
|
||||
AdminEnv env;
|
||||
env.userInfo = generateUserInfo(config.user_info());
|
||||
env.kvEngineGetter = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
fdbContext = kv::fdb::FDBContext::create(config.fdb());
|
||||
kvEngine = std::make_unique<kv::FDBKVEngine>(fdbContext->getDB());
|
||||
return true;
|
||||
}();
|
||||
return kvEngine;
|
||||
};
|
||||
|
||||
env.clientGetter = [&] {
|
||||
ensureClient();
|
||||
return client;
|
||||
};
|
||||
|
||||
env.mgmtdClientGetter = [&] {
|
||||
ensureMgmtdClient();
|
||||
if (auto ri = mgmtdClient->getRoutingInfo(); !ri || !ri->raw())
|
||||
throw StatusException(Status(MgmtdClientCode::kRoutingInfoNotReady));
|
||||
return mgmtdClient;
|
||||
};
|
||||
|
||||
env.unsafeMgmtdClientGetter = [&] {
|
||||
ensureMgmtdClient();
|
||||
return mgmtdClient;
|
||||
};
|
||||
|
||||
env.storageClientGetter = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
ensureMgmtdClient();
|
||||
|
||||
storageClient = StorageClient::create(clientId, config.storage_client(), *mgmtdClient);
|
||||
return true;
|
||||
}();
|
||||
if (auto ri = mgmtdClient->getRoutingInfo(); !ri || !ri->raw())
|
||||
throw StatusException(Status(MgmtdClientCode::kRoutingInfoNotReady));
|
||||
return storageClient;
|
||||
};
|
||||
|
||||
env.metaClientGetter = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
ensureMgmtdClient();
|
||||
|
||||
metaClient = std::make_shared<MetaClient>(
|
||||
clientId,
|
||||
config.meta_client(),
|
||||
std::make_unique<MetaClient::StubFactory>([&client](net::Address addr) { return client->serdeCtx(addr); }),
|
||||
mgmtdClient,
|
||||
env.storageClientGetter(),
|
||||
false);
|
||||
return true;
|
||||
}();
|
||||
if (auto ri = mgmtdClient->getRoutingInfo(); !ri || !ri->raw())
|
||||
throw StatusException(Status(MgmtdClientCode::kRoutingInfoNotReady));
|
||||
return metaClient;
|
||||
};
|
||||
|
||||
env.coreClientGetter = [&] {
|
||||
[[maybe_unused]] static bool inited = [&] {
|
||||
ensureClient();
|
||||
coreClient = std::make_shared<CoreClient>(std::make_unique<RealStubFactory<core::CoreServiceStub>>(
|
||||
[&client](net::Address addr) { return client->serdeCtx(addr); }));
|
||||
return true;
|
||||
}();
|
||||
return coreClient;
|
||||
};
|
||||
|
||||
std::string cmd = argc > 1 ? fmt::format("{}", fmt::join(&argv[1], &argv[argc], " ")) : "";
|
||||
XLOGF(INFO, "cli start cmd = \"{}\" verbose = {} profile = {}", cmd, config.verbose(), config.profile());
|
||||
|
||||
Dispatcher dispatcher;
|
||||
|
||||
auto result = folly::coro::blockingWait([&]() -> CoTryTask<void> {
|
||||
auto res = co_await registerAdminCommands(dispatcher);
|
||||
if (res.hasError()) {
|
||||
fmt::print("Register commands failed: {}", res.error());
|
||||
CO_RETURN_ERROR(res);
|
||||
}
|
||||
co_return co_await dispatcher.run(
|
||||
env,
|
||||
[&env] { return env.currentDir; },
|
||||
cmd,
|
||||
config.verbose(),
|
||||
config.profile(),
|
||||
config.break_multi_line_command_on_failure());
|
||||
}());
|
||||
|
||||
if (mgmtdClient) {
|
||||
folly::coro::blockingWait(mgmtdClient->stop());
|
||||
}
|
||||
|
||||
if (client) {
|
||||
client->stopAndJoin();
|
||||
}
|
||||
|
||||
return result.hasError();
|
||||
}
|
||||
2
src/client/cli/CMakeLists.txt
Normal file
2
src/client/cli/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(admin)
|
||||
25
src/client/cli/admin/AdminEnv.h
Normal file
25
src/client/cli/admin/AdminEnv.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "client/cli/common/IEnv.h"
|
||||
#include "client/core/CoreClient.h"
|
||||
#include "client/meta/MetaClient.h"
|
||||
#include "client/mgmtd/IMgmtdClientForAdmin.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "common/kv/IKVEngine.h"
|
||||
#include "fbs/core/user/User.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
struct AdminEnv : IEnv {
|
||||
flat::UserInfo userInfo{flat::Uid{0}, flat::Gid{0}, ""};
|
||||
meta::InodeId currentDirId = meta::InodeId::root();
|
||||
String currentDir = "/";
|
||||
|
||||
std::function<std::shared_ptr<net::Client>()> clientGetter;
|
||||
std::function<std::shared_ptr<meta::client::MetaClient>()> metaClientGetter;
|
||||
std::function<std::shared_ptr<kv::IKVEngine>()> kvEngineGetter;
|
||||
std::function<std::shared_ptr<IMgmtdClientForAdmin>()> mgmtdClientGetter;
|
||||
std::function<std::shared_ptr<IMgmtdClientForAdmin>()> unsafeMgmtdClientGetter;
|
||||
std::function<std::shared_ptr<storage::client::StorageClient>()> storageClientGetter;
|
||||
std::function<std::shared_ptr<CoreClient>()> coreClientGetter;
|
||||
};
|
||||
} // namespace hf3fs::client::cli
|
||||
402
src/client/cli/admin/AdminUserCtrl.cc
Normal file
402
src/client/cli/admin/AdminUserCtrl.cc
Normal file
@@ -0,0 +1,402 @@
|
||||
#include "AdminUserCtrl.h"
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
#include <string>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/kv/WithTransaction.h"
|
||||
#include "common/utils/ArgParse.h"
|
||||
#include "common/utils/OptionalUtils.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
#include "common/utils/Transform.h"
|
||||
#include "common/utils/Uuid.h"
|
||||
#include "core/user/UserStore.h"
|
||||
#include "core/user/UserToken.h"
|
||||
#include "fdb/FDBRetryStrategy.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
using flat::Gid;
|
||||
using flat::Uid;
|
||||
|
||||
kv::FDBRetryStrategy getRetryStrategy() { return kv::FDBRetryStrategy({1_s, 10, false}); }
|
||||
|
||||
CoTryTask<void> ensureAdmin([[maybe_unused]] kv::IReadOnlyTransaction &txn,
|
||||
[[maybe_unused]] core::UserStore &store,
|
||||
[[maybe_unused]] std::string_view token,
|
||||
[[maybe_unused]] std::optional<flat::Uid> self = std::nullopt) {
|
||||
auto uid = core::decodeUidFromUserToken(token);
|
||||
if (UNLIKELY(uid.hasError())) {
|
||||
co_return makeError(StatusCode::kAuthenticationFail, "Token decode failed");
|
||||
}
|
||||
auto user = co_await store.getUser(txn, *uid);
|
||||
CO_RETURN_ON_ERROR(user);
|
||||
if (!user->has_value()) {
|
||||
co_return makeError(StatusCode::kAuthenticationFail, fmt::format("Uid {} not found", uid->toUnderType()));
|
||||
}
|
||||
if (auto res = user->value().validateToken(token, UtcClock::now()); res.hasError()) {
|
||||
XLOGF(ERR, "Token validate failed: {}, uid {}", res.error(), *uid);
|
||||
co_return makeError(StatusCode::kAuthenticationFail, "Token validate failed");
|
||||
}
|
||||
if (*uid != 0 && !user->value().admin && (!self || *self != *uid)) {
|
||||
co_return makeError(StatusCode::kAuthenticationFail, "Not Admin");
|
||||
}
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
void printUserAttr(Dispatcher::OutputTable &table, flat::Uid uid, const flat::UserAttr &attr) {
|
||||
table.push_back({"Uid", std::to_string(uid)});
|
||||
table.push_back({"Name", attr.name});
|
||||
table.push_back({"Token", fmt::format("{}(Expired at N/A)", attr.token)});
|
||||
for (auto it = attr.tokens.rbegin(); it != attr.tokens.rend(); ++it) {
|
||||
const auto &sa = *it;
|
||||
table.push_back({"Token", fmt::format("{}(Expired at {})", sa.token, sa.endTime.YmdHMS())});
|
||||
}
|
||||
table.push_back({"IsRootUser", attr.root ? "true" : "false"});
|
||||
table.push_back({"IsAdmin", attr.admin ? "true" : "false"});
|
||||
table.push_back({"Gid", std::to_string(attr.gid)});
|
||||
auto groups = fmt::format("{}", fmt::join(attr.groups, ", "));
|
||||
table.push_back({"SupplementaryGids", groups});
|
||||
}
|
||||
|
||||
struct UserAddHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-add");
|
||||
cmd.add_argument("uid").scan<'u', uint32_t>();
|
||||
cmd.add_argument("name");
|
||||
cmd.add_argument("--root").help("is root or not").default_value(false).implicit_value(true);
|
||||
cmd.add_argument("--admin").help("is admin or not").default_value(false).implicit_value(true);
|
||||
cmd.add_argument("--groups").help("group ids separated by comma (',')");
|
||||
cmd.add_argument("--token");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto uid = Uid(parser.get<uint32_t>("uid"));
|
||||
auto name = parser.get<std::string>("name");
|
||||
auto isRootUser = parser.get<bool>("--root");
|
||||
auto isAdmin = parser.get<bool>("--admin");
|
||||
auto token = parser.present<String>("--token").value_or("");
|
||||
auto gids = splitAndTransform(parser.present<String>("--groups").value_or(""), boost::is_any_of(","), [](auto s) {
|
||||
return flat::Gid(folly::to<uint32_t>(s));
|
||||
});
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<flat::UserAttr> {
|
||||
auto userList = co_await store.listUsers(txn);
|
||||
CO_RETURN_ON_ERROR(userList);
|
||||
if (userList->empty()) {
|
||||
if (!isAdmin) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "The first user must be admin");
|
||||
}
|
||||
} else {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token));
|
||||
}
|
||||
co_return co_await store.addUser(txn, uid, name, gids, isRootUser, isAdmin, token);
|
||||
};
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(commitRes);
|
||||
printUserAttr(output, uid, *commitRes);
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct UserRemoveHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-remove");
|
||||
cmd.add_argument("uid").scan<'u', uint32_t>();
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto uid = Uid(parser.get<uint32_t>("uid"));
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<void> {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token));
|
||||
co_return co_await store.removeUser(txn, uid);
|
||||
};
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(commitRes);
|
||||
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct UserSetHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-set");
|
||||
cmd.add_argument("uid").scan<'u', uint32_t>();
|
||||
cmd.add_argument("--name").help("name of the new user");
|
||||
cmd.add_argument("--root").scan<'i', int32_t>().help("1: set; 0: unset");
|
||||
cmd.add_argument("--admin").scan<'i', int32_t>().help("1: set; 0: unset");
|
||||
cmd.add_argument("--groups").help("group ids separated by comma (',')");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto uid = Uid(parser.get<uint32_t>("uid"));
|
||||
auto name = parser.present("--name");
|
||||
auto isRootValue = parser.present<int32_t>("--root");
|
||||
auto isAdminValue = parser.present<int32_t>("--admin");
|
||||
auto groups = splitAndTransform(parser.present<String>("--groups").value_or(""), boost::is_any_of(","), [](auto s) {
|
||||
return flat::Gid(folly::to<uint32_t>(s));
|
||||
});
|
||||
|
||||
if (isRootValue) {
|
||||
ENSURE_USAGE(*isRootValue == 0 || *isRootValue == 1, "-r should be 0 or 1");
|
||||
}
|
||||
if (isAdminValue) {
|
||||
ENSURE_USAGE(*isAdminValue == 0 || *isAdminValue == 1, "--admin should be 0 or 1");
|
||||
}
|
||||
|
||||
auto toBool = [](auto v) -> bool { return v; };
|
||||
|
||||
std::optional<bool> isRoot = optionalMap(isRootValue, toBool);
|
||||
std::optional<bool> isAdmin = optionalMap(isAdminValue, toBool);
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<meta::UserAttr> {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token));
|
||||
co_return co_await store.setUserAttr(txn,
|
||||
uid,
|
||||
name ? std::string_view(*name) : std::string_view{},
|
||||
groups.empty() ? nullptr : &groups,
|
||||
isRoot,
|
||||
isAdmin);
|
||||
};
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(commitRes);
|
||||
printUserAttr(output, uid, *commitRes);
|
||||
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct UserSetTokenHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-set-token");
|
||||
cmd.add_argument("uid").scan<'u', uint32_t>();
|
||||
cmd.add_argument("--new").help("generate new token").default_value(false).implicit_value(true);
|
||||
cmd.add_argument("--token");
|
||||
cmd.add_argument("--invalidate-existed-tokens").default_value(false).implicit_value(true);
|
||||
cmd.add_argument("--max-active-tokens").scan<'u', uint32_t>().default_value(5U);
|
||||
cmd.add_argument("--last-token-lifetime-days").scan<'u', uint32_t>().default_value(7U);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto uid = Uid(parser.get<uint32_t>("uid"));
|
||||
auto token = parser.present<String>("--token").value_or("");
|
||||
auto generateNewToken = parser.get<bool>("--new");
|
||||
auto invalidateExistedToken = parser.get<bool>("--invalidate-existed-tokens");
|
||||
auto maxActiveTokens = parser.get<uint32_t>("--max-active-tokens");
|
||||
auto tokenLifetimeExtendLength = Duration(std::chrono::days(parser.get<uint32_t>("--last-token-lifetime-days")));
|
||||
|
||||
ENSURE_USAGE(!token.empty() + generateNewToken == 1, "must set one of token and generate-token");
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<meta::UserAttr> {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token));
|
||||
co_return co_await store
|
||||
.setUserToken(txn, uid, token, invalidateExistedToken, maxActiveTokens, tokenLifetimeExtendLength);
|
||||
};
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(commitRes);
|
||||
printUserAttr(output, uid, *commitRes);
|
||||
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct UserStatHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-stat");
|
||||
cmd.add_argument("uid").scan<'u', uint32_t>();
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto uid = Uid(parser.get<uint32_t>("uid"));
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadOnlyTransaction &txn) -> CoTryTask<meta::UserAttr> {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token, uid));
|
||||
auto fetchResult = co_await store.getUser(txn, uid);
|
||||
CO_RETURN_ON_ERROR(fetchResult);
|
||||
if (!fetchResult->has_value()) {
|
||||
co_return makeError(MetaCode::kNotFound, fmt::format("Uid {} not found", uid.toUnderType()));
|
||||
}
|
||||
co_return **fetchResult;
|
||||
};
|
||||
auto res = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadonlyTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
printUserAttr(output, uid, *res);
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct UserListHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-list");
|
||||
cmd.add_argument("--json").default_value(false).implicit_value(true);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
core::UserStore store;
|
||||
auto handler = [&](kv::IReadOnlyTransaction &txn) -> CoTryTask<std::vector<flat::UserAttr>> {
|
||||
CO_RETURN_ON_ERROR(co_await ensureAdmin(txn, store, env.userInfo.token));
|
||||
co_return co_await store.listUsers(txn);
|
||||
};
|
||||
auto res = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadonlyTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
if (parser.get<bool>("--json")) {
|
||||
struct UserAttrLite {
|
||||
SERDE_STRUCT_FIELD(name, std::string());
|
||||
SERDE_STRUCT_FIELD(gid, flat::Gid());
|
||||
SERDE_STRUCT_FIELD(groups, std::vector<flat::Gid>());
|
||||
SERDE_STRUCT_FIELD(uid, flat::Uid());
|
||||
};
|
||||
|
||||
auto users = transformTo<std::vector>(std::span(res->begin(), res->end()), [](const flat::UserAttr &attr) {
|
||||
return UserAttrLite{
|
||||
.name = attr.name,
|
||||
.gid = attr.gid,
|
||||
.groups = attr.groups,
|
||||
.uid = attr.uid,
|
||||
};
|
||||
});
|
||||
output.push_back({serde::toJsonString(users, false, true)});
|
||||
} else {
|
||||
output.push_back({"Uid", "Name", "Token", "IsRoot", "IsAdmin", "Gids"});
|
||||
for (const auto &attr : *res) {
|
||||
std::vector<String> row;
|
||||
row.push_back(std::to_string(attr.uid));
|
||||
row.push_back(attr.name);
|
||||
row.push_back(attr.token);
|
||||
row.push_back(attr.root ? "true" : "false");
|
||||
row.push_back(attr.admin ? "true" : "false");
|
||||
row.push_back(fmt::format("{}", fmt::join(attr.groups, ", ")));
|
||||
output.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
void printUserInfo(Dispatcher::OutputTable &output, const flat::UserInfo &userInfo) {
|
||||
output.push_back({"Uid", std::to_string(userInfo.uid)});
|
||||
output.push_back({"Gid", std::to_string(userInfo.gid)});
|
||||
|
||||
auto groups = transformTo<std::vector>(std::span{userInfo.groups.begin(), userInfo.groups.size()},
|
||||
[](flat::Gid gid) { return gid.toUnderType(); });
|
||||
output.push_back({"Groups", fmt::format("[{}]", fmt::join(groups, ", "))});
|
||||
output.push_back({"Token", userInfo.token});
|
||||
}
|
||||
|
||||
struct UserSwitchHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("user-switch");
|
||||
cmd.add_argument("-u", "--uid").help("-1 means geteuid").scan<'i', int64_t>();
|
||||
cmd.add_argument("-g", "--gid").help("-1 means getegid").scan<'i', int64_t>();
|
||||
cmd.add_argument("-t", "--token");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
auto u = parser.present<int64_t>("-u");
|
||||
auto g = parser.present<int64_t>("-g");
|
||||
auto t = parser.present<String>("-t");
|
||||
|
||||
if (u) env.userInfo.uid = flat::Uid(*u == -1 ? geteuid() : *u);
|
||||
if (g) env.userInfo.gid = flat::Gid(*g == -1 ? getegid() : *g);
|
||||
if (t) env.userInfo.token = *t;
|
||||
|
||||
printUserInfo(output, env.userInfo);
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
|
||||
struct CurrentUserHandler {
|
||||
static auto getParser() {
|
||||
argparse::ArgumentParser cmd("current-user");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
static CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable output;
|
||||
|
||||
printUserInfo(output, env.userInfo);
|
||||
co_return output;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerAdminUserCtrlHandler(Dispatcher &dispatcher) {
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserAddHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserRemoveHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserSetHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserSetTokenHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserStatHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserListHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<UserSwitchHandler>());
|
||||
CO_RETURN_ON_ERROR(co_await dispatcher.registerHandler<CurrentUserHandler>());
|
||||
co_return Void{};
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/AdminUserCtrl.h
Normal file
8
src/client/cli/admin/AdminUserCtrl.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerAdminUserCtrlHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
137
src/client/cli/admin/Bench.cc
Normal file
137
src/client/cli/admin/Bench.cc
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "client/cli/admin/Bench.h"
|
||||
|
||||
#include <boost/iostreams/device/array.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/GlobalExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "client/storage/TargetSelection.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("bench");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("--rank").default_value(uint32_t{0}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--timeout").default_value(uint32_t{1}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--coroutines").default_value(uint32_t{1}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--seconds").default_value(uint32_t{60}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--remove").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
Result<Void> check(const Path &src, uint32_t expected, storage::ChecksumInfo checksum) {
|
||||
if (checksum.value != expected) {
|
||||
auto msg = fmt::format("check {} failed {:08x} != {:08x}", src, checksum.value, expected);
|
||||
XLOG(ERR, msg);
|
||||
return makeError(StorageClientCode::kChecksumMismatch, std::move(msg));
|
||||
}
|
||||
return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<Void> writeFileAndCheck(AdminEnv &env,
|
||||
const Path &path,
|
||||
std::string_view str,
|
||||
uint32_t repeat,
|
||||
Duration timeout,
|
||||
uint32_t crc32c) {
|
||||
auto openResult = co_await FileWrapper::openOrCreateFile(env, path, true);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(openResult);
|
||||
auto &file = *openResult;
|
||||
|
||||
boost::iostreams::stream<boost::iostreams::basic_array_source<char>> stream(str.begin(), str.size());
|
||||
auto writeResult = co_await file.writeFile(stream, str.size(), 0, 10_s);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(writeResult);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(check(path, crc32c, *writeResult));
|
||||
|
||||
std::ofstream out("/dev/null");
|
||||
auto readResult =
|
||||
co_await file.readFile(out, str.size(), 0, true, false, storage::client::TargetSelectionMode::HeadTarget);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(check(path, crc32c, *readResult));
|
||||
|
||||
readResult =
|
||||
co_await file.readFile(out, str.size(), 0, true, false, storage::client::TargetSelectionMode::TailTarget);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(check(path, crc32c, *readResult));
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleWriteFile(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
Path src = parser.get<std::string>("path");
|
||||
auto rank = parser.get<uint32_t>("--rank");
|
||||
auto timeout = 1_s * parser.get<uint32_t>("--timeout");
|
||||
auto coroutines = parser.get<uint32_t>("--coroutines");
|
||||
auto deadline = RelativeTime::now() + 1_s * parser.get<uint32_t>("--seconds");
|
||||
auto remove = parser.get<bool>("--remove");
|
||||
|
||||
// 2. prepare datas.
|
||||
std::vector<std::string> datas;
|
||||
std::vector<uint32_t> crc32cList;
|
||||
for (auto i = 0; i < 16; ++i) {
|
||||
datas.emplace_back(std::string(128_MB, 'A' + i));
|
||||
auto checksum = storage::ChecksumInfo::create(storage::ChecksumType::CRC32C,
|
||||
reinterpret_cast<const uint8_t *>(datas.back().data()),
|
||||
datas.back().length());
|
||||
crc32cList.emplace_back(checksum.value);
|
||||
}
|
||||
|
||||
// 3. write and check.
|
||||
std::vector<CoTryTask<Void>> total;
|
||||
total.reserve(coroutines);
|
||||
for (auto i = 0u; i < coroutines; ++i) {
|
||||
total.push_back(folly::coro::co_invoke([&, i]() -> CoTryTask<Void> {
|
||||
auto path = src / fmt::format("{:03}.{:03}", rank, i);
|
||||
while (RelativeTime::now() <= deadline) {
|
||||
auto idx = folly::Random::rand32() % datas.size();
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await writeFileAndCheck(env, path, datas[idx], 1, timeout, crc32cList[idx])
|
||||
.scheduleOn(folly::getGlobalCPUExecutor()));
|
||||
if (remove) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(
|
||||
co_await env.metaClientGetter()->remove(env.userInfo, env.currentDirId, path, false));
|
||||
}
|
||||
}
|
||||
co_return Void{};
|
||||
}));
|
||||
}
|
||||
|
||||
auto results = co_await folly::coro::collectAllRange(std::move(total));
|
||||
for (auto result : results) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerBenchHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleWriteFile);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/Bench.h
Normal file
10
src/client/cli/admin/Bench.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerBenchHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
1
src/client/cli/admin/CMakeLists.txt
Normal file
1
src/client/cli/admin/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
target_add_lib(admin-cli mgmtd meta common-cli mgmtd-client meta-client storage-client core-client kv apache_arrow_static chunk_engine)
|
||||
75
src/client/cli/admin/Chdir.cc
Normal file
75
src/client/cli/admin/Chdir.cc
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "Chdir.h"
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "scn/tuple_return/tuple_return.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("cd");
|
||||
parser.add_argument("-L").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--inode").default_value(false).implicit_value(true);
|
||||
parser.add_argument("path");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleChdir(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
// TODO: now cd can't correctly handle many cases e.g.:
|
||||
// - path contains symlink.
|
||||
// - and so on.
|
||||
Dispatcher::OutputTable table;
|
||||
auto pathstr = parser.get<std::string>("path");
|
||||
Path path(pathstr);
|
||||
auto inodeid = env.currentDirId;
|
||||
auto metaClient = env.metaClientGetter();
|
||||
bool followLastSymlink = parser.get<bool>("-L");
|
||||
bool byinode = parser.get<bool>("--inode");
|
||||
|
||||
Result<meta::Inode> res = makeError(MetaCode::kFoundBug);
|
||||
if (!byinode) {
|
||||
inodeid = meta::InodeId::root();
|
||||
if (!path.is_absolute()) {
|
||||
inodeid = env.currentDirId;
|
||||
}
|
||||
res = co_await metaClient->stat(env.userInfo, inodeid, path, true);
|
||||
} else {
|
||||
auto [r, v] = scn::scan_tuple<uint64_t>(pathstr, "{:i}");
|
||||
if (!r) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "invalid inodeid {}", path);
|
||||
}
|
||||
inodeid = meta::InodeId(v);
|
||||
path = ".";
|
||||
res = co_await metaClient->stat(env.userInfo, inodeid, std::nullopt, true);
|
||||
}
|
||||
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
const auto &inode = res.value();
|
||||
if (inode.getType() != meta::InodeType::Directory) {
|
||||
co_return makeError(MetaCode::kNotDirectory);
|
||||
}
|
||||
|
||||
if (followLastSymlink || byinode) {
|
||||
auto getRealPathRes = co_await metaClient->getRealPath(env.userInfo, inodeid, path, true);
|
||||
CO_RETURN_ON_ERROR(getRealPathRes);
|
||||
path = *getRealPathRes;
|
||||
}
|
||||
|
||||
env.currentDirId = inode.id;
|
||||
env.currentDir = path.native();
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerChdirHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleChdir);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/Chdir.h
Normal file
8
src/client/cli/admin/Chdir.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerChdirHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
169
src/client/cli/admin/Checksum.cc
Normal file
169
src/client/cli/admin/Checksum.cc
Normal file
@@ -0,0 +1,169 @@
|
||||
#include "Checksum.h"
|
||||
|
||||
#include <folly/executors/GlobalExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#define OPENSSL_SUPPRESS_DEPRECATED
|
||||
#include <openssl/md5.h>
|
||||
#include <string>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("checksum");
|
||||
parser.add_argument("path").nargs(argparse::nargs_pattern::at_least_one);
|
||||
parser.add_argument("--list").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--batch").default_value(uint32_t{8}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--md5").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--fillZero").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-o", "--output");
|
||||
return parser;
|
||||
}
|
||||
|
||||
struct ReadResult {
|
||||
Path path;
|
||||
Result<Void> result = makeError(Status::OK);
|
||||
storage::ChecksumInfo checksum;
|
||||
std::array<uint8_t, MD5_DIGEST_LENGTH> md5{};
|
||||
};
|
||||
|
||||
CoTryTask<Void> checksumFile(AdminEnv &env, const Path &path, bool fillZero, bool md5Enabled, ReadResult &result) {
|
||||
result.path = path;
|
||||
|
||||
auto openResult = co_await FileWrapper::openOrCreateFile(env, path, false);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(openResult);
|
||||
auto &file = *openResult;
|
||||
|
||||
MD5_CTX md5;
|
||||
int md5ret = MD5_Init(&md5);
|
||||
if (UNLIKELY(md5ret != 1)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("md5 init failed: {}", md5ret));
|
||||
}
|
||||
|
||||
auto replicasNum = file.replicasNum();
|
||||
CO_RETURN_AND_LOG_ON_ERROR(replicasNum);
|
||||
for (auto i = 0u; i < *replicasNum; ++i) {
|
||||
std::ofstream out("/dev/null");
|
||||
auto readResult = co_await file.readFile(out,
|
||||
file.length(),
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
storage::client::TargetSelectionMode::ManualMode,
|
||||
i == 0 && md5Enabled ? &md5 : nullptr,
|
||||
fillZero,
|
||||
false,
|
||||
0);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
if (i == 0) {
|
||||
result.checksum = *readResult;
|
||||
} else if (result.checksum != *readResult) {
|
||||
co_return makeError(
|
||||
StorageCode::kChecksumMismatch,
|
||||
fmt::format("file checksum mismatch, path {}, head {}, now:{} {}", path, result.checksum, i, *readResult));
|
||||
}
|
||||
}
|
||||
|
||||
md5ret = MD5_Final(result.md5.data(), &md5);
|
||||
if (UNLIKELY(md5ret != 1)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("md5 final failed: {}", md5ret));
|
||||
}
|
||||
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
CoTask<void> callChecksumFile(AdminEnv &env, const Path &path, bool fillZero, bool md5Enabled, ReadResult &result) {
|
||||
result.result =
|
||||
co_await checksumFile(env, path, fillZero, md5Enabled, result).scheduleOn(folly::getGlobalCPUExecutor());
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleChecksum(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
auto paths = parser.get<std::vector<std::string>>("path");
|
||||
auto list = parser.get<bool>("--list");
|
||||
auto batchSize = parser.get<uint32_t>("--batch");
|
||||
auto md5Enabled = parser.get<bool>("--md5");
|
||||
auto fillZero = parser.get<bool>("--fillZero");
|
||||
auto output = parser.present<std::string>("--output");
|
||||
|
||||
// 2. prepare file list.
|
||||
std::vector<Path> fileList;
|
||||
for (auto &src : paths) {
|
||||
if (list) {
|
||||
std::ifstream listFile(src);
|
||||
if (!listFile) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("open list file failed, {}", src));
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(listFile, line)) {
|
||||
if (!line.empty()) {
|
||||
fileList.push_back(Path{line});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileList.push_back(src);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. read file.
|
||||
std::ofstream outputFile;
|
||||
if (output.has_value()) {
|
||||
outputFile = std::ofstream{output.value()};
|
||||
if (!outputFile) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("create output file {} failed", output.value()));
|
||||
}
|
||||
}
|
||||
std::ostream &out = output.has_value() ? outputFile : std::cout;
|
||||
|
||||
std::vector<ReadResult> readResults;
|
||||
readResults.reserve(batchSize);
|
||||
std::vector<CoTask<void>> batch;
|
||||
for (auto &filePath : fileList) {
|
||||
readResults.emplace_back();
|
||||
batch.push_back(callChecksumFile(env, filePath, fillZero, md5Enabled, readResults.back()));
|
||||
if (batch.size() >= batchSize || std::addressof(filePath) == std::addressof(fileList.back())) {
|
||||
co_await folly::coro::collectAllRange(std::move(batch));
|
||||
for (auto &result : readResults) {
|
||||
if (result.result.hasValue()) {
|
||||
if (md5Enabled) {
|
||||
out << fmt::format("{} {:02x}\n", result.path, fmt::join(result.md5, ""));
|
||||
} else {
|
||||
out << fmt::format("{} {:08X}\n", result.path, result.checksum.value);
|
||||
}
|
||||
} else {
|
||||
out << fmt::format("{} {}\n", result.path, result.result.error());
|
||||
}
|
||||
}
|
||||
readResults.clear();
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerChecksumHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleChecksum);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/Checksum.h
Normal file
10
src/client/cli/admin/Checksum.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerChecksumHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
51
src/client/cli/admin/Create.cc
Normal file
51
src/client/cli/admin/Create.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "Create.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/mgmtd/ChainRef.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("create");
|
||||
parser.add_argument("-p", "--perm");
|
||||
parser.add_argument("path");
|
||||
addLayoutArguments(parser);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleCreate(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto path = parser.get<std::string>("path");
|
||||
auto layout = parseLayout(parser);
|
||||
auto p = parser.present<String>("-p");
|
||||
|
||||
meta::Permission perm(0644);
|
||||
if (p) {
|
||||
auto [r, v] = scn::scan_tuple<uint32_t>(*p, "{:o}");
|
||||
if (!r) co_return makeError(StatusCode::kInvalidArg, fmt::format("invalid permission format: {}", r.error().msg()));
|
||||
perm = meta::Permission(v);
|
||||
}
|
||||
|
||||
auto res = co_await env.metaClientGetter()
|
||||
->create(env.userInfo, env.currentDirId, Path(path), std::nullopt, perm, 0, layout);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerCreateHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleCreate);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/Create.h
Normal file
8
src/client/cli/admin/Create.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerCreateHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
81
src/client/cli/admin/CreateRange.cc
Normal file
81
src/client/cli/admin/CreateRange.cc
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "CreateRange.h"
|
||||
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("create-range");
|
||||
parser.add_argument("prefix");
|
||||
parser.add_argument("inclusive_start").scan<'i', int64_t>();
|
||||
parser.add_argument("exclusive_end").scan<'i', int64_t>();
|
||||
parser.add_argument("-c", "--concurrency").default_value(1).scan<'i', int>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTask<Dispatcher::OutputTable> handleCreateSubRange(AdminEnv &env, const String &prefix, int64_t start, int64_t n) {
|
||||
Dispatcher::OutputTable table;
|
||||
for (auto i = 0; i < n; ++i) {
|
||||
auto path = fmt::format("{}{}", prefix, start + i);
|
||||
auto res = co_await env.metaClientGetter()
|
||||
->create(env.userInfo, env.currentDirId, Path(path), std::nullopt, meta::Permission(0777), 0);
|
||||
if (res.hasError()) {
|
||||
table.push_back({fmt::format("Error at {}", start + i), res.error().describe()});
|
||||
}
|
||||
}
|
||||
co_return std::move(table);
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleCreateRange(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
|
||||
ENSURE_USAGE(args.empty());
|
||||
|
||||
auto prefix = parser.get<std::string>("prefix");
|
||||
auto inclusiveStart = parser.get<int64_t>("inclusive_start");
|
||||
auto exclusiveEnd = parser.get<int64_t>("exclusive_end");
|
||||
auto concurrency = parser.get<int>("-c");
|
||||
|
||||
auto total = exclusiveEnd - inclusiveStart;
|
||||
auto every = total / concurrency;
|
||||
auto remain = total % concurrency;
|
||||
|
||||
std::vector<CoTask<Dispatcher::OutputTable>> tasks;
|
||||
auto start = inclusiveStart;
|
||||
for (auto i = 0; i < remain; ++i) {
|
||||
tasks.push_back(handleCreateSubRange(env, prefix, start, every + 1));
|
||||
start += every + 1;
|
||||
}
|
||||
for (auto i = remain; i < concurrency; ++i) {
|
||||
tasks.push_back(handleCreateSubRange(env, prefix, start, every));
|
||||
start += every;
|
||||
}
|
||||
auto res = co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
|
||||
auto succeeded = total;
|
||||
Dispatcher::OutputTable table;
|
||||
for (auto &t : res) {
|
||||
for (auto &r : t) {
|
||||
table.push_back(std::move(r));
|
||||
}
|
||||
succeeded -= t.size();
|
||||
}
|
||||
table.push_back({"Succeeded", std::to_string(succeeded)});
|
||||
table.push_back({"Failed", std::to_string(total - succeeded)});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerCreateRangeHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleCreateRange);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/CreateRange.h
Normal file
8
src/client/cli/admin/CreateRange.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerCreateRangeHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
83
src/client/cli/admin/CreateTarget.cc
Normal file
83
src/client/cli/admin/CreateTarget.cc
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "CreateTarget.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/RapidCsv.h"
|
||||
#include "common/utils/Result.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("create-target");
|
||||
parser.add_argument("--node-id").scan<'u', flat::NodeId::UnderlyingType>().required();
|
||||
parser.add_argument("--disk-index").scan<'u', uint32_t>().required();
|
||||
parser.add_argument("--target-id").scan<'u', flat::TargetId::UnderlyingType>().required();
|
||||
parser.add_argument("--chain-id").scan<'u', flat::ChainId::UnderlyingType>().required();
|
||||
parser.add_argument("--add-chunk-size").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--chunk-size").nargs(argparse::nargs_pattern::any);
|
||||
parser.add_argument("--use-new-chunk-engine").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleCreateTarget(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = flat::NodeId(parser.get<flat::NodeId::UnderlyingType>("--node-id"));
|
||||
|
||||
storage::CreateTargetReq req;
|
||||
req.targetId = flat::TargetId(parser.get<flat::TargetId::UnderlyingType>("--target-id"));
|
||||
req.chainId = flat::ChainId(parser.get<flat::ChainId::UnderlyingType>("--chain-id"));
|
||||
req.diskIndex = parser.get<uint32_t>("--disk-index");
|
||||
req.addChunkSize = parser.get<bool>("--add-chunk-size");
|
||||
req.onlyChunkEngine = parser.get<bool>("--use-new-chunk-engine");
|
||||
std::vector<Size> chunkSizeList;
|
||||
for (const auto &size : parser.get<std::vector<std::string>>("--chunk-size")) {
|
||||
auto sizeResult = Size::from(size);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(sizeResult);
|
||||
chunkSizeList.push_back(*sizeResult);
|
||||
}
|
||||
if (!chunkSizeList.empty()) {
|
||||
req.chunkSizeList = std::move(chunkSizeList);
|
||||
}
|
||||
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
if (routingInfo == nullptr || routingInfo->raw() == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
auto chainInfo = routingInfo->getChain(req.chainId);
|
||||
if (chainInfo && chainInfo->targets.size() > 1) {
|
||||
// chain info exists and the number of replicas is greater than one.
|
||||
for (auto target : chainInfo->targets) {
|
||||
if (target.targetId == req.targetId && target.publicState == flat::PublicTargetState::LASTSRV) {
|
||||
co_return makeError(StorageClientCode::kRoutingError,
|
||||
fmt::format("creation of this target is prohibited {}", target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto client = env.storageClientGetter();
|
||||
auto res = co_await client->createTarget(nodeId, req);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({fmt::format("Create target {} on disk {} of {} succeeded",
|
||||
req.targetId.toUnderType(),
|
||||
req.diskIndex,
|
||||
nodeId.toUnderType())});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
CoTryTask<void> registerCreateTargetHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleCreateTarget);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/CreateTarget.h
Normal file
8
src/client/cli/admin/CreateTarget.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerCreateTargetHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
80
src/client/cli/admin/CreateTargets.cc
Normal file
80
src/client/cli/admin/CreateTargets.cc
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "CreateTargets.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/RapidCsv.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("create-targets");
|
||||
parser.add_argument("--node-id").scan<'u', flat::NodeId::UnderlyingType>().required();
|
||||
parser.add_argument("--disk-index").scan<'u', uint32_t>().nargs(argparse::nargs_pattern::at_least_one);
|
||||
parser.add_argument("--allow-existing-target").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--add-chunk-size").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--use-new-chunk-engine").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleCreateTarget(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = flat::NodeId(parser.get<flat::NodeId::UnderlyingType>("--node-id"));
|
||||
auto diskIndexes = parser.get<std::vector<uint32_t>>("--disk-index");
|
||||
auto diskIndexSet = std::set<uint32_t>(diskIndexes.begin(), diskIndexes.end());
|
||||
auto allowExistingTarget = parser.get<bool>("--allow-existing-target");
|
||||
auto addChunkSize = parser.get<bool>("--add-chunk-size");
|
||||
auto onlyChunkEngine = parser.get<bool>("--use-new-chunk-engine");
|
||||
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto rawRoutingInfo = routingInfo->raw();
|
||||
if (routingInfo == nullptr || rawRoutingInfo == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
for (auto &[targetId, targetInfo] : rawRoutingInfo->targets) {
|
||||
if (targetInfo.nodeId == nodeId && targetInfo.diskIndex && diskIndexSet.contains(*targetInfo.diskIndex)) {
|
||||
if (targetInfo.publicState == flat::PublicTargetState::LASTSRV) {
|
||||
auto chainInfo = routingInfo->getChain(targetInfo.chainId);
|
||||
if (chainInfo->targets.size() > 1) {
|
||||
std::cout << fmt::format("creation of this target is prohibited {}, skip\n", targetInfo);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
storage::CreateTargetReq req;
|
||||
req.targetId = targetId;
|
||||
req.chainId = targetInfo.chainId;
|
||||
req.diskIndex = *targetInfo.diskIndex;
|
||||
req.allowExistingTarget = allowExistingTarget;
|
||||
req.addChunkSize = addChunkSize;
|
||||
req.onlyChunkEngine = onlyChunkEngine;
|
||||
auto client = env.storageClientGetter();
|
||||
auto res = co_await client->createTarget(nodeId, req);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
std::cout << fmt::format("Create target {} on disk {} of {} succeeded\n",
|
||||
req.targetId.toUnderType(),
|
||||
req.diskIndex,
|
||||
nodeId.toUnderType());
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerCreateTargetsHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleCreateTarget);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/CreateTargets.h
Normal file
10
src/client/cli/admin/CreateTargets.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerCreateTargetsHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
36
src/client/cli/admin/DecodeUserToken.cc
Normal file
36
src/client/cli/admin/DecodeUserToken.cc
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "DecodeUserToken.h"
|
||||
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "core/user/UserToken.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("decode-user-token");
|
||||
parser.add_argument("token");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleDecodeUserToken(IEnv &,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
Dispatcher::OutputTable table;
|
||||
ENSURE_USAGE(args.empty());
|
||||
|
||||
auto token = parser.get<std::string>("token");
|
||||
auto res = core::decodeUserToken(token);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
auto [uid, ts] = *res;
|
||||
table.push_back({"Uid", fmt::format("{}", uid)});
|
||||
table.push_back({"Timestamp", fmt::format("{}", ts)});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerDecodeUserTokenHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleDecodeUserToken);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/DecodeUserToken.h
Normal file
8
src/client/cli/admin/DecodeUserToken.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDecodeUserTokenHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
77
src/client/cli/admin/DropUserCache.cc
Normal file
77
src/client/cli/admin/DropUserCache.cc
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "DropUserCache.h"
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
#include "stubs/MetaService/MetaServiceStub.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("drop-user-cache");
|
||||
parser.add_argument("-u", "--uid").scan<'u', uint32_t>();
|
||||
parser.add_argument("-a", "--all").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto u = parser.present<uint32_t>("-u");
|
||||
auto all = parser.get<bool>("-a");
|
||||
|
||||
std::optional<flat::Uid> uid;
|
||||
if (u) uid = flat::Uid(*u);
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
|
||||
auto req = meta::DropUserCacheReq(uid, all);
|
||||
|
||||
table.push_back({"NodeId", "Status", "Desc"});
|
||||
|
||||
auto client = env.clientGetter();
|
||||
for (const auto &[nid, ni] : routingInfo->nodes) {
|
||||
if (ni.type != flat::NodeType::META) continue;
|
||||
if (ni.status != flat::NodeStatus::HEARTBEAT_CONNECTED) {
|
||||
table.push_back({std::to_string(nid), "Skip", fmt::format("Status is {}", toStringView(ni.status))});
|
||||
continue;
|
||||
}
|
||||
auto addrs = ni.extractAddresses("MetaSerde");
|
||||
if (addrs.empty()) {
|
||||
table.push_back({std::to_string(nid), "Skip", "Address of MetaSerde not found"});
|
||||
continue;
|
||||
}
|
||||
auto result = co_await [&]() -> CoTask<std::vector<Status>> {
|
||||
std::vector<Status> rets;
|
||||
for (auto addr : addrs) {
|
||||
auto ctx = client->serdeCtx(addr);
|
||||
auto stub = meta::MetaServiceStub<serde::ClientContext>(ctx);
|
||||
auto ret = co_await stub.dropUserCache(req, {});
|
||||
if (!ret.hasError()) co_return std::vector<Status>{};
|
||||
rets.push_back(std::move(ret.error()));
|
||||
}
|
||||
co_return rets;
|
||||
}();
|
||||
|
||||
if (result.empty()) {
|
||||
table.push_back({std::to_string(nid), "Succeed", ""});
|
||||
} else {
|
||||
table.push_back({std::to_string(nid), "Failed", fmt::format("[{}]", fmt::join(result, ", "))});
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerDropUserCacheHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/DropUserCache.h
Normal file
8
src/client/cli/admin/DropUserCache.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDropUserCacheHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
53
src/client/cli/admin/DumpChainTable.cc
Normal file
53
src/client/cli/admin/DumpChainTable.cc
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "DumpChainTable.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-chain-table");
|
||||
parser.add_argument("tableId").scan<'u', uint32_t>();
|
||||
parser.add_argument("-v", "--version").scan<'u', uint32_t>();
|
||||
parser.add_argument("csv-file-path");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleDumpChainTable(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto tableId = flat::ChainTableId(parser.get<uint32_t>("tableId"));
|
||||
auto tableVer = flat::ChainTableVersion(parser.present<uint32_t>("-v").value_or(0));
|
||||
auto csvFilePath = parser.get<std::string>("csv-file-path");
|
||||
|
||||
std::ofstream of(csvFilePath);
|
||||
of.exceptions(std::ofstream::failbit | std::ofstream::badbit);
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
XLOGF(DBG, "DumpChainTable routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
auto *t = routingInfo->raw()->getChainTable(tableId, tableVer);
|
||||
if (!t) co_return makeError(StatusCode::kInvalidArg, fmt::format("{}@{} not found", tableId, tableVer));
|
||||
tableVer = t->chainTableVersion;
|
||||
|
||||
of << "ChainId\n";
|
||||
for (auto cid : t->chains) {
|
||||
of << cid.toUnderType() << "\n";
|
||||
}
|
||||
|
||||
table.push_back({fmt::format("Dump {} of {} to {} succeeded", tableId, tableVer, csvFilePath)});
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerDumpChainTableHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleDumpChainTable);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/DumpChainTable.h
Normal file
8
src/client/cli/admin/DumpChainTable.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpChainTableHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
59
src/client/cli/admin/DumpChains.cc
Normal file
59
src/client/cli/admin/DumpChains.cc
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "DumpChains.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-chains");
|
||||
parser.add_argument("csv-file-path-prefix");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleDumpChains(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto csvFilePathPrefix = parser.get<std::string>("csv-file-path-prefix");
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
XLOGF(DBG, "DumpChains routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
|
||||
robin_hood::unordered_map<size_t, std::vector<flat::ChainId>> chainsByReplicaCount;
|
||||
|
||||
for (const auto &[cid, ci] : routingInfo->raw()->chains) {
|
||||
chainsByReplicaCount[ci.targets.size()].push_back(cid);
|
||||
}
|
||||
|
||||
for (const auto &[rc, chains] : chainsByReplicaCount) {
|
||||
auto path = fmt::format("{}.{}", csvFilePathPrefix, rc);
|
||||
std::ofstream of(path);
|
||||
of.exceptions(std::ofstream::failbit | std::ofstream::badbit);
|
||||
of << "ChainId";
|
||||
for (size_t i = 0; i < rc; ++i) of << ",TargetId";
|
||||
of << "\n";
|
||||
for (auto cid : chains) {
|
||||
const auto &ci = routingInfo->raw()->chains[cid];
|
||||
of << cid.toUnderType();
|
||||
for (const auto &ti : ci.targets) of << "," << ti.targetId.toUnderType();
|
||||
of << "\n";
|
||||
}
|
||||
table.push_back({fmt::format("Dump {} chains of {} replicas to {} succeeded", chains.size(), rc, path)});
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerDumpChainsHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleDumpChains);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/DumpChains.h
Normal file
8
src/client/cli/admin/DumpChains.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpChainsHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
262
src/client/cli/admin/DumpChunkMeta.cc
Normal file
262
src/client/cli/admin/DumpChunkMeta.cc
Normal file
@@ -0,0 +1,262 @@
|
||||
#include "DumpChunkMeta.h"
|
||||
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <vector>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "analytics/SerdeObjectReader.h"
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/IEnv.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-chunkmeta");
|
||||
parser.add_argument("-c", "--chain-ids").nargs(argparse::nargs_pattern::any).scan<'u', uint32_t>();
|
||||
parser.add_argument("-m", "--chunkmeta-dir").default_value(std::string{"chunkmeta"});
|
||||
parser.add_argument("-q", "--parquet-format").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-h", "--only-head").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-p", "--parallel").default_value(uint32_t{32}).scan<'u', uint32_t>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> dumpChunkMeta(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
const auto &chainIds = parser.get<std::vector<uint32_t>>("chain-ids");
|
||||
const auto &chunkmetaDir = parser.get<std::string>("chunkmeta-dir");
|
||||
const auto parallel = std::min(parser.get<uint32_t>("parallel"), std::thread::hardware_concurrency() / 2);
|
||||
const auto &parquetFormat = parser.get<bool>("parquet-format");
|
||||
const auto &onlyHead = parser.get<bool>("only-head");
|
||||
|
||||
if (boost::filesystem::exists(chunkmetaDir)) {
|
||||
XLOGF(CRITICAL, "Output directory for chunk metadata already exists: {}", chunkmetaDir);
|
||||
co_return makeError(StatusCode::kInvalidArg);
|
||||
}
|
||||
|
||||
boost::system::error_code err{};
|
||||
boost::filesystem::create_directories(chunkmetaDir, err);
|
||||
|
||||
if (UNLIKELY(err.failed())) {
|
||||
XLOGF(CRITICAL, "Failed to create directory {}, error: {}", chunkmetaDir, err.message());
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL, "Saving chunk metadata to directory: {}", chunkmetaDir);
|
||||
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto chainInfos = routingInfo->raw()->chains;
|
||||
|
||||
XLOGF(CRITICAL, "Found {} replication chains in {}", chainInfos.size(), routingInfo->raw()->routingInfoVersion);
|
||||
|
||||
std::map<flat::ChainId, flat::ChainInfo> sortedChainInfos;
|
||||
for (const auto &[chainId, chainInfo] : chainInfos) sortedChainInfos.emplace(chainId, chainInfo);
|
||||
std::set<uint32_t> selectedChainIds(chainIds.begin(), chainIds.end());
|
||||
|
||||
size_t numChainsToSave = selectedChainIds.empty() ? sortedChainInfos.size() : selectedChainIds.size();
|
||||
std::atomic_size_t numTargetsSaved = 0;
|
||||
|
||||
auto dumpChunkMeta = [env, chunkmetaDir, parquetFormat, numChainsToSave, &numTargetsSaved](
|
||||
const size_t chainIndex,
|
||||
const flat::ChainId chainId,
|
||||
const flat::TargetId targetId) -> CoTask<bool> {
|
||||
XLOGF(INFO, "#{}/{} Getting chunk metadata from target: {}@{}", chainIndex, numChainsToSave, targetId, chainId);
|
||||
|
||||
auto chunkMetadata = co_await env.storageClientGetter()->getAllChunkMetadata(chainId, targetId);
|
||||
|
||||
if (!chunkMetadata) {
|
||||
XLOGF(ERR,
|
||||
"Failed to get chunk metadata from target: {}@{}, error: {}",
|
||||
targetId,
|
||||
chainId,
|
||||
chunkMetadata.error());
|
||||
co_return false;
|
||||
}
|
||||
|
||||
time_t timestamp = UtcClock::secondsSinceEpoch();
|
||||
ChunkMetaTable metadata{chainId, targetId, timestamp};
|
||||
|
||||
for (const auto &chunkmeta : *chunkMetadata) {
|
||||
auto metaChunkId = meta::ChunkId::unpack(chunkmeta.chunkId.data());
|
||||
metadata.chunks.push_back({timestamp, chainId, targetId, metaChunkId.inode(), chunkmeta});
|
||||
}
|
||||
|
||||
XLOGF(INFO,
|
||||
"#{}/{} Found {} chunks on target: {}@{}",
|
||||
chainIndex,
|
||||
numChainsToSave,
|
||||
metadata.chunks.size(),
|
||||
targetId,
|
||||
chainId);
|
||||
|
||||
auto filePath =
|
||||
Path(chunkmetaDir) / fmt::format("{}-{}.meta", uint32_t(metadata.chainId), uint64_t(metadata.targetId));
|
||||
bool writeOk = parquetFormat ? metadata.dumpToParquetFile(filePath.replace_extension(".parquet"))
|
||||
: metadata.dumpToFile(filePath);
|
||||
numTargetsSaved += writeOk;
|
||||
|
||||
XLOGF_IF(WARN,
|
||||
writeOk,
|
||||
"#{}/{} Metadata of {} chunks on {}@{} saved to file: {}",
|
||||
chainIndex,
|
||||
numChainsToSave,
|
||||
metadata.chunks.size(),
|
||||
targetId,
|
||||
chainId,
|
||||
filePath);
|
||||
co_return writeOk;
|
||||
};
|
||||
|
||||
auto executor = std::make_unique<folly::CPUThreadPoolExecutor>(parallel);
|
||||
std::vector<folly::coro::TaskWithExecutor<bool>> tasks;
|
||||
size_t chainIndex = 0;
|
||||
|
||||
for (const auto &[chainId, chainInfo] : sortedChainInfos) {
|
||||
if (!selectedChainIds.empty() && !selectedChainIds.count(uint32_t(chainId))) continue;
|
||||
|
||||
chainIndex++;
|
||||
|
||||
if (chainInfo.targets.empty()) {
|
||||
XLOGF(CRITICAL, "Empty list of targets on {}: {}", chainId, chainInfo);
|
||||
co_return makeError(StorageClientCode::kRoutingError);
|
||||
}
|
||||
|
||||
for (const auto &target : chainInfo.targets) {
|
||||
if (target.publicState == flat::PublicTargetState::SERVING) {
|
||||
if (parallel > 0) {
|
||||
tasks.push_back(dumpChunkMeta(chainIndex, chainId, target.targetId)
|
||||
.scheduleOn(folly::Executor::getKeepAliveToken(*executor)));
|
||||
} else {
|
||||
bool ok = co_await dumpChunkMeta(chainIndex, chainId, target.targetId);
|
||||
if (!ok) co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
} else {
|
||||
XLOGF(WARN, "Skip target {}@{} not in serving state: {}", target.targetId, chainId, target);
|
||||
}
|
||||
|
||||
if (onlyHead) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parallel > 0 && !tasks.empty()) {
|
||||
auto results = co_await folly::coro::collectAllWindowed(std::move(tasks), parallel);
|
||||
if (!std::all_of(results.begin(), results.end(), [](bool ok) { return ok; })) {
|
||||
XLOGF(CRITICAL, "Some of the chunkmeta dump tasks failed");
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL,
|
||||
"Chunk metadata on {} targets from {} chains saved to directory: {}",
|
||||
numTargetsSaved.load(),
|
||||
numChainsToSave,
|
||||
chunkmetaDir);
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ChunkMetaTable::dumpToFile(const Path &filePath, bool jsonFormat) const {
|
||||
std::ofstream dumpFile{filePath};
|
||||
if (UNLIKELY(!dumpFile)) {
|
||||
XLOGF(ERR, "Failed to create file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string buffer;
|
||||
|
||||
if (jsonFormat) {
|
||||
buffer = serde::toJsonString(*this, false /*sortKeys*/, true /*prettyFormatting*/);
|
||||
} else {
|
||||
auto bytes = serde::serializeBytes(*this);
|
||||
buffer = std::string(bytes);
|
||||
}
|
||||
|
||||
dumpFile.write(buffer.data(), buffer.size());
|
||||
|
||||
if (UNLIKELY(!dumpFile)) {
|
||||
XLOGF(ERR, "Failed to write to file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChunkMetaTable::loadFromFile(const Path &filePath, bool jsonFormat) {
|
||||
std::ifstream inputFile{filePath, std::ios_base::binary};
|
||||
if (UNLIKELY(!inputFile)) {
|
||||
XLOGF(ERR, "Failed to open file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fileSize = static_cast<size_t>(boost::filesystem::file_size(filePath));
|
||||
std::string buffer;
|
||||
buffer.resize(fileSize, '\0');
|
||||
|
||||
inputFile.read(&buffer[0], fileSize);
|
||||
|
||||
if (buffer.size() != fileSize) {
|
||||
XLOGF(ERR, "Read size {} not equal to file size {}, file: {}", buffer.size(), fileSize, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jsonFormat) {
|
||||
auto result = serde::fromJsonString(*this, buffer);
|
||||
return bool(result);
|
||||
} else {
|
||||
auto result = serde::deserialize(*this, buffer);
|
||||
return bool(result);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChunkMetaTable::dumpToParquetFile(const Path &filePath) const {
|
||||
auto openRes = analytics::SerdeObjectWriter<ChunkMetaRow>::open(filePath);
|
||||
if (!openRes) return false;
|
||||
|
||||
auto serdeWriter = std::move(*openRes);
|
||||
|
||||
for (const auto &chunkmeta : chunks) {
|
||||
serdeWriter << chunkmeta;
|
||||
if (!serdeWriter) break;
|
||||
}
|
||||
|
||||
auto timestamp = UtcClock::secondsSinceEpoch();
|
||||
serdeWriter << ChunkMetaRow{timestamp, chainId, targetId, meta::InodeId{}, storage::ChunkMeta{}};
|
||||
|
||||
return serdeWriter.ok();
|
||||
}
|
||||
|
||||
bool ChunkMetaTable::loadFromParquetFile(const Path &filePath) {
|
||||
auto openRes = analytics::SerdeObjectReader<ChunkMetaRow>::open(filePath);
|
||||
if (!openRes) return false;
|
||||
|
||||
auto serdeReader = std::move(*openRes);
|
||||
chunks.reserve(serdeReader.numRows());
|
||||
timestamp = 0;
|
||||
|
||||
ChunkMetaRow chunkRow;
|
||||
while (serdeReader >> chunkRow) {
|
||||
chunks.push_back(chunkRow);
|
||||
chainId = chunkRow.chainId;
|
||||
targetId = chunkRow.targetId;
|
||||
timestamp = std::max(timestamp, chunkRow.timestamp);
|
||||
}
|
||||
|
||||
return serdeReader.ok();
|
||||
}
|
||||
|
||||
CoTryTask<void> registerDumpChunkMetaHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, dumpChunkMeta);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
36
src/client/cli/admin/DumpChunkMeta.h
Normal file
36
src/client/cli/admin/DumpChunkMeta.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <parquet/schema.h>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class ChunkMetaRow {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(chainId, storage::ChainId{});
|
||||
SERDE_STRUCT_FIELD(targetId, storage::TargetId{});
|
||||
SERDE_STRUCT_FIELD(inodeId, meta::InodeId{});
|
||||
SERDE_STRUCT_FIELD(chunkmeta, storage::ChunkMeta{});
|
||||
};
|
||||
|
||||
struct ChunkMetaTable {
|
||||
SERDE_STRUCT_FIELD(chainId, storage::ChainId{});
|
||||
SERDE_STRUCT_FIELD(targetId, storage::TargetId{});
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(chunks, std::vector<ChunkMetaRow>{});
|
||||
|
||||
public:
|
||||
bool dumpToFile(const Path &filePath, bool jsonFormat = false) const;
|
||||
bool loadFromFile(const Path &filePath, bool jsonFormat = false);
|
||||
bool dumpToParquetFile(const Path &filePath) const;
|
||||
bool loadFromParquetFile(const Path &filePath);
|
||||
};
|
||||
static_assert(serde::Serializable<ChunkMetaTable>);
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpChunkMetaHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
164
src/client/cli/admin/DumpDirEntries.cc
Normal file
164
src/client/cli/admin/DumpDirEntries.cc
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "DumpDirEntries.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "analytics/SerdeObjectReader.h"
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/IEnv.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/logging/LogHelper.h"
|
||||
#include "common/utils/Utf8.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fmt/core.h"
|
||||
#include "meta/event/Scan.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-dentries");
|
||||
parser.add_argument("-n", "--num-dentries-perfile").default_value(uint32_t{10'000'000}).scan<'u', uint32_t>();
|
||||
parser.add_argument("-f", "--fdb-cluster-file").default_value(std::string{"./fdb.cluster"});
|
||||
parser.add_argument("-d", "--dentry-dir").default_value(std::string{"dentries"});
|
||||
parser.add_argument("-t", "--threads").default_value(uint32_t(4)).scan<'u', uint32_t>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> dumpDirEntries(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
ENSURE_USAGE(env.mgmtdClientGetter);
|
||||
|
||||
const auto &fdbClusterFile = parser.get<std::string>("fdb-cluster-file");
|
||||
const auto &numEntriesPerFile = parser.get<uint32_t>("num-dentries-perfile");
|
||||
const auto &dentryDir = parser.get<std::string>("dentry-dir");
|
||||
const auto threads = parser.get<uint32_t>("threads");
|
||||
|
||||
ENSURE_USAGE(threads > 0);
|
||||
ENSURE_USAGE(!fdbClusterFile.empty());
|
||||
ENSURE_USAGE(!dentryDir.empty());
|
||||
ENSURE_USAGE(numEntriesPerFile > 0);
|
||||
|
||||
if (boost::filesystem::exists(dentryDir)) {
|
||||
XLOGF(CRITICAL, "Output directory for directory entries already exists: {}", dentryDir);
|
||||
co_return makeError(StatusCode::kInvalidArg, "output directory already exists");
|
||||
}
|
||||
|
||||
Dispatcher::OutputTable table;
|
||||
auto dumpRes =
|
||||
co_await dumpDirEntriesFromFdb(fdbClusterFile, numEntriesPerFile, dentryDir, std::max(uint32_t(1), threads));
|
||||
if (!dumpRes) co_return makeError(dumpRes.error());
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<Void> dumpDirEntriesFromFdb(const std::string fdbClusterFile,
|
||||
const uint32_t numEntriesPerDir,
|
||||
const std::string dentryDir,
|
||||
const uint32_t threads) {
|
||||
boost::system::error_code err{};
|
||||
boost::filesystem::create_directories(dentryDir, err);
|
||||
|
||||
if (UNLIKELY(err.failed())) {
|
||||
XLOGF(CRITICAL, "Failed to create directory {}, error: {}", dentryDir, err.message());
|
||||
co_return makeError(StatusCode::kIOError,
|
||||
fmt::format("failed to create directory {}, error {}", dentryDir, err.message()));
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL, "Saving directory entries to directory: {}", dentryDir);
|
||||
|
||||
meta::server::MetaScan::Options options;
|
||||
options.fdb_cluster_file = fdbClusterFile;
|
||||
options.threads = 8;
|
||||
options.coroutines = 32;
|
||||
auto scan = std::make_unique<meta::server::MetaScan>(options);
|
||||
auto exec = std::make_unique<folly::CPUThreadPoolExecutor>(16);
|
||||
|
||||
time_t timestamp = UtcClock::secondsSinceEpoch();
|
||||
DirEntryTable dentryBatch{.timestamp = timestamp};
|
||||
dentryBatch.entries.reserve(numEntriesPerDir);
|
||||
size_t numEntriesSaved = 0;
|
||||
|
||||
std::atomic<size_t> running = 0;
|
||||
|
||||
auto dumpDirEntryTable = [&running, dentryDir](size_t numEntriesSaved,
|
||||
const DirEntryTable dentryBatch) -> CoTask<bool> {
|
||||
SCOPE_EXIT { running--; };
|
||||
auto filePath = Path(dentryDir) / fmt::format("{}.parquet", numEntriesSaved);
|
||||
bool writeOk = dentryBatch.dumpToParquetFile(filePath);
|
||||
XLOGF_IF(WARN, writeOk, "{} directory entries saved to file: {}", dentryBatch.entries.size(), filePath);
|
||||
co_return writeOk;
|
||||
};
|
||||
|
||||
std::vector<folly::SemiFuture<bool>> tasks;
|
||||
while (true) {
|
||||
auto entries = scan->getDirEntries();
|
||||
|
||||
for (auto &entry : entries) {
|
||||
dentryBatch.entries.push_back({timestamp, entry});
|
||||
numEntriesSaved++;
|
||||
}
|
||||
|
||||
bool fullBatch = dentryBatch.entries.size() >= numEntriesPerDir;
|
||||
bool lastBatch = entries.empty() && !dentryBatch.entries.empty();
|
||||
|
||||
if (fullBatch || lastBatch) {
|
||||
running++;
|
||||
auto task = folly::coro::co_invoke(dumpDirEntryTable,
|
||||
numEntriesSaved,
|
||||
std::exchange(dentryBatch, DirEntryTable{.timestamp = timestamp}))
|
||||
.scheduleOn(exec.get())
|
||||
.start();
|
||||
tasks.push_back(std::move(task));
|
||||
dentryBatch.entries.reserve(numEntriesPerDir);
|
||||
|
||||
while (running >= threads) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.empty()) break;
|
||||
}
|
||||
|
||||
auto result = co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
auto succ = std::all_of(result.begin(), result.end(), [](auto v) { return v; });
|
||||
if (!succ) {
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL, "In total {} directory entries saved to directory: {}", numEntriesSaved, dentryDir);
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
bool DirEntryTable::dumpToParquetFile(const Path &filePath) const {
|
||||
auto openRes = analytics::SerdeObjectWriter<DirEntryRow>::open(filePath);
|
||||
if (!openRes) return false;
|
||||
|
||||
auto serdeWriter = std::move(*openRes);
|
||||
|
||||
for (const auto &entry : entries) {
|
||||
utf8makevalid((utf8_int8_t *)entry.entry.name.c_str(), '?');
|
||||
serdeWriter << entry;
|
||||
if (!serdeWriter) break;
|
||||
}
|
||||
|
||||
auto timestamp = UtcClock::secondsSinceEpoch();
|
||||
serdeWriter << DirEntryRow{timestamp, meta::DirEntry{}};
|
||||
|
||||
return serdeWriter.ok();
|
||||
}
|
||||
|
||||
CoTryTask<void> registerDumpDirEntriesHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, dumpDirEntries);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
30
src/client/cli/admin/DumpDirEntries.h
Normal file
30
src/client/cli/admin/DumpDirEntries.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
struct DirEntryRow {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(entry, meta::DirEntry{});
|
||||
};
|
||||
|
||||
struct DirEntryTable {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(entries, std::vector<DirEntryRow>());
|
||||
|
||||
public:
|
||||
bool dumpToParquetFile(const Path &filePath) const;
|
||||
};
|
||||
static_assert(serde::Serializable<DirEntryTable>);
|
||||
|
||||
CoTryTask<Void> dumpDirEntriesFromFdb(const std::string fdbClusterFile,
|
||||
const uint32_t numEntriesPerDir,
|
||||
const std::string dentryDir,
|
||||
const uint32_t threads);
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpDirEntriesHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
341
src/client/cli/admin/DumpInodes.cc
Normal file
341
src/client/cli/admin/DumpInodes.cc
Normal file
@@ -0,0 +1,341 @@
|
||||
#include "DumpInodes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/CurrentExecutor.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <folly/futures/Future.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "analytics/SerdeObjectReader.h"
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/IEnv.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/logging/LogHelper.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/Utf8.h"
|
||||
#include "meta/event/Scan.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-inodes");
|
||||
parser.add_argument("-n", "--num-inodes-perfile").default_value(uint32_t{10'000'000}).scan<'u', uint32_t>();
|
||||
parser.add_argument("-f", "--fdb-cluster-file").default_value(std::string{"./fdb.cluster"});
|
||||
parser.add_argument("-i", "--inode-dir").default_value(std::string{"inodes"});
|
||||
parser.add_argument("-q", "--parquet-format").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-a", "--all-inodes").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-t", "--threads").default_value(uint32_t(4)).scan<'u', uint32_t>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> dumpInodes(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
ENSURE_USAGE(env.mgmtdClientGetter);
|
||||
|
||||
const auto &fdbClusterFile = parser.get<std::string>("fdb-cluster-file");
|
||||
const auto &numInodesPerFile = parser.get<uint32_t>("num-inodes-perfile");
|
||||
const auto &inodeDir = parser.get<std::string>("inode-dir");
|
||||
const auto &parquetFormat = parser.get<bool>("parquet-format");
|
||||
const auto &allInodes = parser.get<bool>("all-inodes");
|
||||
const auto &threads = parser.get<uint32_t>("threads");
|
||||
|
||||
ENSURE_USAGE(threads > 0);
|
||||
ENSURE_USAGE(!fdbClusterFile.empty());
|
||||
|
||||
if (boost::filesystem::exists(inodeDir)) {
|
||||
XLOGF(CRITICAL, "Output directory for inodes already exists: {}", inodeDir);
|
||||
co_return makeError(StatusCode::kInvalidArg);
|
||||
}
|
||||
|
||||
Dispatcher::OutputTable table;
|
||||
auto dumpRes = co_await dumpInodesFromFdb(fdbClusterFile,
|
||||
numInodesPerFile,
|
||||
inodeDir,
|
||||
parquetFormat,
|
||||
allInodes,
|
||||
std::max((uint32_t)1, threads));
|
||||
if (!dumpRes) co_return makeError(dumpRes.error());
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<Void> dumpInodesFromFdb(const std::string fdbClusterFile,
|
||||
const uint32_t numInodesPerFile,
|
||||
const std::string inodeDir,
|
||||
const bool parquetFormat,
|
||||
const bool dumpAllInodes,
|
||||
const uint32_t threads) {
|
||||
boost::system::error_code err{};
|
||||
boost::filesystem::create_directories(inodeDir, err);
|
||||
|
||||
if (UNLIKELY(err.failed())) {
|
||||
XLOGF(CRITICAL, "Failed to create directory {}, error: {}", inodeDir, err.message());
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL, "Saving inodes to directory: {}", inodeDir);
|
||||
|
||||
meta::server::MetaScan::Options options;
|
||||
options.fdb_cluster_file = fdbClusterFile;
|
||||
options.threads = 8;
|
||||
options.coroutines = 32;
|
||||
auto scan = std::make_unique<meta::server::MetaScan>(options);
|
||||
auto exec = std::make_unique<folly::CPUThreadPoolExecutor>(16);
|
||||
|
||||
time_t timestamp = UtcClock::secondsSinceEpoch();
|
||||
InodeTable inodeBatch{.timestamp = timestamp};
|
||||
inodeBatch.inodes.reserve(numInodesPerFile);
|
||||
size_t numInodesSaved = 0;
|
||||
std::atomic<size_t> running = 0;
|
||||
|
||||
auto dumpInodeTable = [&running, inodeDir, parquetFormat](size_t numInodesSaved,
|
||||
const InodeTable inodeBatch) -> CoTask<bool> {
|
||||
SCOPE_EXIT { running--; };
|
||||
auto filePath = Path(inodeDir) / fmt::format("{}.inodes", numInodesSaved);
|
||||
bool writeOk = parquetFormat ? inodeBatch.dumpToParquetFile(filePath.replace_extension(".parquet"))
|
||||
: inodeBatch.dumpToFile(filePath);
|
||||
XLOGF_IF(WARN, writeOk, "{} inodes saved to file: {}", inodeBatch.inodes.size(), filePath);
|
||||
co_return writeOk;
|
||||
};
|
||||
|
||||
std::vector<folly::SemiFuture<bool>> tasks;
|
||||
while (true) {
|
||||
auto inodes = scan->getInodes();
|
||||
|
||||
for (const auto &inode : inodes) {
|
||||
if (inode.isFile() || dumpAllInodes) {
|
||||
inodeBatch.inodes.push_back({timestamp, inode});
|
||||
numInodesSaved++;
|
||||
}
|
||||
}
|
||||
|
||||
bool fullBatch = inodeBatch.inodes.size() >= numInodesPerFile;
|
||||
bool lastBatch = inodes.empty() && !inodeBatch.inodes.empty();
|
||||
|
||||
if (fullBatch || lastBatch) {
|
||||
running++;
|
||||
auto task = folly::coro::co_invoke(dumpInodeTable,
|
||||
numInodesSaved,
|
||||
std::exchange(inodeBatch, InodeTable{.timestamp = timestamp}))
|
||||
.scheduleOn(exec.get())
|
||||
.start();
|
||||
tasks.push_back(std::move(task));
|
||||
inodeBatch.inodes.reserve(numInodesPerFile);
|
||||
|
||||
while (running >= threads) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
}
|
||||
|
||||
if (inodes.empty()) break;
|
||||
}
|
||||
|
||||
auto result = co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
auto succ = std::all_of(result.begin(), result.end(), [](auto v) { return v; });
|
||||
if (!succ) {
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
XLOGF(CRITICAL, "In total {} inodes saved to directory: {}", numInodesSaved, inodeDir);
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
std::vector<Path> listFilesFromPath(const Path path) {
|
||||
std::vector<Path> filePaths;
|
||||
|
||||
if (boost::filesystem::is_directory(path)) {
|
||||
for (boost::filesystem::directory_iterator iter{path}; iter != boost::filesystem::directory_iterator(); iter++) {
|
||||
if (boost::filesystem::is_regular_file(iter->path())) {
|
||||
filePaths.push_back(iter->path());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filePaths.push_back(path);
|
||||
}
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
CoTryTask<robin_hood::unordered_set<meta::InodeId>> loadInodeFromFiles(
|
||||
const Path inodePath,
|
||||
const robin_hood::unordered_set<meta::InodeId> &inodeIdsToPeek,
|
||||
const uint32_t parallel,
|
||||
const bool parquetFormat,
|
||||
std::time_t *inodeDumpTime) {
|
||||
std::mutex inodeIdsMutex;
|
||||
size_t inodeFilesLoaded = 0;
|
||||
robin_hood::unordered_set<meta::InodeId> uniqInodeIds(10'000'000);
|
||||
std::time_t inodeFileMinDumpTime(UtcClock::secondsSinceEpoch());
|
||||
|
||||
auto executor = std::make_unique<folly::CPUThreadPoolExecutor>(parallel);
|
||||
std::vector<Path> inodeFilePaths = listFilesFromPath(inodePath);
|
||||
CountDownLatch<folly::fibers::Baton> loadInodesDone(inodeFilePaths.size());
|
||||
|
||||
for (size_t inodeFileIndex = 0; inodeFileIndex < inodeFilePaths.size(); inodeFileIndex++) {
|
||||
executor->add([&, inodeFileIndex, inodeFilePath = inodeFilePaths[inodeFileIndex]]() {
|
||||
auto guard = folly::makeGuard([&loadInodesDone]() { loadInodesDone.countDown(); });
|
||||
InodeTable inodeBatch;
|
||||
|
||||
bool ok = parquetFormat ? inodeBatch.loadFromParquetFile(inodeFilePath) : inodeBatch.loadFromFile(inodeFilePath);
|
||||
if (!ok) {
|
||||
XLOGF(FATAL, "Failed to load file: {}", inodeFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<meta::InodeId> inodeIdVec;
|
||||
inodeIdVec.reserve(inodeBatch.inodes.size());
|
||||
|
||||
for (const auto &inodeRow : inodeBatch.inodes) {
|
||||
inodeIdVec.push_back(inodeRow.inode.id);
|
||||
|
||||
if (!inodeIdsToPeek.empty() && inodeIdsToPeek.count(inodeRow.inode.id)) {
|
||||
XLOGF(CRITICAL, "Found inode {}: {}", inodeRow.inode.id, inodeRow.inode);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::scoped_lock lock(inodeIdsMutex);
|
||||
inodeFileMinDumpTime = std::min(inodeBatch.timestamp, inodeFileMinDumpTime);
|
||||
uniqInodeIds.insert(inodeIdVec.begin(), inodeIdVec.end());
|
||||
inodeFilesLoaded++;
|
||||
|
||||
XLOGF(WARN,
|
||||
"#{}/{} Loaded {} inodes from file: {}, total number of inodes: {}, timestamp: {:%c}",
|
||||
inodeFileIndex + 1,
|
||||
inodeFilePaths.size(),
|
||||
inodeBatch.inodes.size(),
|
||||
inodeFilePath,
|
||||
uniqInodeIds.size(),
|
||||
fmt::localtime(inodeBatch.timestamp));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
co_await loadInodesDone.wait();
|
||||
executor->join();
|
||||
|
||||
XLOGF(CRITICAL,
|
||||
"Loaded {} inodes from {}/{} files in path: {}",
|
||||
uniqInodeIds.size(),
|
||||
inodeFilesLoaded,
|
||||
inodeFilePaths.size(),
|
||||
inodePath);
|
||||
|
||||
if (inodeFilesLoaded < inodeFilePaths.size()) {
|
||||
XLOGF(FATAL,
|
||||
"Failed to load {}/{} inode files in path: {}",
|
||||
inodeFilePaths.size() - inodeFilesLoaded,
|
||||
inodeFilePaths.size(),
|
||||
inodePath);
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
if (inodeDumpTime != nullptr) *inodeDumpTime = inodeFileMinDumpTime;
|
||||
co_return uniqInodeIds;
|
||||
}
|
||||
|
||||
bool InodeTable::dumpToFile(const Path &filePath) const {
|
||||
std::ofstream dumpFile{filePath};
|
||||
if (UNLIKELY(!dumpFile)) {
|
||||
XLOGF(ERR, "Failed to create file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto bytes = serde::serializeBytes(*this);
|
||||
auto str = std::string_view{bytes};
|
||||
dumpFile.write(str.data(), str.size());
|
||||
|
||||
if (UNLIKELY(!dumpFile)) {
|
||||
XLOGF(ERR, "Failed to write to file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InodeTable::loadFromFile(const Path &filePath) {
|
||||
std::ifstream inputFile{filePath, std::ios_base::binary};
|
||||
if (UNLIKELY(!inputFile)) {
|
||||
XLOGF(ERR, "Failed to open file: {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fileSize = static_cast<size_t>(boost::filesystem::file_size(filePath));
|
||||
std::string str;
|
||||
str.resize(fileSize, '\0');
|
||||
|
||||
inputFile.read(&str[0], fileSize);
|
||||
|
||||
if (str.size() != fileSize) {
|
||||
XLOGF(ERR, "Read size {} not equal to file size {}, file: {}", str.size(), fileSize, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto result = serde::deserialize(*this, str);
|
||||
|
||||
return bool(result);
|
||||
}
|
||||
|
||||
bool InodeTable::dumpToParquetFile(const Path &filePath) const {
|
||||
auto openRes = analytics::SerdeObjectWriter<InodeRow>::open(filePath);
|
||||
if (!openRes) return false;
|
||||
|
||||
auto serdeWriter = std::move(*openRes);
|
||||
|
||||
for (const auto &inode : inodes) {
|
||||
if (inode.inode.isDirectory()) {
|
||||
utf8makevalid((utf8_int8_t *)inode.inode.asDirectory().name.c_str(), '?');
|
||||
}
|
||||
serdeWriter << inode;
|
||||
if (!serdeWriter) break;
|
||||
}
|
||||
|
||||
auto timestamp = UtcClock::secondsSinceEpoch();
|
||||
serdeWriter << InodeRow{timestamp, meta::Inode{}};
|
||||
|
||||
return serdeWriter.ok();
|
||||
}
|
||||
|
||||
bool InodeTable::loadFromParquetFile(const Path &filePath) {
|
||||
auto openRes = analytics::SerdeObjectReader<InodeRow>::open(filePath);
|
||||
if (!openRes) return false;
|
||||
|
||||
auto serdeReader = std::move(*openRes);
|
||||
inodes.reserve(serdeReader.numRows());
|
||||
timestamp = UtcClock::secondsSinceEpoch();
|
||||
|
||||
InodeRow inodeRow;
|
||||
while (serdeReader >> inodeRow) {
|
||||
inodes.push_back(inodeRow);
|
||||
timestamp = std::min(timestamp, inodeRow.timestamp);
|
||||
}
|
||||
|
||||
ERRLOGF_IF(INFO,
|
||||
inodes.size() != serdeReader.numRows(),
|
||||
"Loaded {} inodes from {} rows in file: {}",
|
||||
inodes.size(),
|
||||
serdeReader.numRows(),
|
||||
filePath);
|
||||
return serdeReader.ok();
|
||||
}
|
||||
|
||||
CoTryTask<void> registerDumpInodesHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, dumpInodes);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
44
src/client/cli/admin/DumpInodes.h
Normal file
44
src/client/cli/admin/DumpInodes.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
struct InodeRow {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(inode, meta::Inode{});
|
||||
};
|
||||
|
||||
struct InodeTable {
|
||||
SERDE_STRUCT_FIELD(timestamp, std::time_t{});
|
||||
SERDE_STRUCT_FIELD(inodes, std::vector<InodeRow>{});
|
||||
|
||||
public:
|
||||
bool dumpToFile(const Path &filePath) const;
|
||||
bool loadFromFile(const Path &filePath);
|
||||
bool dumpToParquetFile(const Path &filePath) const;
|
||||
bool loadFromParquetFile(const Path &filePath);
|
||||
};
|
||||
static_assert(serde::Serializable<InodeTable>);
|
||||
|
||||
std::vector<Path> listFilesFromPath(const Path path);
|
||||
|
||||
CoTryTask<Void> dumpInodesFromFdb(const std::string fdbClusterFile,
|
||||
const uint32_t numInodesPerFile,
|
||||
const std::string inodeDir,
|
||||
const bool parquetFormat = false,
|
||||
const bool dumpAllInodes = false,
|
||||
const uint32_t threads = 4);
|
||||
|
||||
CoTryTask<robin_hood::unordered_set<meta::InodeId>> loadInodeFromFiles(
|
||||
const Path inodePath,
|
||||
const robin_hood::unordered_set<meta::InodeId> &inodeIdsToPeek,
|
||||
const uint32_t parallel,
|
||||
const bool parquetFormat = false,
|
||||
std::time_t *inodeDumpTime = nullptr);
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpInodesHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
89
src/client/cli/admin/DumpSession.cc
Normal file
89
src/client/cli/admin/DumpSession.cc
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "client/cli/admin/DumpSession.h"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Likely.h>
|
||||
#include <string>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "analytics/SerdeObjectWriter.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/kv/ITransaction.h"
|
||||
#include "common/kv/WithTransaction.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fdb/FDBRetryStrategy.h"
|
||||
#include "meta/components/SessionManager.h"
|
||||
#include "meta/store/FileSession.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
kv::FDBRetryStrategy getRetryStrategy() { return kv::FDBRetryStrategy({1_s, 10, false}); }
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("dump-session");
|
||||
parser.add_argument("--output").help("output parquet filename");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleDumpSession(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
|
||||
auto output = parser.present<std::string>("--output");
|
||||
ENSURE_USAGE(!output->empty(), "must specify output filename");
|
||||
ENSURE_USAGE(!boost::filesystem::exists(*output), "output filename already exists");
|
||||
|
||||
auto openRes = analytics::SerdeObjectWriter<meta::server::FileSession>::open(*output);
|
||||
if (!openRes) {
|
||||
XLOGF(ERR, "Failed to open writer {}", *output);
|
||||
co_return makeError(StatusCode::kOSError, "failed to create writer");
|
||||
}
|
||||
auto &writer = *openRes;
|
||||
size_t cnt = 0;
|
||||
for (size_t shard = 0; shard < meta::server::FileSession::kShard; shard++) {
|
||||
std::optional<meta::server::FileSession> prev;
|
||||
while (true) {
|
||||
auto handler = [&](kv::IReadOnlyTransaction &txn) -> CoTryTask<std::vector<meta::server::FileSession>> {
|
||||
co_return co_await meta::server::FileSession::scan(txn, shard, prev);
|
||||
};
|
||||
auto sessions = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
CO_RETURN_ON_ERROR(sessions);
|
||||
if (sessions->empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
prev = sessions->back();
|
||||
for (auto &s : *sessions) {
|
||||
writer << s;
|
||||
cnt++;
|
||||
if (!writer) {
|
||||
co_return makeError(StatusCode::kUnknown, "serde writer error");
|
||||
}
|
||||
}
|
||||
if (cnt > 1 << 20) {
|
||||
writer.endRowGroup();
|
||||
if (!writer) {
|
||||
co_return makeError(StatusCode::kUnknown, "serde writer error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.endRowGroup();
|
||||
if (!writer) {
|
||||
co_return makeError(StatusCode::kUnknown, "serde writer error");
|
||||
}
|
||||
|
||||
co_return Dispatcher::OutputTable{};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerDumpSessionHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleDumpSession);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/DumpSession.h
Normal file
8
src/client/cli/admin/DumpSession.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerDumpSessionHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
287
src/client/cli/admin/FileWrapper.cc
Normal file
287
src/client/cli/admin/FileWrapper.cc
Normal file
@@ -0,0 +1,287 @@
|
||||
#include "FileWrapper.h"
|
||||
|
||||
#include "common/net/ib/RDMABuf.h"
|
||||
#include "common/utils/Result.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto rdmabufPool = net::RDMABufPool::create(256_MB, 1024);
|
||||
|
||||
} // namespace
|
||||
|
||||
static constexpr std::array<uint8_t, 16_MB> zeros{};
|
||||
|
||||
Result<std::vector<Block>> FileWrapper::prepareBlocks(size_t offset, size_t end, const flat::RoutingInfo &routingInfo) {
|
||||
auto chunkSize = file().layout.chunkSize;
|
||||
std::vector<Block> blocks;
|
||||
while (offset < end) {
|
||||
auto chainResult = file().getChainId(inode_, offset, routingInfo, 0);
|
||||
RETURN_AND_LOG_ON_ERROR(chainResult);
|
||||
auto chunk = file().getChunkId(inode_.id, offset).then([](auto chunk) { return storage::ChunkId(chunk); });
|
||||
RETURN_AND_LOG_ON_ERROR(chunk);
|
||||
auto next = std::min((offset / chunkSize + 1) * chunkSize, end);
|
||||
blocks.push_back(Block{*chainResult, *chunk, offset % chunkSize, next - offset});
|
||||
offset = next;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
Result<uint32_t> FileWrapper::replicasNum() {
|
||||
RETURN_AND_LOG_ON_ERROR(folly::coro::blockingWait(env_.mgmtdClientGetter()->refreshRoutingInfo(true)));
|
||||
auto routingInfo = env_.mgmtdClientGetter()->getRoutingInfo();
|
||||
if (routingInfo == nullptr || routingInfo->raw() == nullptr) {
|
||||
return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
auto chainIdResult = file().getChainId(inode_, 0, *routingInfo->raw(), 0);
|
||||
RETURN_AND_LOG_ON_ERROR(chainIdResult);
|
||||
auto chainId = *chainIdResult;
|
||||
|
||||
auto chainResult = routingInfo->getChain(chainId);
|
||||
if (!chainResult) {
|
||||
return makeError(StorageClientCode::kRoutingError, fmt::format("chain info is null, {}", chainId));
|
||||
}
|
||||
|
||||
uint32_t num = 0;
|
||||
for (auto &info : chainResult->targets) {
|
||||
num += info.publicState == flat::PublicTargetState::SERVING;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
Result<Void> FileWrapper::showChunks(size_t offset, size_t length) {
|
||||
RETURN_AND_LOG_ON_ERROR(folly::coro::blockingWait(env_.mgmtdClientGetter()->refreshRoutingInfo(true)));
|
||||
auto routingInfo = env_.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (routingInfo == nullptr) {
|
||||
return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
auto blocksResult = prepareBlocks(offset, offset + length, *routingInfo);
|
||||
RETURN_AND_LOG_ON_ERROR(blocksResult);
|
||||
auto &blocks = *blocksResult;
|
||||
auto idx = 0;
|
||||
for (auto &block : blocks) {
|
||||
if (idx == 10) {
|
||||
std::cout << "... (has more chunks)" << std::endl;
|
||||
break;
|
||||
}
|
||||
std::cout << fmt::format("block{}: {}", idx++, block) << std::endl;
|
||||
}
|
||||
return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<storage::ChecksumInfo> FileWrapper::readFile(std::ostream &out,
|
||||
size_t length,
|
||||
size_t offset,
|
||||
bool checksum,
|
||||
bool hex,
|
||||
storage::client::TargetSelectionMode mode,
|
||||
MD5_CTX *md5 /* = nullptr */,
|
||||
bool fillZero /* = false */,
|
||||
bool verbose /* = false */,
|
||||
uint32_t targetIndex /* = 0 */) {
|
||||
auto routingInfo = env_.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (routingInfo == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
auto blocksResult = prepareBlocks(offset, offset + length, *routingInfo);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(blocksResult);
|
||||
auto &blocks = *blocksResult;
|
||||
co_return co_await readFile(env_, out, blocks, checksum, hex, mode, md5, fillZero, verbose, targetIndex);
|
||||
}
|
||||
|
||||
CoTryTask<storage::ChecksumInfo> FileWrapper::readFile(AdminEnv &env,
|
||||
std::ostream &out,
|
||||
const std::vector<Block> &blocks,
|
||||
bool checksum,
|
||||
bool hex,
|
||||
storage::client::TargetSelectionMode mode,
|
||||
MD5_CTX *md5 /* = nullptr */,
|
||||
bool fillZero /* = false */,
|
||||
bool verbose /* = false */,
|
||||
uint32_t targetIndex /* = 0 */) {
|
||||
auto client = env.storageClientGetter();
|
||||
auto buffer = co_await rdmabufPool->allocate();
|
||||
if (UNLIKELY(!buffer)) {
|
||||
XLOGF(ERR, "allocate buffer failed");
|
||||
co_return makeError(RPCCode::kRDMANoBuf);
|
||||
}
|
||||
std::basic_string_view<uint8_t> data(buffer.ptr(), buffer.size());
|
||||
auto readBuffer = storage::client::IOBuffer{buffer};
|
||||
auto bufferOffset = 0;
|
||||
std::vector<storage::client::ReadIO> batch;
|
||||
storage::client::ReadOptions readOptions;
|
||||
readOptions.set_enableChecksum(checksum);
|
||||
readOptions.targetSelection().set_mode(mode);
|
||||
readOptions.targetSelection().set_targetIndex(targetIndex);
|
||||
|
||||
storage::ChecksumInfo checksumInfo;
|
||||
for (auto &block : blocks) {
|
||||
auto readIO = client->createReadIO(block.chainId,
|
||||
block.chunkId,
|
||||
block.offset,
|
||||
block.length,
|
||||
(uint8_t *)&data[bufferOffset],
|
||||
&readBuffer);
|
||||
batch.push_back(std::move(readIO));
|
||||
bufferOffset += block.length;
|
||||
if (&block == &blocks.back() || bufferOffset + (&block + 1)->length > buffer.size()) {
|
||||
auto result = co_await client->batchRead(batch, env.userInfo, readOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
for (auto &readIO : batch) {
|
||||
auto succLength = 0ul;
|
||||
if (readIO.result.lengthInfo) {
|
||||
succLength = *readIO.result.lengthInfo;
|
||||
} else if (readIO.result.lengthInfo.error().code() == StorageClientCode::kChunkNotFound && fillZero) {
|
||||
} else {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readIO.result.lengthInfo);
|
||||
}
|
||||
if (succLength != readIO.length) {
|
||||
if (fillZero) {
|
||||
if (verbose) {
|
||||
std::cout << fmt::format("{} find hole, expected size {}, actual size {}\n",
|
||||
readIO.chunkId,
|
||||
readIO.length,
|
||||
succLength);
|
||||
}
|
||||
auto needFill = readIO.length - succLength;
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readIO.result.checksum.combine(
|
||||
storage::ChecksumInfo::create(storage::ChecksumType::CRC32C, zeros.data(), needFill),
|
||||
needFill));
|
||||
succLength = readIO.length;
|
||||
} else {
|
||||
co_return makeError(StatusCode::kInvalidFormat,
|
||||
fmt::format("read is short, chain {} chunk {} offset {} expect {} read {}",
|
||||
readIO.routingTarget.chainId,
|
||||
readIO.chunkId,
|
||||
readIO.offset,
|
||||
readIO.length,
|
||||
succLength));
|
||||
}
|
||||
}
|
||||
CO_RETURN_AND_LOG_ON_ERROR(checksumInfo.combine(readIO.result.checksum, succLength));
|
||||
}
|
||||
|
||||
// clean up.
|
||||
if (hex) {
|
||||
out << fmt::format("{:02X}", fmt::join(data.substr(0, bufferOffset), " "));
|
||||
} else {
|
||||
out.write(reinterpret_cast<const char *>(data.data()), bufferOffset);
|
||||
}
|
||||
if (md5) {
|
||||
int ret = MD5_Update(md5, reinterpret_cast<const char *>(data.data()), bufferOffset);
|
||||
if (UNLIKELY(ret != 1)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("md5 update error {}", ret));
|
||||
}
|
||||
}
|
||||
if (!out) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "write to stream failed");
|
||||
}
|
||||
batch.clear();
|
||||
bufferOffset = 0;
|
||||
}
|
||||
}
|
||||
if (hex) {
|
||||
out << std::endl;
|
||||
}
|
||||
co_return checksumInfo;
|
||||
}
|
||||
|
||||
CoTryTask<storage::ChecksumInfo> FileWrapper::writeFile(std::istream &in,
|
||||
size_t length,
|
||||
size_t offset,
|
||||
Duration timeout,
|
||||
bool syncAfterWrite /* = true */) {
|
||||
auto chunkSize = file().layout.chunkSize;
|
||||
auto client = env_.storageClientGetter();
|
||||
std::vector<uint8_t> data(64_MB, 0);
|
||||
auto registerResult = client->registerIOBuffer(&data[0], data.size());
|
||||
CO_RETURN_AND_LOG_ON_ERROR(registerResult);
|
||||
auto writeBuffer = std::move(*registerResult);
|
||||
auto bufferOffset = 0;
|
||||
std::vector<storage::client::WriteIO> batch;
|
||||
storage::client::WriteOptions writeOptions;
|
||||
writeOptions.retry().set_init_wait_time(timeout);
|
||||
writeOptions.retry().set_max_wait_time(timeout);
|
||||
writeOptions.retry().set_max_retry_time(timeout * 10);
|
||||
|
||||
auto routingInfo = env_.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (routingInfo == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
auto blocksResult = prepareBlocks(offset, offset + length, *routingInfo);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(blocksResult);
|
||||
auto &blocks = *blocksResult;
|
||||
|
||||
storage::ChecksumInfo checksum;
|
||||
for (auto &block : blocks) {
|
||||
if (!in.read(reinterpret_cast<char *>(&data[bufferOffset]), block.length)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "write from file failed");
|
||||
}
|
||||
auto writeIO = client->createWriteIO(block.chainId,
|
||||
block.chunkId,
|
||||
block.offset,
|
||||
block.length,
|
||||
chunkSize,
|
||||
&data[bufferOffset],
|
||||
&writeBuffer);
|
||||
batch.push_back(std::move(writeIO));
|
||||
bufferOffset += block.length;
|
||||
if (bufferOffset + chunkSize > data.capacity() || &block == &blocks.back()) {
|
||||
auto result = co_await client->batchWrite(batch, env_.userInfo, writeOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
for (auto &writeIO : batch) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(writeIO.result.lengthInfo);
|
||||
auto succLength = *writeIO.result.lengthInfo;
|
||||
if (succLength != writeIO.length) {
|
||||
co_return makeError(StatusCode::kInvalidFormat,
|
||||
fmt::format("write is short, chain {} chunk {} offset {} expect {} write {}",
|
||||
writeIO.routingTarget.chainId,
|
||||
writeIO.chunkId,
|
||||
writeIO.offset,
|
||||
writeIO.length,
|
||||
succLength));
|
||||
}
|
||||
CO_RETURN_AND_LOG_ON_ERROR(checksum.combine(writeIO.result.checksum, succLength));
|
||||
}
|
||||
|
||||
// clean up.
|
||||
batch.clear();
|
||||
bufferOffset = 0;
|
||||
}
|
||||
}
|
||||
if (syncAfterWrite) {
|
||||
auto syncResult = co_await sync();
|
||||
CO_RETURN_AND_LOG_ON_ERROR(syncResult);
|
||||
}
|
||||
|
||||
co_return checksum;
|
||||
}
|
||||
|
||||
CoTryTask<FileWrapper> FileWrapper::openOrCreateFile(AdminEnv &env,
|
||||
const Path &src,
|
||||
bool createIsMissing /* = false */) {
|
||||
meta::Inode inode;
|
||||
auto statResult = co_await env.metaClientGetter()->stat(env.userInfo, env.currentDirId, src, true);
|
||||
if (!statResult && statResult.error().code() == MetaCode::kNotFound && createIsMissing) {
|
||||
auto result = co_await env.metaClientGetter()
|
||||
->create(env.userInfo, env.currentDirId, src, std::nullopt, meta::Permission(0644), 0);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
inode = *result;
|
||||
} else if (!statResult) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(statResult);
|
||||
} else {
|
||||
inode = *statResult;
|
||||
}
|
||||
if (!inode.isFile()) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "not a file");
|
||||
}
|
||||
|
||||
FileWrapper file(env);
|
||||
file.inode_ = std::move(inode);
|
||||
co_return file;
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
83
src/client/cli/admin/FileWrapper.h
Normal file
83
src/client/cli/admin/FileWrapper.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <fstream>
|
||||
#define OPENSSL_SUPPRESS_DEPRECATED
|
||||
#include <openssl/md5.h>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#include "client/cli/admin/AdminEnv.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "client/storage/TargetSelection.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
struct Block {
|
||||
SERDE_STRUCT_FIELD(chainId, storage::ChainId{});
|
||||
SERDE_STRUCT_FIELD(chunkId, storage::ChunkId{});
|
||||
SERDE_STRUCT_FIELD(offset, uint64_t{});
|
||||
SERDE_STRUCT_FIELD(length, uint64_t{});
|
||||
};
|
||||
|
||||
class FileWrapper {
|
||||
public:
|
||||
FileWrapper(AdminEnv &env)
|
||||
: env_(env) {}
|
||||
|
||||
auto &file() { return inode_.asFile(); }
|
||||
auto &file() const { return inode_.asFile(); }
|
||||
auto length() const { return file().length; }
|
||||
|
||||
auto truncate(size_t end) { return env_.metaClientGetter()->truncate(env_.userInfo, inode_.id, end); }
|
||||
auto sync() { return env_.metaClientGetter()->sync(env_.userInfo, inode_.id, false, true, std::nullopt); }
|
||||
|
||||
Result<std::vector<Block>> prepareBlocks(size_t offset, size_t end, const flat::RoutingInfo &routingInfo);
|
||||
Result<uint32_t> replicasNum();
|
||||
Result<Void> showChunks(size_t offset, size_t length);
|
||||
|
||||
CoTryTask<storage::ChecksumInfo> readFile(
|
||||
std::ostream &out,
|
||||
size_t length,
|
||||
size_t offset,
|
||||
bool checksum = true,
|
||||
bool hex = false,
|
||||
storage::client::TargetSelectionMode mode = storage::client::TargetSelectionMode::Default,
|
||||
MD5_CTX *md5 = nullptr,
|
||||
bool fillZero = false,
|
||||
bool verbose = false,
|
||||
uint32_t targetIndex = 0);
|
||||
|
||||
static CoTryTask<storage::ChecksumInfo> readFile(
|
||||
AdminEnv &env,
|
||||
std::ostream &out,
|
||||
const std::vector<Block> &blocks,
|
||||
bool checksum = true,
|
||||
bool hex = false,
|
||||
storage::client::TargetSelectionMode mode = storage::client::TargetSelectionMode::Default,
|
||||
MD5_CTX *md5 = nullptr,
|
||||
bool fillZero = false,
|
||||
bool verbose = false,
|
||||
uint32_t targetIndex = 0);
|
||||
|
||||
CoTryTask<storage::ChecksumInfo> writeFile(std::istream &in,
|
||||
size_t length,
|
||||
size_t offset,
|
||||
Duration timeout,
|
||||
bool syncAfterWrite = true);
|
||||
|
||||
static CoTryTask<FileWrapper> openOrCreateFile(AdminEnv &env, const Path &src, bool createIsMissing = false);
|
||||
|
||||
private:
|
||||
AdminEnv &env_;
|
||||
SERDE_CLASS_FIELD(inode, meta::Inode{});
|
||||
};
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
123
src/client/cli/admin/FillZero.cc
Normal file
123
src/client/cli/admin/FillZero.cc
Normal file
@@ -0,0 +1,123 @@
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "WriteFile.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("fill-zero");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("--dry-run").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--verbose").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleFillZero(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
Path src = parser.get<std::string>("path");
|
||||
auto dryRun = parser.get<bool>("--dry-run");
|
||||
auto verbose = parser.get<bool>("--verbose");
|
||||
auto rdmabufPool = net::RDMABufPool::create(32_MB, 1024);
|
||||
if (dryRun) {
|
||||
std::cout << "Dry-run mode, no data will be written" << std::endl;
|
||||
}
|
||||
|
||||
// 2. stat file.
|
||||
auto openResult = co_await FileWrapper::openOrCreateFile(env, src, true);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(openResult);
|
||||
auto &file = *openResult;
|
||||
|
||||
auto storageClient = env.storageClientGetter();
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (UNLIKELY(routingInfo == nullptr)) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
// 3. read and fill.
|
||||
storage::client::ReadOptions readOptions;
|
||||
storage::client::WriteOptions writeOptions;
|
||||
uint64_t chunkSize = file.file().layout.chunkSize;
|
||||
for (auto startPoint = 0ul; startPoint < file.length(); startPoint += 32_MB) {
|
||||
std::vector<storage::client::ReadIO> readIOs;
|
||||
std::vector<storage::client::WriteIO> writeIOs;
|
||||
net::RDMABuf current = co_await rdmabufPool->allocate();
|
||||
storage::client::IOBuffer buffer(current);
|
||||
|
||||
for (auto offset = startPoint; offset < file.length() && offset < startPoint + 32_MB; offset += chunkSize) {
|
||||
auto chainResult = file.file().getChainId(file.inode(), offset, *routingInfo, 0);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chainResult);
|
||||
auto chunk =
|
||||
file.file().getChunkId(file.inode().id, offset).then([](auto chunk) { return storage::ChunkId(chunk); });
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chunk);
|
||||
auto l = std::min(file.file().length - offset, chunkSize);
|
||||
readIOs.push_back(
|
||||
storageClient->createReadIO(*chainResult, *chunk, offset % chunkSize, l, current.ptr(), &buffer));
|
||||
current.advance(chunkSize);
|
||||
}
|
||||
|
||||
auto readResult = co_await storageClient->batchRead(readIOs, env.userInfo, readOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
|
||||
net::RDMABuf writeCurrent = co_await rdmabufPool->allocate();
|
||||
storage::client::IOBuffer writeBuffer(writeCurrent);
|
||||
std::memset(writeCurrent.ptr(), 0, 32_MB);
|
||||
for (auto &readIO : readIOs) {
|
||||
auto succLength = 0ul;
|
||||
if (readIO.result.lengthInfo) {
|
||||
succLength = *readIO.result.lengthInfo;
|
||||
} else if (readIO.result.lengthInfo.error().code() != StorageClientCode::kChunkNotFound) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readIO.result.lengthInfo);
|
||||
}
|
||||
|
||||
if (succLength < readIO.length) {
|
||||
if (verbose || dryRun) {
|
||||
std::cout << fmt::format("{} find hole, expected size {}, actual size {}\n",
|
||||
readIO.chunkId,
|
||||
readIO.length,
|
||||
succLength);
|
||||
}
|
||||
writeIOs.push_back(storageClient->createWriteIO(readIO.routingTarget.chainId,
|
||||
readIO.chunkId,
|
||||
succLength,
|
||||
readIO.length - succLength,
|
||||
chunkSize,
|
||||
writeCurrent.ptr(),
|
||||
&writeBuffer));
|
||||
writeCurrent.advance(chunkSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (!writeIOs.empty() && !dryRun) {
|
||||
auto writeResult = co_await storageClient->batchWrite(writeIOs, env.userInfo, writeOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(writeResult);
|
||||
}
|
||||
}
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerFillZeroHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleFillZero);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/FillZero.h
Normal file
10
src/client/cli/admin/FillZero.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerFillZeroHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
183
src/client/cli/admin/FindOrphanedChunks.cc
Normal file
183
src/client/cli/admin/FindOrphanedChunks.cc
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "FindOrphanedChunks.h"
|
||||
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <vector>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "DumpChunkMeta.h"
|
||||
#include "DumpInodes.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/IEnv.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("find-orphaned-chunks");
|
||||
parser.add_argument("-i", "--inode-path").default_value(std::string{"inodes"});
|
||||
parser.add_argument("-m", "--chunkmeta-path").default_value(std::string{"chunkmeta"});
|
||||
parser.add_argument("-n", "--inode-ids").nargs(argparse::nargs_pattern::any).scan<'x', uint64_t>();
|
||||
parser.add_argument("-o", "--orphaned-dir").default_value(std::string{"orphaned"});
|
||||
parser.add_argument("-x", "--ignore-chunkid-prefix").default_value(std::string{"F"});
|
||||
parser.add_argument("-v", "--only-chunkid-prefix").default_value(std::string{""});
|
||||
parser.add_argument("-S", "--skip-safety-check").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-q", "--parquet-format").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-p", "--parallel").default_value(uint32_t{32}).scan<'u', uint32_t>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> findOrphanedChunks(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
ENSURE_USAGE(env.mgmtdClientGetter);
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
const auto &inodePath = parser.get<std::string>("inode-path");
|
||||
const auto &chunkmetaPath = parser.get<std::string>("chunkmeta-path");
|
||||
const auto &inodeInt64Ids = parser.get<std::vector<uint64_t>>("inode-ids");
|
||||
const auto &orphanedDir = parser.get<std::string>("orphaned-dir");
|
||||
const auto &ignoreChunkIdPrefix = parser.get<std::string>("ignore-chunkid-prefix");
|
||||
const auto &onlyChunkIdPrefix = parser.get<std::string>("only-chunkid-prefix");
|
||||
const auto &parquetFormat = parser.get<bool>("parquet-format");
|
||||
const auto &skipSafetyCheck = parser.get<bool>("skip-safety-check");
|
||||
const auto parallel = std::min(parser.get<uint32_t>("parallel"), std::thread::hardware_concurrency() / 2);
|
||||
|
||||
if (boost::filesystem::exists(orphanedDir)) {
|
||||
XLOGF(CRITICAL, "Output directory for orphaned chunks already exists: {}", orphanedDir);
|
||||
co_return makeError(StatusCode::kInvalidArg);
|
||||
}
|
||||
|
||||
boost::system::error_code err{};
|
||||
boost::filesystem::create_directories(orphanedDir, err);
|
||||
|
||||
if (UNLIKELY(err.failed())) {
|
||||
XLOGF(CRITICAL, "Failed to create directory {}, error: {}", orphanedDir, err.message());
|
||||
co_return makeError(StatusCode::kIOError);
|
||||
}
|
||||
|
||||
std::time_t inodeDumpTime(std::time(nullptr));
|
||||
robin_hood::unordered_set<meta::InodeId> inodeIdsToPeek;
|
||||
for (const auto &n : inodeInt64Ids) inodeIdsToPeek.insert(meta::InodeId{n});
|
||||
|
||||
auto uniqInodeIds = co_await loadInodeFromFiles(inodePath, inodeIdsToPeek, parallel, parquetFormat, &inodeDumpTime);
|
||||
if (!uniqInodeIds) {
|
||||
XLOGF(FATAL, "Failed to load inodes from directory {}, error: {}", inodePath, uniqInodeIds.error());
|
||||
co_return makeError(uniqInodeIds.error());
|
||||
}
|
||||
|
||||
if (!inodeIdsToPeek.empty()) {
|
||||
XLOGF(CRITICAL, "Completed to find {} inodes in path: {}", inodeIdsToPeek.size(), inodePath);
|
||||
co_return table;
|
||||
}
|
||||
|
||||
std::vector<Path> chunkmetaFilePaths = listFilesFromPath(chunkmetaPath);
|
||||
XLOGF(CRITICAL, "Processing {} chunk metadata files in path: {}", chunkmetaFilePaths.size(), chunkmetaPath);
|
||||
|
||||
std::atomic_size_t numOrphanedChunks = 0;
|
||||
std::atomic_size_t numIgnoredOrphanedChunks = 0;
|
||||
std::atomic_size_t numProcessedChunks = 0;
|
||||
std::atomic_size_t sizeOfOrphanedChunks = 0;
|
||||
std::atomic_size_t sizeOfIgnoredOrphanedChunks = 0;
|
||||
auto executor = std::make_unique<folly::CPUThreadPoolExecutor>(parallel);
|
||||
CountDownLatch<folly::fibers::Baton> loadChunkmetaDone(chunkmetaFilePaths.size());
|
||||
|
||||
for (size_t chunkmetaFileIndex = 0; chunkmetaFileIndex < chunkmetaFilePaths.size(); chunkmetaFileIndex++) {
|
||||
executor->add([&, chunkmetaFileIndex, chunkmetaFilePath = chunkmetaFilePaths[chunkmetaFileIndex]]() {
|
||||
auto guard = folly::makeGuard([&loadChunkmetaDone]() { loadChunkmetaDone.countDown(); });
|
||||
ChunkMetaTable metadata;
|
||||
|
||||
bool ok =
|
||||
parquetFormat ? metadata.loadFromParquetFile(chunkmetaFilePath) : metadata.loadFromFile(chunkmetaFilePath);
|
||||
if (!ok) {
|
||||
XLOGF(FATAL, "Failed to load file: {}", chunkmetaFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skipSafetyCheck && metadata.timestamp >= inodeDumpTime) {
|
||||
XLOGF(FATAL,
|
||||
"Chunk metadata dump time '{:%c}' >= inode snapshot time '{:%c}', skipping file: {}",
|
||||
fmt::localtime(metadata.timestamp),
|
||||
fmt::localtime(inodeDumpTime),
|
||||
chunkmetaFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
ChunkMetaTable orphanedMetadata{.chainId = metadata.chainId,
|
||||
.targetId = metadata.targetId,
|
||||
.timestamp = metadata.timestamp};
|
||||
|
||||
for (const auto &chunk : metadata.chunks) {
|
||||
const auto &chunkmeta = chunk.chunkmeta;
|
||||
|
||||
if (chunkmeta.chunkId != storage::ChunkId{} && !uniqInodeIds->count(chunk.inodeId)) {
|
||||
if ((!ignoreChunkIdPrefix.empty() && chunkmeta.chunkId.toString().starts_with(ignoreChunkIdPrefix)) ||
|
||||
(!onlyChunkIdPrefix.empty() && !chunkmeta.chunkId.toString().starts_with(onlyChunkIdPrefix))) {
|
||||
XLOGF(DBG, "Ignore an orphaned chunk on {}@{}: {}", chunkmeta, metadata.targetId, metadata.chainId);
|
||||
numIgnoredOrphanedChunks++;
|
||||
sizeOfIgnoredOrphanedChunks += chunkmeta.length;
|
||||
} else {
|
||||
XLOGF(DBG, "Found an orphaned chunk on {}@{}: {}", chunkmeta, metadata.targetId, metadata.chainId);
|
||||
numOrphanedChunks++;
|
||||
sizeOfOrphanedChunks += chunkmeta.length;
|
||||
orphanedMetadata.chunks.push_back(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!orphanedMetadata.chunks.empty()) {
|
||||
auto orphanedChunkFilePath = Path(orphanedDir) / chunkmetaFilePath.filename();
|
||||
|
||||
bool writeOk = parquetFormat
|
||||
? orphanedMetadata.dumpToParquetFile(orphanedChunkFilePath.replace_extension(".parquet"))
|
||||
: orphanedMetadata.dumpToFile(orphanedChunkFilePath, true /*jsonFormat*/);
|
||||
if (writeOk) {
|
||||
XLOGF(CRITICAL,
|
||||
"{} orphaned chunks on {}@{} saved to file: {}",
|
||||
orphanedMetadata.chunks.size(),
|
||||
orphanedMetadata.targetId,
|
||||
orphanedMetadata.chainId,
|
||||
orphanedChunkFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
XLOGF(WARN,
|
||||
"#{}/{} Processed metadata of {} chunks in file: {}",
|
||||
chunkmetaFileIndex + 1,
|
||||
chunkmetaFilePaths.size(),
|
||||
metadata.chunks.size(),
|
||||
chunkmetaFilePath);
|
||||
|
||||
numProcessedChunks += metadata.chunks.size();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
co_await loadChunkmetaDone.wait();
|
||||
executor->join();
|
||||
|
||||
XLOGF(CRITICAL,
|
||||
"In total {}/{} orphaned chunks ({:.3f} GB) ignored",
|
||||
numIgnoredOrphanedChunks.load(),
|
||||
numProcessedChunks.load(),
|
||||
double(sizeOfIgnoredOrphanedChunks.load()) / 1_GB);
|
||||
XLOGF(CRITICAL,
|
||||
"In total {}/{} orphaned chunks ({:.3f} GB) saved to directory: {}",
|
||||
numOrphanedChunks.load(),
|
||||
numProcessedChunks.load(),
|
||||
double(sizeOfOrphanedChunks.load()) / 1_GB,
|
||||
orphanedDir);
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerFindOrphanedChunksHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, findOrphanedChunks);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/FindOrphanedChunks.h
Normal file
10
src/client/cli/admin/FindOrphanedChunks.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/Path.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerFindOrphanedChunksHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
180
src/client/cli/admin/GetConfig.cc
Normal file
180
src/client/cli/admin/GetConfig.cc
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "GetConfig.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/FileUtils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
const std::map<String, flat::NodeType> typeMappings = {
|
||||
{"MGMTD", flat::NodeType::MGMTD},
|
||||
{"META", flat::NodeType::META},
|
||||
{"STORAGE", flat::NodeType::STORAGE},
|
||||
{"CLIENT", flat::NodeType::CLIENT},
|
||||
{"CLIENT_AGENT", flat::NodeType::CLIENT},
|
||||
{"FUSE", flat::NodeType::FUSE},
|
||||
};
|
||||
|
||||
const std::set<String> &typeChoices() {
|
||||
static auto choices = [] {
|
||||
std::set<String> s;
|
||||
for (const auto &[k, _] : typeMappings) {
|
||||
s.insert(k);
|
||||
}
|
||||
return s;
|
||||
}();
|
||||
return choices;
|
||||
}
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("get-config");
|
||||
parser.add_argument("-n", "--node-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("-c", "--client-id");
|
||||
parser.add_argument("-a", "--addr");
|
||||
parser.add_argument("-t", "--node-type").help(fmt::format("choices : {}", fmt::join(typeChoices(), " | ")));
|
||||
parser.add_argument("-l", "--list-versions")
|
||||
.help("list versions of all types")
|
||||
.default_value(false)
|
||||
.implicit_value(true);
|
||||
parser.add_argument("-k", "--config-key");
|
||||
parser.add_argument("-o", "--output-file").help("the path where config is saved at");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<void> handleConfigOfType(Dispatcher::OutputTable &table,
|
||||
IMgmtdClientForAdmin &client,
|
||||
flat::NodeType t,
|
||||
std::optional<String> outputFile) {
|
||||
auto configRes = co_await client.getConfig(t, flat::ConfigVersion(0));
|
||||
CO_RETURN_ON_ERROR(configRes);
|
||||
|
||||
const auto &optionalConfig = *configRes;
|
||||
table.push_back({"NodeType", toString(t)});
|
||||
if (optionalConfig) {
|
||||
table.push_back({"ConfigVersion", std::to_string(optionalConfig->configVersion)});
|
||||
table.push_back({"ConfigDesc", optionalConfig->desc});
|
||||
if (outputFile) {
|
||||
CO_RETURN_ON_ERROR(storeToFile(*outputFile, optionalConfig->content));
|
||||
table.push_back({"OutputFile", *outputFile});
|
||||
}
|
||||
|
||||
} else {
|
||||
table.push_back({"ConfigVersion", "null"});
|
||||
}
|
||||
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = parser.present<uint32_t>("-n");
|
||||
auto clientId = parser.present<String>("-c");
|
||||
auto addr = parser.present<String>("-a");
|
||||
auto type = parser.present<String>("-t");
|
||||
auto listVersions = parser.get<bool>("-l");
|
||||
auto configKey = parser.present<String>("-k").value_or("");
|
||||
auto outputFile = parser.present<String>("-o");
|
||||
|
||||
ENSURE_USAGE(nodeId.has_value() + clientId.has_value() + type.has_value() + addr.has_value() + listVersions == 1,
|
||||
"must and can only specify one of -n, -c, -t, -a, and -l");
|
||||
|
||||
if (nodeId) {
|
||||
ENSURE_USAGE(outputFile.has_value(), "must specify -o with -n");
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto node = routingInfo->getNode(flat::NodeId(*nodeId));
|
||||
if (!node) {
|
||||
co_return makeError(MgmtdCode::kNodeNotFound);
|
||||
}
|
||||
|
||||
auto addresses = node->extractAddresses("Core");
|
||||
auto res = co_await env.coreClientGetter()->getConfig(addresses, core::GetConfigReq::create(configKey));
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({"NodeId", std::to_string(*nodeId)});
|
||||
|
||||
CO_RETURN_ON_ERROR(storeToFile(*outputFile, res->config));
|
||||
table.push_back({"OutputFile", *outputFile});
|
||||
} else if (type) {
|
||||
ENSURE_USAGE(typeMappings.contains(*type), fmt::format("invalid type: {}", *type));
|
||||
ENSURE_USAGE(outputFile.has_value(), "must specify -o with -t");
|
||||
|
||||
CO_RETURN_ON_ERROR(
|
||||
co_await handleConfigOfType(table, *env.mgmtdClientGetter(), typeMappings.at(*type), outputFile));
|
||||
} else if (clientId) {
|
||||
ENSURE_USAGE(outputFile.has_value(), "must specify -o with -c");
|
||||
|
||||
auto clientSession = co_await env.mgmtdClientGetter()->getClientSession(*clientId);
|
||||
CO_RETURN_ON_ERROR(clientSession);
|
||||
|
||||
if (clientSession->session) {
|
||||
auto addresses = flat::extractAddresses(clientSession->session->serviceGroups, "Core");
|
||||
auto res = co_await env.coreClientGetter()->getConfig(addresses, core::GetConfigReq::create(configKey));
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({"ClientId", *clientId});
|
||||
|
||||
CO_RETURN_ON_ERROR(storeToFile(*outputFile, res->config));
|
||||
table.push_back({"OutputFile", *outputFile});
|
||||
} else {
|
||||
table.push_back({"Session not found"});
|
||||
table.push_back({"ClientId", *clientId});
|
||||
table.push_back({"MgmtdBootstrapping", clientSession->bootstrapping ? "Yes" : "No"});
|
||||
}
|
||||
} else if (addr) {
|
||||
ENSURE_USAGE(outputFile.has_value(), "must specify -o with -a");
|
||||
|
||||
auto addrRes = net::Address::from(*addr);
|
||||
CO_RETURN_ON_ERROR(addrRes);
|
||||
|
||||
auto res = co_await env.coreClientGetter()->getConfig(std::vector{*addrRes}, core::GetConfigReq::create(configKey));
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({"Address", *addr});
|
||||
|
||||
CO_RETURN_ON_ERROR(storeToFile(*outputFile, res->config));
|
||||
table.push_back({"OutputFile", *outputFile});
|
||||
} else {
|
||||
ENSURE_USAGE(listVersions);
|
||||
|
||||
table.push_back({"Type", "ConfigVersion"});
|
||||
|
||||
RHStringHashMap<flat::ConfigVersion> versions;
|
||||
auto res = co_await env.mgmtdClientGetter()->getConfigVersions();
|
||||
if (res) {
|
||||
versions = std::move(*res);
|
||||
} else {
|
||||
auto values = magic_enum::enum_values<flat::NodeType>();
|
||||
for (auto t : values) {
|
||||
auto configRes = co_await env.mgmtdClientGetter()->getConfig(t, flat::ConfigVersion(0));
|
||||
CO_RETURN_ON_ERROR(configRes);
|
||||
const auto &optionalConfig = *configRes;
|
||||
if (optionalConfig) {
|
||||
versions[toString(t)] = optionalConfig->configVersion;
|
||||
} else {
|
||||
versions[toString(t)] = flat::ConfigVersion(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &[k, v] : versions) {
|
||||
table.push_back({k, v == 0 ? "N/A" : std::to_string(v)});
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerGetConfigHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/GetConfig.h
Normal file
8
src/client/cli/admin/GetConfig.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerGetConfigHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
83
src/client/cli/admin/GetLastConfigUpdateRecord.cc
Normal file
83
src/client/cli/admin/GetLastConfigUpdateRecord.cc
Normal file
@@ -0,0 +1,83 @@
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "RenderConfig.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/FileUtils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("get-last-config-update-record");
|
||||
parser.add_argument("-n", "--node-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("-c", "--client-id");
|
||||
parser.add_argument("-a", "--addr");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = parser.present<uint32_t>("-n");
|
||||
auto clientId = parser.present<String>("-c");
|
||||
auto addr = parser.present<String>("-a");
|
||||
|
||||
ENSURE_USAGE(nodeId.has_value() + clientId.has_value() + addr.has_value() == 1,
|
||||
"must and can only specify one of -n, -c, and -a");
|
||||
|
||||
std::vector<net::Address> addresses;
|
||||
if (nodeId) {
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto node = routingInfo->getNode(flat::NodeId(*nodeId));
|
||||
if (!node) {
|
||||
co_return makeError(MgmtdCode::kNodeNotFound);
|
||||
}
|
||||
|
||||
addresses = node->extractAddresses("Core");
|
||||
|
||||
table.push_back({"NodeId", std::to_string(*nodeId)});
|
||||
} else if (clientId) {
|
||||
auto clientSession = co_await env.mgmtdClientGetter()->getClientSession(*clientId);
|
||||
CO_RETURN_ON_ERROR(clientSession);
|
||||
if (clientSession->session) {
|
||||
addresses = flat::extractAddresses(clientSession->session->serviceGroups, "Core");
|
||||
}
|
||||
|
||||
table.push_back({"ClientId", *clientId});
|
||||
} else if (addr) {
|
||||
auto res = net::Address::from(*addr);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
addresses.push_back(*res);
|
||||
|
||||
table.push_back({"Address", *addr});
|
||||
}
|
||||
|
||||
auto req = core::GetLastConfigUpdateRecordReq::create();
|
||||
auto res = co_await env.coreClientGetter()->getLastConfigUpdateRecord(addresses, req);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
if (res->record.has_value()) {
|
||||
const auto &record = res->record.value();
|
||||
table.push_back({"UpdateTime", record.updateTime.YmdHMS()});
|
||||
table.push_back({"Description", record.description});
|
||||
table.push_back({"Result", record.result.describe()});
|
||||
} else {
|
||||
table.push_back({"UpdateTime", "N/A"});
|
||||
table.push_back({"Description", "N/A"});
|
||||
table.push_back({"Result", "N/A"});
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerGetLastConfigUpdateRecordHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/GetLastConfigUpdateRecord.h
Normal file
8
src/client/cli/admin/GetLastConfigUpdateRecord.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerGetLastConfigUpdateRecordHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
35
src/client/cli/admin/GetRealPath.cc
Normal file
35
src/client/cli/admin/GetRealPath.cc
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "GetRealPath.h"
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("getrealpath");
|
||||
parser.add_argument("path");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleGetRealPath(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
auto path = parser.present<std::string>("path").value_or(env.currentDir);
|
||||
auto res = co_await env.metaClientGetter()->getRealPath(env.userInfo, env.currentDirId, Path(path), true);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
table.push_back({res.value().native()});
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerGetRealPathHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleGetRealPath);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/GetRealPath.h
Normal file
8
src/client/cli/admin/GetRealPath.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerGetRealPathHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
121
src/client/cli/admin/HotUpdateConfig.cc
Normal file
121
src/client/cli/admin/HotUpdateConfig.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "HotUpdateConfig.h"
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/FileUtils.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
#include "fbs/core/service/Rpc.h"
|
||||
#include "fbs/mgmtd/MgmtdTypes.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
const std::set<String> typeChoices(magic_enum::enum_names<flat::NodeType>().begin(),
|
||||
magic_enum::enum_names<flat::NodeType>().end());
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("hot-update-config");
|
||||
parser.add_argument("-n", "--node-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("-t", "--node-type")
|
||||
.help(fmt::format("choices : {}", fmt::join(typeChoices.begin(), typeChoices.end(), " | ")));
|
||||
parser.add_argument("-c", "--client-id");
|
||||
parser.add_argument("-a", "--addr");
|
||||
parser.add_argument("-s", "--string");
|
||||
parser.add_argument("-f", "--file");
|
||||
parser.add_argument("--render").default_value(false);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = parser.present<uint32_t>("-n");
|
||||
auto type = parser.present<String>("-t");
|
||||
auto clientId = parser.present<String>("-c");
|
||||
auto addr = parser.present<String>("-a");
|
||||
auto str = parser.present<String>("-s");
|
||||
auto filePath = parser.present<String>("-f");
|
||||
auto render = parser.get<bool>("--render");
|
||||
|
||||
ENSURE_USAGE(nodeId.has_value() + clientId.has_value() + addr.has_value() + type.has_value() == 1,
|
||||
"must and can only specify one of -n, -t, -c, and -a");
|
||||
ENSURE_USAGE(str.has_value() + filePath.has_value() == 1, "must and can only specify one of -s and -f");
|
||||
|
||||
auto updateString = [&]() -> Result<String> {
|
||||
if (str.has_value()) return *str;
|
||||
return loadFile(*filePath);
|
||||
}();
|
||||
CO_RETURN_ON_ERROR(updateString);
|
||||
|
||||
try {
|
||||
[[maybe_unused]] auto result = toml::parse(*updateString);
|
||||
} catch (const toml::parse_error &e) {
|
||||
std::stringstream ss;
|
||||
ss << e;
|
||||
XLOGF(ERR, "Parse config failed: {}", ss.str());
|
||||
co_return makeError(StatusCode::kConfigParseError, fmt::format("Invalid toml: {}", e.what()));
|
||||
} catch (std::exception &e) {
|
||||
co_return makeError(StatusCode::kConfigInvalidValue, fmt::format("Invalid toml: {}", e.what()));
|
||||
}
|
||||
|
||||
auto doUpdate = [&](auto addresses) -> CoTryTask<core::HotUpdateConfigRsp> {
|
||||
auto req = core::HotUpdateConfigReq::create(*updateString, render);
|
||||
co_return co_await env.coreClientGetter()->hotUpdateConfig(addresses, req);
|
||||
};
|
||||
|
||||
if (type) {
|
||||
auto ntype = magic_enum::enum_cast<flat::NodeType>(*type);
|
||||
ENSURE_USAGE(ntype, fmt::format("invalid type: {}", *type));
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto nodes = routingInfo->getNodeBy(flat::selectNodeByType(*ntype) && flat::selectActiveNode());
|
||||
for (const auto &node : nodes) {
|
||||
auto addrs = node.extractAddresses("Core");
|
||||
auto result = co_await doUpdate(addrs);
|
||||
table.push_back(
|
||||
{"NodeId", std::to_string(node.app.nodeId), result.hasError() ? result.error().describe() : "success"});
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
std::vector<net::Address> addresses;
|
||||
if (nodeId) {
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
auto node = routingInfo->getNode(flat::NodeId(*nodeId));
|
||||
if (!node) {
|
||||
co_return makeError(MgmtdCode::kNodeNotFound);
|
||||
}
|
||||
|
||||
addresses = node->extractAddresses("Core");
|
||||
table.push_back({"NodeId", std::to_string(*nodeId)});
|
||||
} else if (clientId) {
|
||||
auto clientSession = co_await env.mgmtdClientGetter()->getClientSession(*clientId);
|
||||
CO_RETURN_ON_ERROR(clientSession);
|
||||
if (clientSession->session) {
|
||||
addresses = flat::extractAddresses(clientSession->session->serviceGroups, "Core");
|
||||
}
|
||||
|
||||
table.push_back({"ClientId", *clientId});
|
||||
} else if (addr) {
|
||||
auto res = net::Address::from(*addr);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
addresses.push_back(*res);
|
||||
}
|
||||
CO_RETURN_ON_ERROR(co_await doUpdate(addresses));
|
||||
table.push_back({"Succeed"});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerHotUpdateConfigHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/HotUpdateConfig.h
Normal file
8
src/client/cli/admin/HotUpdateConfig.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerHotUpdateConfigHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
182
src/client/cli/admin/InitCluster.cc
Normal file
182
src/client/cli/admin/InitCluster.cc
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "InitCluster.h"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <folly/Conv.h>
|
||||
#include <folly/logging/xlog.h>
|
||||
#include <memory>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "Utils.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "common/kv/WithTransaction.h"
|
||||
#include "common/utils/FileUtils.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
#include "fbs/mgmtd/ChainRef.h"
|
||||
#include "fdb/FDBRetryStrategy.h"
|
||||
#include "fuse/FuseConfig.h"
|
||||
#include "meta/components/ChainAllocator.h"
|
||||
#include "meta/service/MetaServer.h"
|
||||
#include "meta/store/MetaStore.h"
|
||||
#include "mgmtd/service/MgmtdConfig.h"
|
||||
#include "mgmtd/store/MgmtdStore.h"
|
||||
#include "storage/service/StorageServer.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
kv::FDBRetryStrategy getRetryStrategy() { return kv::FDBRetryStrategy({1_s, 10, false}); }
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("init-cluster");
|
||||
parser.add_argument("chaintableid").scan<'u', uint32_t>();
|
||||
parser.add_argument("chunksize").scan<'u', uint32_t>();
|
||||
parser.add_argument("stripesize").scan<'u', uint32_t>();
|
||||
parser.add_argument("--mgmtd", "--mgmtd-config-path");
|
||||
parser.add_argument("--meta", "--meta-config-path");
|
||||
parser.add_argument("--storage", "--storage-config-path");
|
||||
parser.add_argument("--fuse", "--fuse-config-path");
|
||||
parser.add_argument("--allow-config-existed").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--skip-config-check").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<void> handleInitFileSystem(AdminEnv &env,
|
||||
const argparse::ArgumentParser &parser,
|
||||
Dispatcher::OutputTable &table) {
|
||||
auto tableId = flat::ChainTableId(parser.get<uint32_t>("chaintableid"));
|
||||
auto chunksize = parser.get<uint32_t>("chunksize");
|
||||
auto stripesize = parser.get<uint32_t>("stripesize");
|
||||
|
||||
auto chainAlloc = std::make_unique<meta::server::ChainAllocator>(nullptr);
|
||||
auto rootLayout = meta::Layout::newEmpty(tableId, chunksize, stripesize);
|
||||
auto op = meta::server::MetaStore::initFileSystem(*chainAlloc, rootLayout);
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<void> { co_return co_await op->run(txn); };
|
||||
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
if (commitRes.hasError()) {
|
||||
co_return makeError(commitRes.error().code(), fmt::format("Failed to init filesystem: {}", commitRes.error()));
|
||||
};
|
||||
|
||||
table.push_back({fmt::format("Init filesystem, root directory layout: chain table {}, chunksize {}, stripesize {}\n",
|
||||
tableId,
|
||||
chunksize,
|
||||
stripesize)});
|
||||
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<void> handleInitConfig(AdminEnv &env,
|
||||
Dispatcher::OutputTable &table,
|
||||
flat::NodeType nodeType,
|
||||
config::IConfig &&cfg [[maybe_unused]],
|
||||
mgmtd::MgmtdStore &store,
|
||||
const String &filePath,
|
||||
bool allowConfigExisted,
|
||||
bool skipConfigCheck) {
|
||||
auto configStr = loadFile(filePath);
|
||||
CO_RETURN_ON_ERROR(configStr);
|
||||
if (!skipConfigCheck) {
|
||||
auto validateRes = cfg.update(std::string_view(*configStr), /*isHotUpdate=*/false);
|
||||
if (validateRes.hasError()) {
|
||||
co_return makeError(validateRes.error().code(),
|
||||
fmt::format("Validate config for {} failed: {}", toString(nodeType), validateRes.error()));
|
||||
}
|
||||
}
|
||||
|
||||
auto handler = [&](kv::IReadWriteTransaction &txn) -> CoTryTask<flat::ConfigVersion> {
|
||||
auto configInfo = co_await store.loadLatestConfig(txn, nodeType);
|
||||
CO_RETURN_ON_ERROR(configInfo);
|
||||
flat::ConfigVersion ver(1);
|
||||
if (configInfo->has_value()) {
|
||||
if (!allowConfigExisted) {
|
||||
co_return makeError(StatusCode::kUnknown,
|
||||
fmt::format("Config for {} existed, version {}",
|
||||
toString(nodeType),
|
||||
configInfo->value().configVersion.toUnderType()));
|
||||
}
|
||||
ver = flat::ConfigVersion(configInfo->value().configVersion + 1);
|
||||
}
|
||||
auto writeRes = co_await store.storeConfig(txn, nodeType, flat::ConfigInfo::create(ver, std::move(*configStr)));
|
||||
CO_RETURN_ON_ERROR(writeRes);
|
||||
co_return ver;
|
||||
};
|
||||
|
||||
auto commitRes = co_await kv::WithTransaction(getRetryStrategy())
|
||||
.run(env.kvEngineGetter()->createReadWriteTransaction(), std::move(handler));
|
||||
if (commitRes.hasError()) {
|
||||
co_return makeError(commitRes.error().code(),
|
||||
fmt::format("Failed to set config for {}: {}", toString(nodeType), commitRes.error()));
|
||||
};
|
||||
|
||||
table.push_back({fmt::format("Init config for {} version {}", toString(nodeType), commitRes->toUnderType())});
|
||||
co_return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleInitCluster(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await handleInitFileSystem(env, parser, table));
|
||||
|
||||
auto allowConfigExisted = parser.get<bool>("--allow-config-existed");
|
||||
// TODO: consider how to check
|
||||
// auto skipConfigCheck = parser.get<bool>("skip-config-check");
|
||||
auto skipConfigCheck = true;
|
||||
mgmtd::MgmtdStore store;
|
||||
if (auto mgmtdConfigPath = parser.present<String>("--mgmtd"); mgmtdConfigPath) {
|
||||
CO_RETURN_ON_ERROR(co_await handleInitConfig(env,
|
||||
table,
|
||||
flat::NodeType::MGMTD,
|
||||
mgmtd::MgmtdConfig{},
|
||||
store,
|
||||
*mgmtdConfigPath,
|
||||
allowConfigExisted,
|
||||
skipConfigCheck));
|
||||
}
|
||||
if (auto configPath = parser.present<String>("--meta"); configPath) {
|
||||
CO_RETURN_ON_ERROR(co_await handleInitConfig(env,
|
||||
table,
|
||||
flat::NodeType::META,
|
||||
meta::server::MetaServer::Config{},
|
||||
store,
|
||||
*configPath,
|
||||
allowConfigExisted,
|
||||
skipConfigCheck));
|
||||
}
|
||||
if (auto configPath = parser.present<String>("--storage"); configPath) {
|
||||
CO_RETURN_ON_ERROR(co_await handleInitConfig(env,
|
||||
table,
|
||||
flat::NodeType::STORAGE,
|
||||
storage::StorageServer::Config{},
|
||||
store,
|
||||
*configPath,
|
||||
allowConfigExisted,
|
||||
skipConfigCheck));
|
||||
}
|
||||
if (auto configPath = parser.present<String>("--fuse"); configPath) {
|
||||
CO_RETURN_ON_ERROR(co_await handleInitConfig(env,
|
||||
table,
|
||||
flat::NodeType::FUSE,
|
||||
fuse::FuseConfig{},
|
||||
store,
|
||||
*configPath,
|
||||
allowConfigExisted,
|
||||
skipConfigCheck));
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerInitClusterHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleInitCluster);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/InitCluster.h
Normal file
8
src/client/cli/admin/InitCluster.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerInitClusterHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
67
src/client/cli/admin/Layout.h
Normal file
67
src/client/cli/admin/Layout.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <folly/Conv.h>
|
||||
#include <optional>
|
||||
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/ArgParse.h"
|
||||
#include "fbs/meta/Schema.h"
|
||||
#include "fbs/mgmtd/ChainRef.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
inline void addLayoutArguments(argparse::ArgumentParser &parser) {
|
||||
parser.add_argument("--chain-table-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("--chain-table-ver").scan<'u', uint32_t>();
|
||||
parser.add_argument("--chain-list").help("Chain IDs separated by ',', eg: 1,2,3,4,5, can use with chain-table");
|
||||
parser.add_argument("--chunk-size");
|
||||
parser.add_argument("--stripe-size").scan<'u', uint32_t>();
|
||||
}
|
||||
|
||||
inline std::optional<meta::Layout> parseLayout(const argparse::ArgumentParser &parser,
|
||||
std::optional<meta::Layout> base = {}) {
|
||||
auto chainTableId = parser.present<uint32_t>("--chain-table-id");
|
||||
auto chainTableVersion = parser.present<uint32_t>("--chain-table-ver");
|
||||
auto chainList = parser.present<std::string>("--chain-list");
|
||||
std::optional<uint32_t> chunkSize;
|
||||
if (auto l = parser.present<std::string>("--chunk-size")) {
|
||||
chunkSize = Size::from(*l).value();
|
||||
}
|
||||
auto stripeSize = parser.present<uint32_t>("--stripe-size");
|
||||
size_t args = chainTableId.has_value() + chainTableVersion.has_value() + chainList.has_value() +
|
||||
chunkSize.has_value() + stripeSize.has_value();
|
||||
if (args == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (base.has_value() && !chainList.has_value() && !chainTableId.has_value() && !chainTableVersion.has_value()) {
|
||||
base->chunkSize = chunkSize.value_or(base->chunkSize);
|
||||
base->stripeSize = stripeSize.value_or(base->stripeSize);
|
||||
return *base;
|
||||
}
|
||||
ENSURE_USAGE(chunkSize.has_value() && (stripeSize.has_value() || chainList.has_value()) &&
|
||||
(chainList.has_value() || chainTableId.has_value()));
|
||||
if (chainList.has_value()) {
|
||||
std::vector<std::string> idList;
|
||||
folly::split(',', *chainList, idList, true);
|
||||
ENSURE_USAGE(!idList.empty(), "chain-list is empty");
|
||||
ENSURE_USAGE(!stripeSize.has_value() || *stripeSize == idList.size(),
|
||||
fmt::format("chain-list and stripe-size not match, len({}) == {}, stripe {}",
|
||||
*chainList,
|
||||
idList.size(),
|
||||
*stripeSize));
|
||||
std::vector<uint32_t> chains;
|
||||
for (const auto &id : idList) {
|
||||
auto chain = folly::tryTo<uint32_t>(id);
|
||||
ENSURE_USAGE(chain.hasValue(), fmt::format("Failed to parse chain-list {}, element {}", *chainList, id));
|
||||
chains.push_back(*chain);
|
||||
}
|
||||
return meta::Layout::newChainList(flat::ChainTableId(chainTableId.value_or(0)),
|
||||
flat::ChainTableVersion(chainTableVersion.value_or(0)),
|
||||
*chunkSize,
|
||||
std::move(chains));
|
||||
} else {
|
||||
return meta::Layout::newEmpty(flat::ChainTableId(*chainTableId), *chunkSize, *stripeSize);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
70
src/client/cli/admin/List.cc
Normal file
70
src/client/cli/admin/List.cc
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <fmt/chrono.h>
|
||||
#include <string>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
#include "fmt/core.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("ls");
|
||||
parser.add_argument("-l", "--limit").scan<'i', int>();
|
||||
parser.add_argument("-s", "--status").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleList(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
ENSURE_USAGE(args.size() <= 1);
|
||||
|
||||
auto path = args.empty() ? "." : args[0];
|
||||
auto limit = parser.present<int>("-l");
|
||||
auto status = parser.get<bool>("-s");
|
||||
|
||||
String prev;
|
||||
for (;;) {
|
||||
auto res = co_await env.metaClientGetter()
|
||||
->list(env.userInfo, env.currentDirId, Path(path), prev, limit.value_or(0), status);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
const auto &lsp = res.value();
|
||||
for (size_t i = 0; i < lsp.entries.size(); ++i) {
|
||||
const auto &entry = lsp.entries.at(i);
|
||||
if (!status) {
|
||||
table.push_back({String(entry.name), String(magic_enum::enum_name(entry.type)), entry.id.toHexString()});
|
||||
} else {
|
||||
const auto &inode = lsp.inodes.at(i);
|
||||
table.push_back({String(entry.name),
|
||||
String(magic_enum::enum_name(entry.type)),
|
||||
entry.id.toHexString(),
|
||||
fmt::format("{}", inode.data().mtime),
|
||||
inode.isFile() ? fmt::format("{}", inode.asFile().getVersionedLength())
|
||||
: (inode.isDirectory() ? std::string("-") : inode.asSymlink().target.string()),
|
||||
fmt::format("{:04o}", inode.acl.perm.toUnderType())});
|
||||
}
|
||||
}
|
||||
if (limit.has_value()) {
|
||||
limit.value() -= lsp.entries.size();
|
||||
}
|
||||
if (lsp.more && limit.value_or(1) > 0) {
|
||||
prev = lsp.entries.at(lsp.entries.size() - 1).name;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerListHandler(Dispatcher &dispatcher) {
|
||||
constexpr auto usage = "Usage: ls [path] [-l limit] [-s]";
|
||||
co_return co_await dispatcher.registerHandler(usage, usage, getParser, handleList);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/List.h
Normal file
8
src/client/cli/admin/List.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
54
src/client/cli/admin/ListChainTables.cc
Normal file
54
src/client/cli/admin/ListChainTables.cc
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "ListChainTables.h"
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("list-chain-tables");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleListChainTables(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
XLOGF(DBG, "ListChainTables routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
table.push_back({"ChainTableId", "ChainTableVersion", "ChainCount", "ReplicaCount", "Desc"});
|
||||
for ([[maybe_unused]] const auto &[tableId, chainTables] : routingInfo->raw()->chainTables) {
|
||||
for ([[maybe_unused]] const auto &[tv, chainTable] : chainTables) {
|
||||
auto chainCount = chainTable.chains.size();
|
||||
if (chainCount == 0) {
|
||||
co_return makeError(StatusCode::kUnknown, fmt::format("Impossible: {} has no chains", tableId));
|
||||
}
|
||||
std::set<size_t> replicaCount;
|
||||
for (auto i = 0u; i < chainCount; ++i) {
|
||||
auto *ci = routingInfo->raw()->getChain(chainTable.chains[i]);
|
||||
if (!ci) {
|
||||
co_return makeError(MgmtdClientCode::kRoutingInfoNotReady);
|
||||
}
|
||||
replicaCount.insert(ci->targets.size());
|
||||
}
|
||||
table.push_back({std::to_string(tableId.toUnderType()),
|
||||
std::to_string(tv.toUnderType()),
|
||||
std::to_string(chainCount),
|
||||
fmt::format("{}", fmt::join(replicaCount, "/")),
|
||||
chainTable.desc});
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerListChainTablesHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleListChainTables);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListChainTables.h
Normal file
8
src/client/cli/admin/ListChainTables.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListChainTablesHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
165
src/client/cli/admin/ListChains.cc
Normal file
165
src/client/cli/admin/ListChains.cc
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "ListChains.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("list-chains");
|
||||
parser.add_argument("-t", "--table-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("-v", "--version").scan<'u', uint32_t>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
void printChain(Dispatcher::OutputRow &row, const flat::ChainInfo &ci, const flat::RoutingInfo::TargetMap &targets) {
|
||||
row.push_back(std::to_string(ci.chainVersion));
|
||||
auto statusPos = row.size();
|
||||
row.push_back("placeholder");
|
||||
|
||||
auto preferredOrder =
|
||||
transformTo<std::vector>(std::span{ci.preferredTargetOrder.begin(), ci.preferredTargetOrder.size()},
|
||||
[](auto tid) { return tid.toUnderType(); });
|
||||
row.push_back(fmt::format("[{}]", fmt::join(preferredOrder, ",")));
|
||||
|
||||
size_t serving = 0, syncing = 0;
|
||||
for (const auto &t : ci.targets) {
|
||||
const auto &ti = targets.at(t.targetId);
|
||||
row.push_back(fmt::format("{}({}-{})",
|
||||
t.targetId.toUnderType(),
|
||||
magic_enum::enum_name(ti.publicState),
|
||||
magic_enum::enum_name(ti.localState)));
|
||||
using PS = flat::PublicTargetState;
|
||||
switch (ti.publicState) {
|
||||
case PS::SERVING:
|
||||
++serving;
|
||||
break;
|
||||
case PS::SYNCING:
|
||||
++syncing;
|
||||
break;
|
||||
case PS::INVALID:
|
||||
case PS::LASTSRV:
|
||||
case PS::WAITING:
|
||||
case PS::OFFLINE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto &status = row[statusPos];
|
||||
if (syncing) {
|
||||
status = "SYNCING";
|
||||
} else if (serving == ci.targets.size()) {
|
||||
status = "SERVING";
|
||||
} else if (serving == 0) {
|
||||
status = "UNAVAILABLE";
|
||||
} else {
|
||||
status = fmt::format("SERVING({}/{})", serving, ci.targets.size());
|
||||
}
|
||||
}
|
||||
|
||||
void printChainTable(Dispatcher::OutputTable &table,
|
||||
const flat::RoutingInfo::ChainMap &chains,
|
||||
const flat::ChainTable &ct,
|
||||
const flat::RoutingInfo::TargetMap &targets) {
|
||||
for (const auto &c : ct.chains) {
|
||||
const auto &ci = chains.at(c);
|
||||
std::vector<String> v = {
|
||||
fmt::format("{}", ci.chainId.toUnderType()),
|
||||
std::to_string(ct.chainTableId.toUnderType()),
|
||||
std::to_string(ct.chainTableVersion.toUnderType()),
|
||||
};
|
||||
printChain(v, ci, targets);
|
||||
table.push_back(std::move(v));
|
||||
}
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleListChains(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto tableId = parser.present<uint32_t>("-t");
|
||||
auto tableVer = parser.present<uint32_t>("-v");
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
XLOGF(DBG, "ListChains routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
const auto &chainTables = routingInfo->raw()->chainTables;
|
||||
if (tableId) {
|
||||
auto tbid = flat::ChainTableId(*tableId);
|
||||
auto tbv = flat::ChainTableVersion(tableVer.value_or(0));
|
||||
auto *ct = routingInfo->raw()->getChainTable(tbid, tbv);
|
||||
if (!ct) co_return makeError(StatusCode::kInvalidArg, fmt::format("{} not found", tbid));
|
||||
if (ct->chains.empty()) co_return makeError(StatusCode::kUnknown, fmt::format("{}@{} has no chains", tbid, tbv));
|
||||
size_t replicaCount = 0;
|
||||
for (auto cid : ct->chains) {
|
||||
auto *ci = routingInfo->raw()->getChain(cid);
|
||||
if (!ci) co_return makeError(StatusCode::kUnknown, fmt::format("{} not found", cid));
|
||||
replicaCount = std::max(replicaCount, ci->targets.size());
|
||||
}
|
||||
|
||||
auto head =
|
||||
std::vector<String>{"ChainId", "ChainTable", "ChainTableVersion", "ChainVersion", "Status", "PreferredOrder"};
|
||||
for (size_t i = 0; i < replicaCount; ++i) {
|
||||
head.push_back("Target");
|
||||
}
|
||||
table.push_back(std::move(head));
|
||||
|
||||
printChainTable(table, routingInfo->raw()->chains, *ct, routingInfo->raw()->targets);
|
||||
} else {
|
||||
size_t maxTargets = 0;
|
||||
for ([[maybe_unused]] const auto &[_, c] : routingInfo->raw()->chains)
|
||||
maxTargets = std::max(maxTargets, c.targets.size());
|
||||
|
||||
auto head = std::vector<String>{"ChainId", "ReferencedBy", "ChainVersion", "Status", "PreferredOrder"};
|
||||
for (size_t i = 0; i < maxTargets; ++i) head.push_back("Target");
|
||||
table.push_back(std::move(head));
|
||||
|
||||
robin_hood::unordered_map<flat::ChainId, std::set<flat::ChainTableId::UnderlyingType>> refs;
|
||||
|
||||
for (const auto &[ctid, vm] : chainTables) {
|
||||
for ([[maybe_unused]] const auto &[_, ct] : vm) {
|
||||
for (const auto &cid : ct.chains) {
|
||||
refs[cid].insert(ctid.toUnderType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<flat::ChainInfo> chains;
|
||||
for (const auto &[_, ci] : routingInfo->raw()->chains) {
|
||||
chains.push_back(ci);
|
||||
}
|
||||
std::sort(chains.begin(), chains.end(), [](const auto &a, const auto &b) { return a.chainId < b.chainId; });
|
||||
|
||||
for (const auto &ci : chains) {
|
||||
auto cid = ci.chainId;
|
||||
Dispatcher::OutputRow row;
|
||||
row.push_back(std::to_string(cid));
|
||||
const auto &ts = refs[cid];
|
||||
if (ts.empty()) {
|
||||
row.push_back("N/A");
|
||||
} else if (ts.size() == 1) {
|
||||
row.push_back(std::to_string(*ts.begin()));
|
||||
} else if (ts.size() == 2) {
|
||||
row.push_back(fmt::format("{}", fmt::join(ts, ",")));
|
||||
} else {
|
||||
auto v = std::array{*ts.begin(), *++ts.begin()};
|
||||
row.push_back(fmt::format("{},...", fmt::join(v, ",")));
|
||||
}
|
||||
printChain(row, ci, routingInfo->raw()->targets);
|
||||
table.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerListChainsHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleListChains);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListChains.h
Normal file
8
src/client/cli/admin/ListChains.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListChainsHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
109
src/client/cli/admin/ListClients.cc
Normal file
109
src/client/cli/admin/ListClients.cc
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "ListClients.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("list-clients");
|
||||
return parser;
|
||||
}
|
||||
|
||||
void printClientSession(Dispatcher::OutputTable &table,
|
||||
const flat::ClientSession &session,
|
||||
const std::vector<flat::TagPair> &clientTags,
|
||||
const flat::ConfigVersion *latestConfigVersion) {
|
||||
Dispatcher::OutputRow row;
|
||||
row.push_back(session.clientId);
|
||||
row.push_back(session.clientStart.YmdHMS());
|
||||
row.push_back(session.start.YmdHMS());
|
||||
row.push_back(session.lastExtend.YmdHMS());
|
||||
|
||||
String configStatus = [&] {
|
||||
if (latestConfigVersion && session.configVersion == *latestConfigVersion) {
|
||||
switch (session.configStatus) {
|
||||
case ConfigStatus::NORMAL:
|
||||
return "UPTODATE";
|
||||
case ConfigStatus::DIRTY:
|
||||
return "DIRTY";
|
||||
case ConfigStatus::FAILED:
|
||||
return "FAILED";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}();
|
||||
|
||||
if (configStatus.empty()) {
|
||||
row.push_back(std::to_string(session.configVersion));
|
||||
} else {
|
||||
row.push_back(fmt::format("{}({})", session.configVersion.toUnderType(), configStatus));
|
||||
}
|
||||
row.push_back(session.universalId.empty() ? "N/A" : session.universalId);
|
||||
row.push_back(session.description.empty() ? "N/A" : session.description);
|
||||
row.push_back(fmt::format("[{}]", fmt::join(clientTags, ",")));
|
||||
row.push_back(fmt::format("{}", session.releaseVersion));
|
||||
|
||||
table.push_back(std::move(row));
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto res = co_await env.mgmtdClientGetter()->listClientSessions();
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({"ClientId",
|
||||
"ClientStart",
|
||||
"SessionStart",
|
||||
"LastExtend",
|
||||
"ConfigVersion",
|
||||
"Hostname",
|
||||
"Description",
|
||||
"Tags",
|
||||
"ReleaseVersion"});
|
||||
|
||||
auto configVersionsRes = co_await env.mgmtdClientGetter()->getConfigVersions();
|
||||
const auto *latestConfigVersion =
|
||||
configVersionsRes && configVersionsRes->contains("CLIENT") ? &configVersionsRes->at("CLIENT") : nullptr;
|
||||
|
||||
std::sort(res->sessions.begin(), res->sessions.end(), [](const flat::ClientSession &a, const flat::ClientSession &b) {
|
||||
if (a.universalId != b.universalId) {
|
||||
// clients without universalId have the lowest priority
|
||||
if (a.universalId.empty() || b.universalId.empty()) return b.universalId.empty();
|
||||
if (b.universalId.empty()) return true;
|
||||
return a.universalId < b.universalId;
|
||||
}
|
||||
if (a.description != b.description) {
|
||||
if (a.description.empty() || b.description.empty()) return b.description.empty();
|
||||
return a.description < b.description;
|
||||
}
|
||||
return a.clientId < b.clientId;
|
||||
});
|
||||
|
||||
for (const auto &session : res->sessions) {
|
||||
printClientSession(table, session, res->referencedTags.at(session.universalId), latestConfigVersion);
|
||||
}
|
||||
|
||||
if (res->bootstrapping) {
|
||||
// TODO: directly print via printer
|
||||
table.push_back({"Mgmtd Bootstrapping", "The client list may be incomplete"});
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerListClientsHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListClients.h
Normal file
8
src/client/cli/admin/ListClients.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListClientsHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
62
src/client/cli/admin/ListGc.cc
Normal file
62
src/client/cli/admin/ListGc.cc
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "ListGc.h"
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/Coroutine.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
#include "common/utils/String.h"
|
||||
#include "meta/components/GcManager.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("gc-list");
|
||||
parser.add_argument("-l", "--limit").default_value(256).scan<'i', int>();
|
||||
parser.add_argument("-p", "--prev");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleListGc(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
ENSURE_USAGE(args.size() <= 1);
|
||||
|
||||
auto limit = parser.get<int>("-l");
|
||||
auto prev = parser.present<std::string>("-p");
|
||||
auto path = args.empty() ? "." : args[0];
|
||||
auto res = co_await env.metaClientGetter()
|
||||
->list(env.userInfo, meta::InodeId::gcRoot(), Path(path), prev.value_or(""), limit, false);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
const auto &lsp = res.value();
|
||||
for (const auto &e : lsp.entries) {
|
||||
auto parsed = meta::server::GcManager::parseGcEntry(e.name);
|
||||
if (parsed.hasValue()) {
|
||||
table.push_back({fmt::format("{}", parsed->first),
|
||||
fmt::format("{}", parsed->second),
|
||||
e.name,
|
||||
e.id.toHexString(),
|
||||
std::string(magic_enum::enum_name(e.type))});
|
||||
} else {
|
||||
table.push_back({String(e.name), e.id.toHexString()});
|
||||
}
|
||||
}
|
||||
if (lsp.more) {
|
||||
table.push_back({"..."});
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerListGcHandler(Dispatcher &dispatcher) {
|
||||
constexpr auto usage = "Usage: gc-list [path] [-l limit] [-p prev]";
|
||||
co_return co_await dispatcher.registerHandler(usage, usage, getParser, handleListGc);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListGc.h
Normal file
8
src/client/cli/admin/ListGc.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListGcHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
93
src/client/cli/admin/ListNodes.cc
Normal file
93
src/client/cli/admin/ListNodes.cc
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "ListNodes.h"
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("list-nodes");
|
||||
return parser;
|
||||
}
|
||||
|
||||
void printNode(Dispatcher::OutputTable &table,
|
||||
const flat::NodeInfo &node,
|
||||
const RHStringHashMap<flat::ConfigVersion> *configVersions) {
|
||||
auto nodeType = toStringView(node.type);
|
||||
Dispatcher::OutputRow row;
|
||||
row.push_back(fmt::format("{}", node.app.nodeId.toUnderType()));
|
||||
row.push_back(String(nodeType));
|
||||
row.push_back(String(magic_enum::enum_name(node.status)));
|
||||
row.push_back(node.app.hostname);
|
||||
row.push_back(fmt::format("{}", node.app.pid));
|
||||
// TODO: print serviceGroups
|
||||
row.push_back(fmt::format("[{}]", fmt::join(node.tags, ",")));
|
||||
row.push_back(node.lastHeartbeatTs.toMicroseconds() ? node.lastHeartbeatTs.YmdHMS() : "N/A");
|
||||
|
||||
String configStatus = [&] {
|
||||
if (configVersions && configVersions->contains(nodeType)) {
|
||||
auto it = configVersions->find(nodeType);
|
||||
if (node.configVersion == it->second) {
|
||||
switch (node.configStatus) {
|
||||
case ConfigStatus::NORMAL:
|
||||
return "UPTODATE";
|
||||
case ConfigStatus::DIRTY:
|
||||
return "DIRTY";
|
||||
case ConfigStatus::FAILED:
|
||||
return "FAILED";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}();
|
||||
|
||||
if (configStatus.empty()) {
|
||||
row.push_back(std::to_string(node.configVersion));
|
||||
} else {
|
||||
row.push_back(fmt::format("{}({})", node.configVersion.toUnderType(), configStatus));
|
||||
}
|
||||
row.push_back(fmt::format("{}", node.app.releaseVersion));
|
||||
table.push_back(std::move(row));
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleListNodes(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto mgmtdClient = env.mgmtdClientGetter();
|
||||
CO_RETURN_ON_ERROR(co_await mgmtdClient->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = mgmtdClient->getRoutingInfo();
|
||||
XLOGF(DBG, "ListNodes routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
const auto &nodes = routingInfo->raw()->nodes;
|
||||
table.push_back(
|
||||
{"Id", "Type", "Status", "Hostname", "Pid", "Tags", "LastHeartbeatTime", "ConfigVersion", "ReleaseVersion"});
|
||||
|
||||
auto configVersionsRes = co_await mgmtdClient->getConfigVersions();
|
||||
|
||||
std::vector<flat::NodeInfo> nodeVec;
|
||||
for ([[maybe_unused]] auto &[_, node] : nodes) nodeVec.push_back(std::move(node));
|
||||
std::sort(nodeVec.begin(), nodeVec.end(), [](const flat::NodeInfo &a, const flat::NodeInfo &b) {
|
||||
if (a.type != b.type) return a.type < b.type;
|
||||
if (a.status != b.status) return a.status < b.status;
|
||||
return a.app.nodeId < b.app.nodeId;
|
||||
});
|
||||
for (const auto &nodeInfo : nodeVec) printNode(table, nodeInfo, configVersionsRes ? &*configVersionsRes : nullptr);
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerListNodesHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleListNodes);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListNodes.h
Normal file
8
src/client/cli/admin/ListNodes.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListNodesHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
104
src/client/cli/admin/ListTargets.cc
Normal file
104
src/client/cli/admin/ListTargets.cc
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "ListTargets.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("list-targets");
|
||||
parser.add_argument("-c", "--chain-id").scan<'u', uint32_t>();
|
||||
parser.add_argument("--orphan").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
Result<Void> printTargetsOfChain(Dispatcher::OutputTable &table,
|
||||
const flat::ChainInfo &ci,
|
||||
const flat::RoutingInfo::TargetMap &targets) {
|
||||
auto n = ci.targets.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto &cti = ci.targets[i];
|
||||
if (!targets.contains(cti.targetId)) {
|
||||
return makeError(StatusCode::kUnknown,
|
||||
fmt::format("{} of {} not found in RoutingInfo", cti.targetId, ci.chainId));
|
||||
}
|
||||
const auto &ti = targets.at(cti.targetId);
|
||||
auto role = i == 0 ? "HEAD" : (i == n - 1 ? "TAIL" : "MIDDLE");
|
||||
table.push_back({std::to_string(cti.targetId),
|
||||
std::to_string(ci.chainId),
|
||||
role,
|
||||
toString(ti.publicState),
|
||||
toString(ti.localState),
|
||||
ti.nodeId ? std::to_string(*ti.nodeId) : "N/A",
|
||||
ti.diskIndex ? std::to_string(*ti.diskIndex) : "N/A",
|
||||
std::to_string(ti.usedSize)});
|
||||
}
|
||||
return Void{};
|
||||
}
|
||||
|
||||
Result<Void> printOrphanTargets(Dispatcher::OutputTable &table, const std::vector<flat::TargetInfo> &targets) {
|
||||
table.push_back({"TargetId", "LocalState", "NodeId", "DiskIndex", "UsedSize"});
|
||||
for (const auto &ti : targets) {
|
||||
table.push_back({std::to_string(ti.targetId),
|
||||
toString(ti.localState),
|
||||
ti.nodeId ? std::to_string(*ti.nodeId) : "N/A",
|
||||
ti.diskIndex ? std::to_string(*ti.diskIndex) : "N/A",
|
||||
std::to_string(ti.usedSize)});
|
||||
}
|
||||
return Void{};
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleListTargets(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto orphan = parser.get<bool>("--orphan");
|
||||
if (orphan) {
|
||||
auto rsp = co_await env.mgmtdClientGetter()->listOrphanTargets();
|
||||
CO_RETURN_ON_ERROR(rsp);
|
||||
printOrphanTargets(table, rsp->targets);
|
||||
} else {
|
||||
auto chainId = parser.present<uint32_t>("-c");
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(/*force=*/true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
XLOGF(DBG, "ListTargets routingInfo:{}", serde::toJsonString(*routingInfo->raw()));
|
||||
auto head = std::vector<String>{"TargetId",
|
||||
"ChainId",
|
||||
"Role",
|
||||
"PublicState",
|
||||
"LocalState",
|
||||
"NodeId",
|
||||
"DiskIndex",
|
||||
"UsedSize"};
|
||||
table.push_back(std::move(head));
|
||||
if (chainId) {
|
||||
auto cid = flat::ChainId(*chainId);
|
||||
auto *ci = routingInfo->raw()->getChain(cid);
|
||||
if (!ci) co_return makeError(StatusCode::kInvalidArg, fmt::format("{} not found", cid));
|
||||
|
||||
CO_RETURN_ON_ERROR(printTargetsOfChain(table, *ci, routingInfo->raw()->targets));
|
||||
} else {
|
||||
const auto &chains = routingInfo->raw()->chains;
|
||||
const auto &targets = routingInfo->raw()->targets;
|
||||
for (const auto &[_, ci] : chains) {
|
||||
CO_RETURN_ON_ERROR(printTargetsOfChain(table, ci, targets));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerListTargetsHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleListTargets);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/ListTargets.h
Normal file
8
src/client/cli/admin/ListTargets.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerListTargetsHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
51
src/client/cli/admin/Mkdir.cc
Normal file
51
src/client/cli/admin/Mkdir.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "Mkdir.h"
|
||||
|
||||
#include <scn/scn.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("mkdir");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("-p", "-r", "--recursive").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--perm");
|
||||
addLayoutArguments(parser);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleMkdir(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto path = parser.get<std::string>("path");
|
||||
auto recursive = parser.get<bool>("-r");
|
||||
auto p = parser.present<String>("--perm");
|
||||
|
||||
meta::Permission perm(0755);
|
||||
if (p) {
|
||||
auto [r, v] = scn::scan_tuple<uint32_t>(*p, "{:o}");
|
||||
if (!r) co_return makeError(StatusCode::kInvalidArg, fmt::format("invalid permission format: {}", r.error().msg()));
|
||||
perm = meta::Permission(v);
|
||||
}
|
||||
|
||||
auto layout = parseLayout(parser);
|
||||
auto res =
|
||||
co_await env.metaClientGetter()->mkdirs(env.userInfo, env.currentDirId, Path(path), perm, recursive, layout);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerMkdirHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleMkdir);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/Mkdir.h
Normal file
8
src/client/cli/admin/Mkdir.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerMkdirHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
64
src/client/cli/admin/OfflineTarget.cc
Normal file
64
src/client/cli/admin/OfflineTarget.cc
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "OfflineTarget.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/RapidCsv.h"
|
||||
#include "common/utils/Result.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("offline-target");
|
||||
parser.add_argument("--node-id").scan<'u', flat::NodeId::UnderlyingType>();
|
||||
parser.add_argument("--target-id").scan<'u', flat::TargetId::UnderlyingType>().required();
|
||||
parser.add_argument("--force").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleOfflineTarget(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
storage::OfflineTargetReq req;
|
||||
req.targetId = flat::TargetId(parser.get<flat::TargetId::UnderlyingType>("--target-id"));
|
||||
req.force = parser.get<bool>("--force");
|
||||
|
||||
flat::NodeId nodeId{};
|
||||
auto nodeIdResult = parser.present<flat::NodeId::UnderlyingType>("--node-id");
|
||||
if (nodeIdResult.has_value()) {
|
||||
nodeId = flat::NodeId{nodeIdResult.value()};
|
||||
} else {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo();
|
||||
if (routingInfo == nullptr || routingInfo->raw() == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
auto targetInfo = routingInfo->getTarget(req.targetId);
|
||||
if (targetInfo.has_value() && targetInfo->nodeId.has_value()) {
|
||||
nodeId = targetInfo->nodeId.value();
|
||||
} else {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "node id is unknown");
|
||||
}
|
||||
}
|
||||
|
||||
auto client = env.storageClientGetter();
|
||||
auto res = co_await client->offlineTarget(nodeId, req);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
|
||||
table.push_back({fmt::format("Offline target {} of {} succeeded", req.targetId.toUnderType(), nodeId.toUnderType())});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
CoTryTask<void> registerOfflineTargetHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleOfflineTarget);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/OfflineTarget.h
Normal file
8
src/client/cli/admin/OfflineTarget.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerOfflineTargetHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
83
src/client/cli/admin/OpenRange.cc
Normal file
83
src/client/cli/admin/OpenRange.cc
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "OpenRange.h"
|
||||
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("open-range");
|
||||
parser.add_argument("prefix");
|
||||
parser.add_argument("inclusive_start").scan<'i', int64_t>();
|
||||
parser.add_argument("exclusive_end").scan<'i', int64_t>();
|
||||
parser.add_argument("-c", "--concurrency").default_value(1).scan<'i', int>();
|
||||
parser.add_argument("-r", "--round").default_value(1).scan<'i', int>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTask<Dispatcher::OutputTable> handleOpenSubRange(AdminEnv &env, const String &prefix, int64_t start, int64_t n) {
|
||||
Dispatcher::OutputTable table;
|
||||
for (auto i = 0; i < n; ++i) {
|
||||
auto path = fmt::format("{}{}", prefix, start + i);
|
||||
auto res = co_await env.metaClientGetter()->open(env.userInfo, env.currentDirId, Path(path), std::nullopt, 0);
|
||||
if (res.hasError()) {
|
||||
table.push_back({fmt::format("Error at {}", start + i), res.error().describe()});
|
||||
}
|
||||
}
|
||||
co_return std::move(table);
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleOpenRange(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
|
||||
auto prefix = parser.get<std::string>("prefix");
|
||||
auto inclusiveStart = parser.get<int64_t>("inclusive_start");
|
||||
auto exclusiveEnd = parser.get<int64_t>("exclusive_end");
|
||||
auto concurrency = parser.get<int>("-c");
|
||||
auto round = parser.get<int>("-r");
|
||||
|
||||
Dispatcher::OutputTable table;
|
||||
auto total = exclusiveEnd - inclusiveStart;
|
||||
auto succeeded = total * round;
|
||||
for (auto r = 0; r < round; ++r) {
|
||||
auto every = total / concurrency;
|
||||
auto remain = total % concurrency;
|
||||
|
||||
std::vector<CoTask<Dispatcher::OutputTable>> tasks;
|
||||
auto start = inclusiveStart;
|
||||
for (auto i = 0; i < remain; ++i) {
|
||||
tasks.push_back(handleOpenSubRange(env, prefix, start, every + 1));
|
||||
start += every + 1;
|
||||
}
|
||||
for (auto i = remain; i < concurrency; ++i) {
|
||||
tasks.push_back(handleOpenSubRange(env, prefix, start, every));
|
||||
start += every;
|
||||
}
|
||||
auto res = co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
|
||||
for (auto &t : res) {
|
||||
for (auto &r : t) {
|
||||
table.push_back(std::move(r));
|
||||
}
|
||||
succeeded -= t.size();
|
||||
}
|
||||
}
|
||||
table.push_back({"Succeeded", std::to_string(succeeded)});
|
||||
table.push_back({"Failed", std::to_string(total * round - succeeded)});
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerOpenRangeHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleOpenRange);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/OpenRange.h
Normal file
8
src/client/cli/admin/OpenRange.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerOpenRangeHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
66
src/client/cli/admin/ParseTargetMeta.cc
Normal file
66
src/client/cli/admin/ParseTargetMeta.cc
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "ParseTargetMeta.h"
|
||||
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <fstream>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/FileUtils.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("parse-target-meta");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("-o", "--output");
|
||||
parser.add_argument("--format").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleParseTargetMeta(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
Path src = parser.get<std::string>("path");
|
||||
auto output = parser.present<std::string>("--output");
|
||||
auto format = parser.get<bool>("--format");
|
||||
|
||||
auto readResult = loadFile(src);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
|
||||
std::map<storage::ChunkId, storage::ChunkMetadata> metas;
|
||||
CO_RETURN_AND_LOG_ON_ERROR(serde::deserialize(metas, *readResult));
|
||||
|
||||
// 5. write to file.
|
||||
std::ofstream outputFile;
|
||||
if (output.has_value()) {
|
||||
outputFile = std::ofstream{output.value()};
|
||||
if (!outputFile) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("create output file {} failed", output.value()));
|
||||
}
|
||||
}
|
||||
std::ostream &out = output.has_value() ? outputFile : std::cout;
|
||||
out << serde::toJsonString(metas, false, format) << std::endl;
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerParseTargetMetaHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleParseTargetMeta);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/ParseTargetMeta.h
Normal file
10
src/client/cli/admin/ParseTargetMeta.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerParseTargetMetaHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
46
src/client/cli/admin/PruneSession.cc
Normal file
46
src/client/cli/admin/PruneSession.cc
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "PruneSession.h"
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/app/NodeId.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "meta/components/SessionManager.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("session-prune");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handlePruneSession(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
meta::server::SessionManager::Config config;
|
||||
config.set_sync_on_prune_session(false);
|
||||
meta::server::SessionManager manager(config,
|
||||
flat::NodeId{},
|
||||
env.kvEngineGetter(),
|
||||
env.mgmtdClientGetter(),
|
||||
{} /* todo: fix */);
|
||||
|
||||
auto result = co_await manager.pruneManually();
|
||||
CO_RETURN_ON_ERROR(result);
|
||||
|
||||
table.push_back({"num sessions", fmt::format("{}", *result)});
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerPruneSessionHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handlePruneSession);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/PruneSession.h
Normal file
8
src/client/cli/admin/PruneSession.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerPruneSessionHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
128
src/client/cli/admin/QueryChunk.cc
Normal file
128
src/client/cli/admin/QueryChunk.cc
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "QueryChunk.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/RapidCsv.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("query-chunk");
|
||||
parser.add_argument("-c", "--chain-id").scan<'u', flat::ChainId::UnderlyingType>();
|
||||
parser.add_argument("--chunk").default_value(std::string{});
|
||||
parser.add_argument("--read").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--index").default_value(uint32_t{0}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--touch").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleQueryChunk(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto chunkResult = storage::ChunkId::fromString(parser.get<std::string>("--chunk"));
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chunkResult);
|
||||
auto chunk = *chunkResult;
|
||||
|
||||
storage::QueryChunkReq req;
|
||||
req.chunkId = chunk;
|
||||
auto chainIdResult = parser.present<flat::ChainId::UnderlyingType>("-c");
|
||||
if (chainIdResult.has_value()) {
|
||||
req.chainId = flat::ChainId{chainIdResult.value()};
|
||||
} else {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (routingInfo == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
auto metaChunk = meta::ChunkId::unpack(chunk.data());
|
||||
auto statResult =
|
||||
co_await env.metaClientGetter()->stat(env.userInfo, meta::InodeId(metaChunk.inode()), std::nullopt, false);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(statResult);
|
||||
meta::Inode &inode = *statResult;
|
||||
std::cout << fmt::format("stat: {}", serde::toJsonString(inode, false, true)) << std::endl;
|
||||
if (!inode.isFile()) {
|
||||
co_return makeError(StatusCode::kInvalidArg, "not a file");
|
||||
}
|
||||
|
||||
const auto &file = inode.asFile();
|
||||
auto offset = metaChunk.chunk() * file.layout.chunkSize;
|
||||
auto chainResult = file.getChainId(inode, offset, *routingInfo, 0);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chainResult);
|
||||
req.chainId = *chainResult;
|
||||
}
|
||||
|
||||
auto client = env.storageClientGetter();
|
||||
auto res = co_await client->queryChunk(req);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(res);
|
||||
std::cout << serde::toJsonString(*res, false, true) << std::endl;
|
||||
|
||||
auto chunkSize = 512_KB;
|
||||
for (auto &info : *res) {
|
||||
if (info.hasValue()) {
|
||||
chunkSize = info->meta->innerFileId.chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
auto doRead = parser.get<bool>("--read");
|
||||
if (doRead) {
|
||||
auto index = parser.get<uint32_t>("--index");
|
||||
auto rdmabufPool = net::RDMABufPool::create(64_MB, 4);
|
||||
auto buffer = co_await rdmabufPool->allocate();
|
||||
if (UNLIKELY(!buffer)) {
|
||||
XLOGF(ERR, "allocate buffer failed");
|
||||
co_return makeError(RPCCode::kRDMANoBuf);
|
||||
}
|
||||
auto readBuffer = storage::client::IOBuffer{buffer};
|
||||
std::vector<storage::client::ReadIO> batch;
|
||||
storage::client::ReadOptions readOptions;
|
||||
readOptions.set_enableChecksum(true);
|
||||
readOptions.targetSelection().set_mode(storage::client::TargetSelectionMode::ManualMode);
|
||||
readOptions.targetSelection().set_targetIndex(index);
|
||||
auto readIO = client->createReadIO(req.chainId, req.chunkId, 0, chunkSize, (uint8_t *)buffer.ptr(), &readBuffer);
|
||||
batch.push_back(std::move(readIO));
|
||||
auto result = co_await client->batchRead(batch, env.userInfo, readOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
fmt::println("result: {}", batch.front().result);
|
||||
}
|
||||
|
||||
auto doTouch = parser.get<bool>("--touch");
|
||||
if (doTouch) {
|
||||
auto rdmabufPool = net::RDMABufPool::create(64_MB, 4);
|
||||
auto buffer = co_await rdmabufPool->allocate();
|
||||
if (UNLIKELY(!buffer)) {
|
||||
XLOGF(ERR, "allocate buffer failed");
|
||||
co_return makeError(RPCCode::kRDMANoBuf);
|
||||
}
|
||||
auto writeBuffer = storage::client::IOBuffer{buffer};
|
||||
std::vector<storage::client::WriteIO> batch;
|
||||
storage::client::WriteOptions writeOptions;
|
||||
auto writeIO =
|
||||
client->createWriteIO(req.chainId, req.chunkId, 0, 0, chunkSize, (uint8_t *)buffer.ptr(), &writeBuffer);
|
||||
batch.push_back(std::move(writeIO));
|
||||
auto result = co_await client->batchWrite(batch, env.userInfo, writeOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
fmt::println("result: {}", batch.front().result);
|
||||
}
|
||||
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerQueryChunkHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleQueryChunk);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/QueryChunk.h
Normal file
10
src/client/cli/admin/QueryChunk.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerQueryChunkHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
213
src/client/cli/admin/ReadBench.cc
Normal file
213
src/client/cli/admin/ReadBench.cc
Normal file
@@ -0,0 +1,213 @@
|
||||
#include "client/cli/admin/ReadBench.h"
|
||||
|
||||
#include <boost/iostreams/device/array.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <folly/Random.h>
|
||||
#include <folly/ScopeGuard.h>
|
||||
#include <folly/executors/CPUThreadPoolExecutor.h>
|
||||
#include <folly/executors/GlobalExecutor.h>
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
#include <folly/experimental/coro/Invoke.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/admin/Layout.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "client/storage/TargetSelection.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Duration.h"
|
||||
#include "common/utils/MagicEnum.hpp"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("read-bench");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("--threads").default_value(uint32_t{16}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--coroutines").default_value(uint32_t{1}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--seconds").default_value(uint32_t{60}).scan<'u', uint32_t>();
|
||||
parser.add_argument("--write").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--bs");
|
||||
parser.add_argument("--iodepth");
|
||||
parser.add_argument("--mode");
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
Path path = parser.get<std::string>("path");
|
||||
auto threads = parser.get<uint32_t>("--threads");
|
||||
auto coroutines = parser.get<uint32_t>("--coroutines");
|
||||
auto deadline = RelativeTime::now() + 1_s * parser.get<uint32_t>("--seconds");
|
||||
auto modeStr = parser.present<std::string>("--mode").value_or("Default");
|
||||
auto mode = magic_enum::enum_cast<storage::client::TargetSelectionMode>(modeStr);
|
||||
auto isWrite = parser.get<bool>("--write");
|
||||
if (!mode) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "mode is invalid");
|
||||
}
|
||||
size_t blockSize = 4_KB;
|
||||
if (auto p = parser.present<std::string>("--bs")) {
|
||||
auto result = Size::from(*p);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
blockSize = *result;
|
||||
}
|
||||
size_t iodepth = 1024;
|
||||
if (auto p = parser.present<std::string>("--iodepth")) {
|
||||
auto result = Size::from(*p);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
iodepth = *result;
|
||||
}
|
||||
auto pool = std::make_unique<folly::CPUThreadPoolExecutor>(std::make_pair(threads, threads),
|
||||
std::make_shared<folly::NamedThreadFactory>("Pool"));
|
||||
|
||||
// 2. open files.
|
||||
std::string prev;
|
||||
std::vector<FileWrapper> files;
|
||||
while (true) {
|
||||
auto res = co_await env.metaClientGetter()->list(env.userInfo, env.currentDirId, path, prev, 0, false);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(res);
|
||||
const auto &lsp = res.value();
|
||||
for (const auto &entry : lsp.entries) {
|
||||
if (entry.isFile()) {
|
||||
auto openResult = co_await FileWrapper::openOrCreateFile(env, path / entry.name);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(openResult);
|
||||
files.push_back(std::move(*openResult));
|
||||
}
|
||||
}
|
||||
if (lsp.more) {
|
||||
prev = lsp.entries.at(lsp.entries.size() - 1).name;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. read.
|
||||
auto storageClient = env.storageClientGetter();
|
||||
std::vector<CoTryTask<Void>> total;
|
||||
std::atomic<size_t> readBytes{};
|
||||
total.reserve(coroutines);
|
||||
co_await env.mgmtdClientGetter()->refreshRoutingInfo(true);
|
||||
auto rdmabufPool = net::RDMABufPool::create(64_MB, 512);
|
||||
for (auto i = 0u; i < coroutines; ++i) {
|
||||
total.push_back(folly::coro::co_invoke([&]() -> CoTryTask<Void> {
|
||||
std::ofstream out("/dev/null");
|
||||
std::vector<storage::client::ReadIO> readIOs;
|
||||
std::vector<storage::client::WriteIO> writeIOs;
|
||||
std::vector<storage::client::IOBuffer> buffers;
|
||||
net::RDMABuf current;
|
||||
storage::client::ReadOptions readOptions;
|
||||
storage::client::WriteOptions writeOptions;
|
||||
readOptions.targetSelection().set_mode(*mode);
|
||||
readIOs.reserve(1024);
|
||||
writeIOs.reserve(1024);
|
||||
buffers.reserve(1024);
|
||||
while (RelativeTime::now() <= deadline) {
|
||||
readIOs.clear();
|
||||
writeIOs.clear();
|
||||
buffers.clear();
|
||||
current = {};
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (UNLIKELY(routingInfo == nullptr)) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < iodepth; ++i) {
|
||||
auto &file = files[folly::Random::rand32() % files.size()];
|
||||
if (UNLIKELY(file.length() == 0)) {
|
||||
continue;
|
||||
}
|
||||
auto chunkSize = file.file().layout.chunkSize;
|
||||
auto offset = folly::Random::rand32() * blockSize % file.length();
|
||||
auto chainResult = file.file().getChainId(file.inode(), offset, *routingInfo, 0);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chainResult);
|
||||
auto chunk =
|
||||
file.file().getChunkId(file.inode().id, offset).then([](auto chunk) { return storage::ChunkId(chunk); });
|
||||
CO_RETURN_AND_LOG_ON_ERROR(chunk);
|
||||
auto l = std::min(file.file().length - offset, blockSize);
|
||||
if (current.size() < l) {
|
||||
current = co_await rdmabufPool->allocate();
|
||||
if (UNLIKELY(!current)) {
|
||||
XLOGF(FATAL, "allocate buffer failed");
|
||||
}
|
||||
buffers.emplace_back(current);
|
||||
}
|
||||
if (isWrite) {
|
||||
writeIOs.push_back(storageClient->createWriteIO(*chainResult,
|
||||
*chunk,
|
||||
offset % chunkSize,
|
||||
l,
|
||||
chunkSize,
|
||||
current.ptr(),
|
||||
&buffers.back()));
|
||||
} else {
|
||||
readIOs.push_back(
|
||||
storageClient
|
||||
->createReadIO(*chainResult, *chunk, offset % chunkSize, l, current.ptr(), &buffers.back()));
|
||||
}
|
||||
current.advance(l);
|
||||
}
|
||||
|
||||
if (isWrite) {
|
||||
auto writeResult = co_await storageClient->batchWrite(writeIOs, env.userInfo, writeOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(writeResult);
|
||||
} else {
|
||||
auto readResult = co_await storageClient->batchRead(readIOs, env.userInfo, readOptions);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(readResult);
|
||||
}
|
||||
size_t succBytes = 0;
|
||||
for (auto &io : readIOs) {
|
||||
if (LIKELY(bool(io.result.lengthInfo))) {
|
||||
succBytes += *io.result.lengthInfo;
|
||||
}
|
||||
}
|
||||
for (auto &io : writeIOs) {
|
||||
if (LIKELY(bool(io.result.lengthInfo))) {
|
||||
succBytes += *io.result.lengthInfo;
|
||||
}
|
||||
}
|
||||
readBytes += succBytes;
|
||||
}
|
||||
co_return Void{};
|
||||
}));
|
||||
}
|
||||
|
||||
std::atomic<bool> stop = false;
|
||||
std::jthread t([&] {
|
||||
while (!stop) {
|
||||
std::this_thread::sleep_for(1_s);
|
||||
XLOGF(WARNING, "{}/s", Size::around(readBytes.exchange(0)));
|
||||
}
|
||||
});
|
||||
|
||||
auto results = co_await folly::coro::collectAllRange(std::move(total)).scheduleOn(pool.get());
|
||||
for (auto result : results) {
|
||||
CO_RETURN_AND_LOG_ON_ERROR(result);
|
||||
}
|
||||
|
||||
stop = true;
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerReadBenchHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/ReadBench.h
Normal file
10
src/client/cli/admin/ReadBench.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerReadBenchHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
135
src/client/cli/admin/ReadFile.cc
Normal file
135
src/client/cli/admin/ReadFile.cc
Normal file
@@ -0,0 +1,135 @@
|
||||
#include "ReadFile.h"
|
||||
|
||||
#include <folly/experimental/coro/BlockingWait.h>
|
||||
#include <fstream>
|
||||
#define OPENSSL_SUPPRESS_DEPRECATED
|
||||
#include <openssl/md5.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/admin/FileWrapper.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "client/storage/StorageClient.h"
|
||||
#include "client/storage/TargetSelection.h"
|
||||
#include "common/serde/Serde.h"
|
||||
#include "common/utils/Result.h"
|
||||
#include "fbs/meta/Common.h"
|
||||
#include "fbs/storage/Common.h"
|
||||
#include "scn/scan/scan.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("read-file");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("--stat").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--offset");
|
||||
parser.add_argument("--length");
|
||||
parser.add_argument("--mode");
|
||||
parser.add_argument("--disableChecksum").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--fillZero").default_value(false).implicit_value(true);
|
||||
parser.add_argument("--verbose").default_value(false).implicit_value(true);
|
||||
parser.add_argument("-o", "--output");
|
||||
parser.add_argument("--hex").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleReadFile(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
[[maybe_unused]] auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
// 1. parse arguments.
|
||||
Path src = parser.get<std::string>("path");
|
||||
auto stat = parser.get<bool>("--stat");
|
||||
auto enableChecksum = !parser.get<bool>("--disableChecksum");
|
||||
auto fillZero = parser.get<bool>("--fillZero");
|
||||
auto verbose = parser.get<bool>("--verbose");
|
||||
auto output = parser.present<std::string>("--output");
|
||||
auto hex = parser.get<bool>("--hex");
|
||||
|
||||
size_t offset = 0;
|
||||
if (auto o = parser.present("--offset")) {
|
||||
auto offsetResult = Size::from(*o);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(offsetResult);
|
||||
offset = *offsetResult;
|
||||
}
|
||||
std::optional<size_t> lengthIn;
|
||||
if (auto l = parser.present<std::string>("--length")) {
|
||||
auto lengthResult = Size::from(*l);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(lengthResult);
|
||||
lengthIn = *lengthResult;
|
||||
}
|
||||
storage::client::TargetSelectionMode mode = storage::client::TargetSelectionMode::Default;
|
||||
uint32_t targetIndex = 0;
|
||||
if (auto m = parser.present("--mode")) {
|
||||
auto result = magic_enum::enum_cast<storage::client::TargetSelectionMode>(*m);
|
||||
if (result) {
|
||||
mode = *result;
|
||||
} else if (auto idx = scn::scan_value<uint32_t>(*m)) {
|
||||
mode = storage::client::TargetSelectionMode::ManualMode;
|
||||
targetIndex = idx.value();
|
||||
} else {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("invalid mode", *m));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. refresh routing info.
|
||||
CO_RETURN_AND_LOG_ON_ERROR(co_await env.mgmtdClientGetter()->refreshRoutingInfo(true));
|
||||
auto routingInfo = env.mgmtdClientGetter()->getRoutingInfo()->raw();
|
||||
if (routingInfo == nullptr) {
|
||||
co_return makeError(StorageClientCode::kRoutingError, "routing info is null");
|
||||
}
|
||||
|
||||
// 3. open file.
|
||||
auto openResult = co_await FileWrapper::openOrCreateFile(env, src);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(openResult);
|
||||
auto &file = *openResult;
|
||||
|
||||
// 4. calc chain and chunk id.
|
||||
if (file.length() <= offset) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("offset {} exceed file length {}", offset, file.length()));
|
||||
}
|
||||
auto readLength = std::min(lengthIn.value_or(file.length()), file.length() - offset);
|
||||
if (stat) {
|
||||
file.showChunks(offset, readLength);
|
||||
}
|
||||
|
||||
// 5. read file.
|
||||
std::ofstream outputFile;
|
||||
if (output.has_value()) {
|
||||
outputFile = std::ofstream{output.value()};
|
||||
if (!outputFile) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("create output file {} failed", output.value()));
|
||||
}
|
||||
}
|
||||
std::ostream &out = output.has_value() ? outputFile : std::cout;
|
||||
|
||||
MD5_CTX md5;
|
||||
int md5ret = MD5_Init(&md5);
|
||||
if (UNLIKELY(md5ret != 1)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("md5 init failed: {}", md5ret));
|
||||
}
|
||||
auto writeResult =
|
||||
co_await file.readFile(out, readLength, offset, enableChecksum, hex, mode, &md5, fillZero, verbose, targetIndex);
|
||||
CO_RETURN_AND_LOG_ON_ERROR(writeResult);
|
||||
auto checksum = *writeResult;
|
||||
std::array<uint8_t, MD5_DIGEST_LENGTH> md5out{};
|
||||
md5ret = MD5_Final(md5out.data(), &md5);
|
||||
if (UNLIKELY(md5ret != 1)) {
|
||||
co_return makeError(StatusCode::kInvalidArg, fmt::format("md5 final failed: {}", md5ret));
|
||||
}
|
||||
std::cout << fmt::format("read length {}, checksum {}, md5 {:02x}\n", readLength, checksum, fmt::join(md5out, ""));
|
||||
co_return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerReadFileHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleReadFile);
|
||||
}
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
10
src/client/cli/admin/ReadFile.h
Normal file
10
src/client/cli/admin/ReadFile.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerReadFileHandler(Dispatcher &dispatcher);
|
||||
|
||||
} // namespace hf3fs::client::cli
|
||||
233
src/client/cli/admin/RecursiveChown.cc
Normal file
233
src/client/cli/admin/RecursiveChown.cc
Normal file
@@ -0,0 +1,233 @@
|
||||
#include "RecursiveChown.h"
|
||||
|
||||
#include <folly/experimental/coro/Collect.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
#define LOG_WITH_STDOUT(level, ...) \
|
||||
do { \
|
||||
auto msg = fmt::format(__VA_ARGS__); \
|
||||
XLOG(level, msg); \
|
||||
fmt::print("{}\n", msg); \
|
||||
} while (0)
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("recursive-chown");
|
||||
parser.add_argument("path");
|
||||
parser.add_argument("uid").scan<'u', uint32_t>();
|
||||
parser.add_argument("gid").scan<'u', uint32_t>();
|
||||
parser.add_argument("-t", "--threads").default_value(uint32_t(16)).scan<'u', uint32_t>();
|
||||
parser.add_argument("-c", "--concurrency").default_value(uint32_t(2048)).scan<'u', uint32_t>();
|
||||
parser.add_argument("--debug").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
struct TaskArg {
|
||||
meta::InodeId ino;
|
||||
Path path;
|
||||
};
|
||||
|
||||
class RecursiveChownTask {
|
||||
public:
|
||||
RecursiveChownTask(AdminEnv &env,
|
||||
Path path,
|
||||
uint32_t uid,
|
||||
uint32_t gid,
|
||||
uint32_t threads_num,
|
||||
uint32_t concurrency,
|
||||
bool debug)
|
||||
: env_(env),
|
||||
path_(path),
|
||||
uid_(uid),
|
||||
gid_(gid),
|
||||
exec_(threads_num),
|
||||
listSemaphore_(concurrency),
|
||||
chownSemaphore_(concurrency),
|
||||
debug_(debug) {}
|
||||
|
||||
~RecursiveChownTask() { exec_.join(); }
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> run() {
|
||||
auto res = co_await env_.metaClientGetter()->stat(env_.userInfo, env_.currentDirId, path_, false);
|
||||
CO_RETURN_ON_ERROR(res);
|
||||
auto ino = res->id;
|
||||
|
||||
++listTasks_;
|
||||
co_await listSemaphore_.co_wait();
|
||||
listDir(TaskArg{.ino = ino, .path = path_}).scheduleOn(&exec_).start();
|
||||
|
||||
int64_t listTasksSuccess = 0;
|
||||
int64_t listTasksFailed = 0;
|
||||
int64_t listTasks = 0;
|
||||
|
||||
int64_t chownTasksSuccess = 0;
|
||||
int64_t chownTasksFailed = 0;
|
||||
int64_t chownTasks = 0;
|
||||
|
||||
for (;;) {
|
||||
listTasksSuccess = listTasksSuccess_.load(std::memory_order_acquire);
|
||||
listTasksFailed = listTasksFailed_.load(std::memory_order_acquire);
|
||||
listTasks = listTasks_.load(std::memory_order_acquire);
|
||||
|
||||
chownTasksSuccess = chownTasksSuccess_.load(std::memory_order_acquire);
|
||||
chownTasksFailed = chownTasksFailed_.load(std::memory_order_acquire);
|
||||
chownTasks = chownTasks_.load(std::memory_order_acquire);
|
||||
|
||||
if (listTasks == listTasksSuccess + listTasksFailed && chownTasks == chownTasksSuccess + chownTasksFailed) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_WITH_STDOUT(INFO,
|
||||
"listTasks: total={} success={} failed={}. chownTasks: total={} success={} failed={}",
|
||||
listTasks,
|
||||
listTasksSuccess,
|
||||
listTasksFailed,
|
||||
chownTasks,
|
||||
chownTasksSuccess,
|
||||
chownTasksFailed);
|
||||
|
||||
co_await folly::coro::sleep(std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
Dispatcher::OutputTable table;
|
||||
table.push_back(
|
||||
{"listTasks", "listTasksSuccess", "listTasksFailed", "chownTasks", "chownTasksSuccess", "chownTasksFailed"});
|
||||
table.push_back({
|
||||
std::to_string(listTasks),
|
||||
std::to_string(listTasksSuccess),
|
||||
std::to_string(listTasksFailed),
|
||||
std::to_string(chownTasks),
|
||||
std::to_string(chownTasksSuccess),
|
||||
std::to_string(chownTasksFailed),
|
||||
});
|
||||
co_return table;
|
||||
}
|
||||
|
||||
CoTask<void> listDir(TaskArg arg) {
|
||||
SCOPE_EXIT { listSemaphore_.signal(); };
|
||||
std::list<TaskArg> args;
|
||||
co_await listDir(arg, args);
|
||||
co_await listDir(args);
|
||||
}
|
||||
|
||||
CoTask<void> listDir(std::list<TaskArg> &args) {
|
||||
while (!args.empty()) {
|
||||
auto &arg = args.front();
|
||||
if (listSemaphore_.try_wait()) {
|
||||
listDir(std::move(arg)).scheduleOn(&exec_).start();
|
||||
} else {
|
||||
co_await listDir(arg, args);
|
||||
}
|
||||
args.pop_front();
|
||||
}
|
||||
co_return;
|
||||
}
|
||||
|
||||
CoTask<void> listDir(const TaskArg &arg, std::list<TaskArg> &args) {
|
||||
auto metaClient = env_.metaClientGetter();
|
||||
if (debug_) {
|
||||
LOG_WITH_STDOUT(INFO, "Listing {} {}", arg.ino, arg.path);
|
||||
}
|
||||
String prev;
|
||||
while (true) {
|
||||
auto res = co_await metaClient->list(env_.userInfo, arg.ino, std::nullopt, prev, 0, false);
|
||||
if (res.hasError()) {
|
||||
LOG_WITH_STDOUT(CRITICAL, "List inode {} {} failed, {}", arg.ino, arg.path, res.error());
|
||||
++listTasksFailed_;
|
||||
co_return;
|
||||
}
|
||||
|
||||
for (const auto &entry : res->entries) {
|
||||
auto nextPath = arg.path / entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
++listTasks_;
|
||||
if (listSemaphore_.try_wait()) {
|
||||
listDir(TaskArg{.ino = entry.id, .path = nextPath}).scheduleOn(&exec_).start();
|
||||
} else {
|
||||
args.push_back(TaskArg{.ino = entry.id, .path = nextPath});
|
||||
}
|
||||
} else {
|
||||
++chownTasks_;
|
||||
co_await chownSemaphore_.co_wait();
|
||||
chown(TaskArg{.ino = entry.id, .path = nextPath}).scheduleOn(&exec_).start();
|
||||
}
|
||||
}
|
||||
if (res->more) {
|
||||
prev = res->entries.back().name;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
++listTasksSuccess_;
|
||||
co_return;
|
||||
}
|
||||
|
||||
CoTask<void> chown(TaskArg arg) {
|
||||
SCOPE_EXIT { chownSemaphore_.signal(); };
|
||||
auto metaClient = env_.metaClientGetter();
|
||||
if (debug_) {
|
||||
LOG_WITH_STDOUT(INFO, "Chowning {} {}", arg.ino, arg.path);
|
||||
}
|
||||
auto res = co_await metaClient->setPermission(env_.userInfo,
|
||||
arg.ino,
|
||||
std::nullopt,
|
||||
false,
|
||||
flat::Uid(uid_),
|
||||
flat::Gid(gid_),
|
||||
std::nullopt,
|
||||
std::nullopt);
|
||||
if (res.hasError()) {
|
||||
LOG_WITH_STDOUT(CRITICAL, "Chown inode {} {} failed, {}", arg.ino, arg.path, res.error());
|
||||
++chownTasksFailed_;
|
||||
co_return;
|
||||
}
|
||||
++chownTasksSuccess_;
|
||||
}
|
||||
|
||||
private:
|
||||
AdminEnv &env_;
|
||||
Path path_;
|
||||
uint32_t uid_;
|
||||
uint32_t gid_;
|
||||
|
||||
folly::CPUThreadPoolExecutor exec_;
|
||||
folly::fibers::Semaphore listSemaphore_;
|
||||
folly::fibers::Semaphore chownSemaphore_;
|
||||
|
||||
std::atomic<int64_t> listTasks_ = 0;
|
||||
std::atomic<int64_t> listTasksSuccess_ = 0;
|
||||
std::atomic<int64_t> listTasksFailed_ = 0;
|
||||
|
||||
std::atomic<int64_t> chownTasks_ = 0;
|
||||
std::atomic<int64_t> chownTasksSuccess_ = 0;
|
||||
std::atomic<int64_t> chownTasksFailed_ = 0;
|
||||
|
||||
bool debug_;
|
||||
};
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handleRecursiveChown(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
|
||||
Path path{parser.get<std::string>("path")};
|
||||
auto uid = parser.get<uint32_t>("uid");
|
||||
auto gid = parser.get<uint32_t>("gid");
|
||||
auto threads_num = parser.get<uint32_t>("-t");
|
||||
auto concurrency = parser.get<uint32_t>("-c");
|
||||
auto debug = parser.get<bool>("--debug");
|
||||
|
||||
auto task = std::make_unique<RecursiveChownTask>(env, path, uid, gid, threads_num, concurrency, debug);
|
||||
co_return co_await task->run();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerRecursiveChownHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handleRecursiveChown);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/RecursiveChown.h
Normal file
8
src/client/cli/admin/RecursiveChown.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerRecursiveChownHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
36
src/client/cli/admin/RefreshRoutingInfo.cc
Normal file
36
src/client/cli/admin/RefreshRoutingInfo.cc
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "RefreshRoutingInfo.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("refresh-routing-info");
|
||||
parser.add_argument("-f", "--force").default_value(false).implicit_value(true);
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto force = parser.get<bool>("-f");
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.unsafeMgmtdClientGetter()->refreshRoutingInfo(force));
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CoTryTask<void> registerRefreshRoutingInfoHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/RefreshRoutingInfo.h
Normal file
8
src/client/cli/admin/RefreshRoutingInfo.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerRefreshRoutingInfoHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
41
src/client/cli/admin/RegisterNode.cc
Normal file
41
src/client/cli/admin/RegisterNode.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "RegisterNode.h"
|
||||
|
||||
#include <folly/Conv.h>
|
||||
|
||||
#include "AdminEnv.h"
|
||||
#include "client/cli/common/Dispatcher.h"
|
||||
#include "client/cli/common/Utils.h"
|
||||
#include "common/utils/StringUtils.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
namespace {
|
||||
const std::set<String> typeChoices(magic_enum::enum_names<flat::NodeType>().begin(),
|
||||
magic_enum::enum_names<flat::NodeType>().end());
|
||||
|
||||
auto getParser() {
|
||||
argparse::ArgumentParser parser("register-node");
|
||||
parser.add_argument("nodeId").scan<'u', uint32_t>();
|
||||
parser.add_argument("type").help(fmt::format("choices : {}", fmt::join(typeChoices, " | ")));
|
||||
return parser;
|
||||
}
|
||||
|
||||
CoTryTask<Dispatcher::OutputTable> handle(IEnv &ienv,
|
||||
const argparse::ArgumentParser &parser,
|
||||
const Dispatcher::Args &args) {
|
||||
auto &env = dynamic_cast<AdminEnv &>(ienv);
|
||||
ENSURE_USAGE(args.empty());
|
||||
Dispatcher::OutputTable table;
|
||||
|
||||
auto nodeId = flat::NodeId(parser.get<uint32_t>("nodeId"));
|
||||
auto type = magic_enum::enum_cast<flat::NodeType>(parser.get<String>("type"));
|
||||
ENSURE_USAGE(type.has_value(), fmt::format("invalid type: {}", parser.get<String>("type")));
|
||||
|
||||
CO_RETURN_ON_ERROR(co_await env.mgmtdClientGetter()->registerNode(env.userInfo, nodeId, *type));
|
||||
|
||||
co_return table;
|
||||
}
|
||||
} // namespace
|
||||
CoTryTask<void> registerRegisterNodesHandler(Dispatcher &dispatcher) {
|
||||
co_return co_await dispatcher.registerHandler(getParser, handle);
|
||||
}
|
||||
} // namespace hf3fs::client::cli
|
||||
8
src/client/cli/admin/RegisterNode.h
Normal file
8
src/client/cli/admin/RegisterNode.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/utils/Coroutine.h"
|
||||
|
||||
namespace hf3fs::client::cli {
|
||||
class Dispatcher;
|
||||
CoTryTask<void> registerRegisterNodesHandler(Dispatcher &dispatcher);
|
||||
} // namespace hf3fs::client::cli
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user