Mooncake/mooncake-store/include/transfer_task.h

664 lines
19 KiB
C++

#pragma once
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <ostream>
#include <queue>
#include <stack>
#include <string>
#include <thread>
#include <vector>
#include "transfer_engine.h"
#include "types.h"
#include "replica.h"
#include "storage_backend.h"
#include "client_metric.h"
#ifdef USE_NOF
#include "spdk/spdk_wrapper.h"
#endif
namespace mooncake {
/**
* @brief Transfer strategy enumeration
*/
enum class TransferStrategy {
LOCAL_MEMCPY = 0, // Local memory copy using memcpy
TRANSFER_ENGINE = 1, // Remote transfer using transfer engine
FILE_READ = 2, // File read operation
EMPTY = 3,
SPDK_NVMF = 4 // Spdk nvmf operation
};
/**
* @brief Stream operator for TransferStrategy
*/
inline std::ostream& operator<<(std::ostream& os,
const TransferStrategy& strategy) noexcept {
switch (strategy) {
case TransferStrategy::LOCAL_MEMCPY:
return os << "LOCAL_MEMCPY";
case TransferStrategy::TRANSFER_ENGINE:
return os << "TRANSFER_ENGINE";
case TransferStrategy::SPDK_NVMF:
return os << "SPDK_NVMF";
case TransferStrategy::FILE_READ:
return os << "FILE_READ";
case TransferStrategy::EMPTY:
return os << "EMPTY";
default:
return os << "UNKNOWN";
}
}
/**
* @brief Abstract base class for operation state management
*
* This class encapsulates the common state and behavior for async transfer
* operations. Derived classes implement strategy-specific waiting logic.
*/
class OperationState {
public:
OperationState() = default;
virtual ~OperationState() = default;
// Non-copyable, non-movable
OperationState(const OperationState&) = delete;
OperationState& operator=(const OperationState&) = delete;
OperationState(OperationState&&) = delete;
OperationState& operator=(OperationState&&) = delete;
/**
* @brief Check if the operation has completed
*/
virtual bool is_completed() = 0;
/**
* @brief Get the operation result. Make sure to call is_completed() first.
*/
ErrorCode get_result() const { // lock mutex
std::lock_guard<std::mutex> lock(mutex_);
assert(result_.has_value() &&
"get_result() called on an incomplete or failed-to-set "
"operation state.");
return result_.value_or(ErrorCode::INVALID_PARAMS);
}
/**
* @brief Get the transfer strategy
*/
virtual TransferStrategy get_strategy() const = 0;
/**
* @brief Wait for the operation to complete (strategy-specific
* implementation)
*/
virtual void wait_for_completion() = 0;
protected:
std::optional<ErrorCode> result_ = std::nullopt;
mutable std::mutex mutex_;
std::condition_variable cv_;
};
/**
* @brief Operation state for local memcpy transfers
*/
class EmptyOperationState : public OperationState {
public:
bool is_completed() override { return true; }
void wait_for_completion() override {}
TransferStrategy get_strategy() const override {
return TransferStrategy::EMPTY;
}
};
/**
* @brief Operation state for local memcpy transfers
*/
class MemcpyOperationState : public OperationState {
public:
bool is_completed() override {
std::lock_guard<std::mutex> lock(mutex_);
return result_.has_value();
}
void set_completed(ErrorCode error_code) {
{
std::lock_guard<std::mutex> lock(mutex_);
assert(!result_.has_value());
result_.emplace(error_code);
}
cv_.notify_all();
}
void wait_for_completion() override {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return result_.has_value(); });
}
TransferStrategy get_strategy() const override {
return TransferStrategy::LOCAL_MEMCPY;
}
};
/**
* @brief Operation state for local memcpy transfers
*/
class SpdkNofOperationState : public OperationState {
public:
bool is_completed() override {
std::lock_guard<std::mutex> lock(mutex_);
return result_.has_value();
}
void set_completed(ErrorCode error_code) {
{
std::lock_guard<std::mutex> lock(mutex_);
assert(!result_.has_value());
result_.emplace(error_code);
}
cv_.notify_all();
}
void wait_for_completion() override {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return result_.has_value(); });
}
TransferStrategy get_strategy() const override {
return TransferStrategy::SPDK_NVMF;
}
};
class FilereadOperationState : public OperationState {
public:
bool is_completed() override {
std::lock_guard<std::mutex> lock(mutex_);
return result_.has_value();
}
void set_completed(ErrorCode error_code) {
{
std::lock_guard<std::mutex> lock(mutex_);
assert(!result_.has_value());
result_.emplace(error_code);
}
cv_.notify_all();
}
void wait_for_completion() override {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return result_.has_value(); });
}
TransferStrategy get_strategy() const override {
return TransferStrategy::FILE_READ;
}
};
/**
* @brief Operation state for transfer engine operations
*/
class TransferEngineOperationState : public OperationState {
public:
TransferEngineOperationState(TransferEngine& engine, BatchID batch_id,
size_t batch_size)
: engine_(engine),
batch_id_(batch_id),
batch_size_(batch_size),
start_ts_(getCurrentTimeInMilli()) {}
~TransferEngineOperationState() { engine_.freeBatchID(batch_id_); }
bool is_completed() override;
void wait_for_completion() override;
TransferStrategy get_strategy() const override {
return TransferStrategy::TRANSFER_ENGINE;
}
private:
/**
* @brief Check the current completion status of the task, make sure to lock
* the mutex before calling this function.
* Updates the internal state and returns true if the task is completed.
*/
void check_task_status();
void set_result_internal(ErrorCode error_code);
TransferEngine& engine_;
BatchID batch_id_;
size_t batch_size_;
const int64_t start_ts_;
};
/**
* @brief Represents the future result of an asynchronous transfer operation
*
* This class provides a std::future-like interface for asynchronous transfer
* operations. Users can check completion status, wait for results, or get the
* final error code.
*/
class TransferFuture {
public:
explicit TransferFuture(std::shared_ptr<OperationState> state);
// Non-copyable but movable
TransferFuture(const TransferFuture&) = delete;
TransferFuture& operator=(const TransferFuture&) = delete;
TransferFuture(TransferFuture&&) = default;
TransferFuture& operator=(TransferFuture&&) = default;
/**
* @brief Check if the operation has completed (non-blocking)
* @return true if the operation is finished, false otherwise
*/
bool isReady() const;
/**
* @brief Wait for the operation to complete (blocking)
* @return ErrorCode indicating success or failure
*/
ErrorCode wait();
/**
* @brief Get the result, waiting if necessary (blocking)
* @return ErrorCode indicating success or failure
*/
ErrorCode get();
/**
* @brief Get the transfer strategy used by this operation
* @return TransferStrategy enum value
*/
TransferStrategy strategy() const;
private:
std::shared_ptr<OperationState> state_;
};
/**
* @brief Memory copy operation descriptor
*/
struct MemcpyOperation {
void* dest;
const void* src;
size_t size;
MemcpyOperation(void* d, const void* s, size_t sz)
: dest(d), src(s), size(sz) {}
};
/**
* @brief Memcpy task for async execution
*/
struct MemcpyTask {
std::vector<MemcpyOperation> operations;
std::shared_ptr<MemcpyOperationState> state;
MemcpyTask(std::vector<MemcpyOperation> ops,
std::shared_ptr<MemcpyOperationState> s)
: operations(std::move(ops)), state(std::move(s)) {}
};
/**
* @brief Thread pool for asynchronous memcpy operations
*
* This class manages a single worker thread that executes memcpy operations
* asynchronously.
*/
class MemcpyWorkerPool {
public:
explicit MemcpyWorkerPool();
~MemcpyWorkerPool();
// Non-copyable, non-movable
MemcpyWorkerPool(const MemcpyWorkerPool&) = delete;
MemcpyWorkerPool& operator=(const MemcpyWorkerPool&) = delete;
MemcpyWorkerPool(MemcpyWorkerPool&&) = delete;
MemcpyWorkerPool& operator=(MemcpyWorkerPool&&) = delete;
/**
* @brief Submit a memcpy task for async execution
* @param task The memcpy task to execute
*/
void submitTask(MemcpyTask task);
private:
void workerThread();
std::vector<std::thread> workers_;
std::queue<MemcpyTask> task_queue_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::atomic<bool> shutdown_;
};
#ifdef USE_NOF
// struct SpdkNofSubTask;
struct SpdkNofQos;
/**
* @brief Spdk nvmf operation descriptor
*/
struct SpdkNofTask {
nof_seg_handle* seg_handle;
void* ptr;
uint64_t lba;
uint32_t lba_count;
int remaining_lba;
int outstanding_sub_io;
int op; // kSpdkNofOpRead or kSpdkNofOpWrite
int idx; // subop idx
bool failed;
bool on_chain;
std::shared_ptr<SpdkNofOperationState> state;
int64_t* io_count;
SpdkNofQos* nof_qos;
SpdkNofTask* nxt;
SpdkNofTask(nof_seg_handle* handle, void* buf, uint64_t off, uint32_t len,
int op_code, std::shared_ptr<SpdkNofOperationState> s)
: seg_handle(handle),
ptr(buf),
lba(off),
lba_count(len),
remaining_lba(lba_count),
outstanding_sub_io(0),
op(op_code),
idx(0),
failed(false),
on_chain(false),
state(std::move(s)),
io_count(nullptr),
nof_qos(nullptr),
nxt(nullptr) {}
};
struct SpdkNofSubTask {
SpdkNofTask* task;
int submit_lba_count;
std::stack<SpdkNofSubTask*>* sub_task_pool;
};
constexpr int kDefaultSpdkNofSubmitChunkBytes = (1 << 17); // 128k
constexpr int kDefaultSpdkNofInflightBytesLimit = (1 << 25); // 32M
struct SpdkNofQos {
int inflight_blocks[kSpdkNofOpNum];
int blocks_per_chunk;
int inflight_blocks_limit;
SpdkNofTask* head[kSpdkNofOpNum];
SpdkNofTask* tail[kSpdkNofOpNum];
explicit SpdkNofQos(uint32_t block_size);
bool Empty() const {
return (head[kSpdkNofOpRead] == nullptr &&
head[kSpdkNofOpWrite] == nullptr);
}
void PushTask(SpdkNofTask* task) {
int op = task->op;
if (head[op] == nullptr) {
head[op] = task;
tail[op] = task;
} else {
tail[op]->nxt = task;
tail[op] = task;
}
}
void PopTask(int op) {
if (head[op]) {
head[op] = head[op]->nxt;
}
}
};
/**
* @brief Thread pool for asynchronous spdk nvmf operations
*
* This class manages multiple worker thread that executes spdk nvmf operations
* asynchronously.
*/
constexpr int kDefaultSpdkNofWorkers = 4;
class SpdkNofWorkerPool {
public:
explicit SpdkNofWorkerPool(int numa_socket_id = 0);
~SpdkNofWorkerPool();
// Non-copyable, non-movable
SpdkNofWorkerPool(const SpdkNofWorkerPool&) = delete;
SpdkNofWorkerPool& operator=(const SpdkNofWorkerPool&) = delete;
SpdkNofWorkerPool(SpdkNofWorkerPool&&) = delete;
SpdkNofWorkerPool& operator=(SpdkNofWorkerPool&&) = delete;
/**
* @brief Submit a spdk nvmf task for async execution
* @param task The spdk nvmf task to execute
*/
void submitTask(SpdkNofTask task);
private:
void workerThread(int work_idx);
int worker_count_;
int numa_socket_id_;
std::vector<std::thread> workers_;
std::unique_ptr<std::queue<SpdkNofTask>[]> task_queue_;
std::unique_ptr<std::mutex[]> queue_mutex_;
std::unique_ptr<std::condition_variable[]> queue_cv_;
std::atomic<bool> shutdown_;
std::mutex seg_mutex_;
int seg_num = 0;
std::map<nof_seg_handle*, int> seg_to_worker_;
};
#endif
/**
* @brief Fileread task for async execution
*/
struct FilereadTask {
std::string file_path;
size_t object_size;
std::vector<Slice> slices;
std::shared_ptr<FilereadOperationState> state;
FilereadTask(const std::string& path, size_t size,
const std::vector<Slice>& slices_ref,
std::shared_ptr<FilereadOperationState> s)
: file_path(path),
object_size(size),
slices(slices_ref),
state(std::move(s)) {}
};
/**
* @brief Thread pool for asynchronous memcpy operations
*
* This class manages a single worker thread that executes memcpy operations
* asynchronously.
*/
class FilereadWorkerPool {
public:
explicit FilereadWorkerPool(std::shared_ptr<StorageBackend>& backend);
~FilereadWorkerPool();
// Non-copyable, non-movable
FilereadWorkerPool(const FilereadWorkerPool&) = delete;
FilereadWorkerPool& operator=(const FilereadWorkerPool&) = delete;
FilereadWorkerPool(FilereadWorkerPool&&) = delete;
FilereadWorkerPool& operator=(FilereadWorkerPool&&) = delete;
/**
* @brief Submit a memcpy task for async execution
* @param task The memcpy task to execute
*/
void submitTask(FilereadTask task);
private:
void workerThread();
std::vector<std::thread> workers_;
std::queue<FilereadTask> task_queue_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::atomic<bool> shutdown_;
std::shared_ptr<StorageBackend> backend_;
};
/**
* @brief Submitter class for asynchronous transfer operations
*
* This class analyzes transfer requirements, selects optimal strategies, and
* immediately submits operations returning TransferFuture objects for result
* tracking.
*/
class TransferSubmitter {
public:
explicit TransferSubmitter(TransferEngine& engine,
std::shared_ptr<StorageBackend>& backend,
const std::string& local_hostname,
TransferMetric* transfer_metric = nullptr,
int numa_socket_id = 0);
/**
* @brief Submit an asynchronous transfer operation
*
* Analyzes the transfer requirements, selects the optimal strategy,
* and immediately submits the operation. Returns a TransferFuture
* that can be used to track completion and get results.
*
* @param handles Buffer descriptors for the transfer
* @param slices Memory slices for the transfer
* @param op_code Transfer operation (READ/WRITE)
* @return TransferFuture representing the async operation, or nullopt on
* failure
*/
std::optional<TransferFuture> submit(const Replica::Descriptor& replica,
std::vector<Slice>& slices,
TransferRequest::OpCode op_code,
void* ptr = nullptr, size_t size = 0);
/**
* @brief Submit a range read: read [src_offset, src_offset+size) from
* object into slice.ptr. Slices must total exactly `size` bytes.
*/
std::optional<TransferFuture> submitRangeRead(
const Replica::Descriptor& replica, std::vector<Slice>& slices,
uint64_t src_offset);
std::optional<TransferFuture> submit_batch(
const std::vector<Replica::Descriptor>& replicas,
std::vector<std::vector<Slice>>& all_slices,
TransferRequest::OpCode op_code);
std::optional<TransferFuture> submit_batch_get_offload_object(
const std::string& transfer_engine_addr,
const std::vector<std::string>& keys,
const std::vector<uint64_t>& pointers,
const std::unordered_map<std::string, std::vector<Slice>>&
batched_slices);
/**
* @brief Pure comparison helper: returns true iff both endpoints are
* non-empty and identical. Exposed for unit testing of the locality
* decision without instantiating a full TransferEngine.
*
* Two endpoints identify the same process only when their ip:port (or
* full hostname) match exactly; same-host different-process pairs share
* an IP but not a port and must NOT be treated as locally addressable.
*/
static bool isSameProcessEndpoint(const std::string& handle_endpoint,
const std::string& local_endpoint);
private:
TransferEngine& engine_;
// Cached at construction: the local transport endpoint never changes for
// the lifetime of the TransferSubmitter, so we avoid calling
// engine_.getLocalIpAndPort() (which allocates a string) on every transfer.
const std::string local_endpoint_;
std::unique_ptr<MemcpyWorkerPool> memcpy_pool_;
#ifdef USE_NOF
std::unique_ptr<SpdkNofWorkerPool> spdk_nvmf_pool_;
#endif
std::unique_ptr<FilereadWorkerPool> fileread_pool_;
bool memcpy_enabled_;
const std::string local_hostname_;
TransferMetric* transfer_metric_;
/**
* @brief Select the optimal transfer strategy
*/
TransferStrategy selectStrategy(const AllocatedBuffer::Descriptor& handle,
const std::vector<Slice>& slices) const;
/**
* @brief Check if all handles refer to local segments
*/
bool isLocalTransfer(const AllocatedBuffer::Descriptor& handle) const;
/**
* @brief Validate transfer parameters
*/
bool validateTransferParams(const AllocatedBuffer::Descriptor& handle,
const std::vector<Slice>& slices) const;
/**
* @brief Submit memcpy operation asynchronously
*/
std::optional<TransferFuture> submitMemcpyOperation(
const AllocatedBuffer::Descriptor& handle,
const std::vector<Slice>& slices, const TransferRequest::OpCode op_code,
uint64_t src_offset = 0);
#ifdef USE_NOF
/**
* @brief Submit SPDK NVMe-oF operation asynchronously
*/
std::optional<TransferFuture> submitSpdkNofOperation(
const AllocatedBuffer::Descriptor& handle, void* ptr, size_t size,
const TransferRequest::OpCode op_code);
#endif
/**
* @brief Submit transfer engine operation asynchronously
* @param src_offset Optional offset in source buffer (default 0)
*/
std::optional<TransferFuture> submitTransferEngineOperation(
const AllocatedBuffer::Descriptor& handle,
const std::vector<Slice>& slices, const TransferRequest::OpCode op_code,
uint64_t src_offset = 0);
std::optional<TransferFuture> submitMemoryReadOperation(
const AllocatedBuffer::Descriptor& handle,
const std::vector<Slice>& slices, uint64_t src_offset);
std::optional<TransferFuture> submitFileReadOperation(
const Replica::Descriptor& replica, std::vector<Slice>& slices,
TransferRequest::OpCode op_code);
/**
* @brief Calculate total bytes for transfer operation and update metrics
*/
void updateTransferMetrics(const std::vector<Slice>& slices,
TransferRequest::OpCode op);
std::optional<TransferFuture> submitTransfer(
std::vector<TransferRequest>& requests);
};
} // namespace mooncake