Files
2026-07-13 12:24:33 +08:00

658 lines
21 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "connector_interface.h"
#include "connector_types.h"
#include "event_notifier.h"
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <condition_variable>
#include <cstdio>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <string>
#include <thread>
#include <tuple>
#include <unordered_map>
#include <vector>
namespace lmcache {
namespace connector {
struct WorkerPoolConfig {
// Maps lane key (e.g. "lookup", "retrieve", "store") to worker count.
// Lane keys not present in this map use the shared num_workers_ pool.
std::unordered_map<std::string, int> per_op_workers;
};
/*
this base needs to have at least four methods be overridden by the derived
class:
- 1. create_connection() e.g. construct TCP socket or RDMA registration
- 2. do_single_get()
- 3. do_single_set()
- 4. do_single_exists()
optionally override do_single_delete() to support eviction (default returns
false for all keys).
see the RedisConnector (csrc/redis/) implementing the RESP2 protocol over TCP
for an example
*/
template <typename ConnectionType>
class ConnectorBase : public IStorageConnector {
public:
ConnectorBase(int num_workers)
: ConnectorBase(num_workers, WorkerPoolConfig{}) {}
ConnectorBase(int num_workers, WorkerPoolConfig worker_pool_config)
: num_workers_(num_workers), worker_pool_config_(worker_pool_config) {
if (num_workers_ <= 0) {
throw std::runtime_error("num_workers must be > 0");
}
for (auto& [key, count] : worker_pool_config_.per_op_workers) {
if (count <= 0) {
throw std::runtime_error("per_op_workers count for '" + key +
"' must be positive");
}
if (lane_registry().find(key) == lane_registry().end()) {
throw std::runtime_error("unknown lane key in per_op_workers: '" + key +
"'");
}
}
// Cross-platform poll-able wakeup fd (eventfd on Linux,
// self-pipe on macOS). See event_notifier.h.
notifier_ = make_event_notifier();
}
virtual ~ConnectorBase() { close(); }
ConnectorBase(const ConnectorBase&) = delete;
ConnectorBase& operator=(const ConnectorBase&) = delete;
int event_fd() const override { return notifier_->fileno(); }
uint64_t submit_batch_get(const std::vector<std::string>& keys,
const std::vector<void*>& bufs,
const std::vector<size_t>& lens,
size_t batch_chunk_num_bytes) override {
validate_batch_inputs(keys, bufs, lens);
size_t num_items = keys.size();
auto [batch_future_id, batch_state, num_tiles, tile_size] =
prepare_batch_operation(num_items, Op::BATCH_TILE_GET);
// pre-allocate per-key results for load error tolerance
batch_state->per_key_results.assign(num_items, 0);
// fan out work to threads
for (size_t tile_idx = 0; tile_idx < num_tiles; ++tile_idx) {
auto tile_req = create_tile_request(
keys, bufs, lens, tile_idx, tile_size, num_items, batch_future_id,
batch_state, Op::BATCH_TILE_GET, batch_chunk_num_bytes);
enqueue_request(std::move(tile_req));
}
return batch_future_id;
}
uint64_t submit_batch_set(const std::vector<std::string>& keys,
const std::vector<void*>& bufs,
const std::vector<size_t>& lens,
size_t batch_chunk_num_bytes) override {
validate_batch_inputs(keys, bufs, lens);
size_t num_items = keys.size();
auto [batch_future_id, batch_state, num_tiles, tile_size] =
prepare_batch_operation(num_items, Op::BATCH_TILE_SET);
// fan out work to threads
for (size_t tile_idx = 0; tile_idx < num_tiles; ++tile_idx) {
auto tile_req = create_tile_request(
keys, bufs, lens, tile_idx, tile_size, num_items, batch_future_id,
batch_state, Op::BATCH_TILE_SET, batch_chunk_num_bytes);
enqueue_request(std::move(tile_req));
}
return batch_future_id;
}
uint64_t submit_batch_exists(const std::vector<std::string>& keys) override {
if (keys.empty()) {
throw std::runtime_error("keys list is empty");
}
size_t num_items = keys.size();
auto [batch_future_id, batch_state, num_tiles, tile_size] =
prepare_batch_operation(num_items, Op::BATCH_TILE_EXISTS);
// pre-allocate results vector with correct size
batch_state->per_key_results.assign(num_items, 0);
// fan out work to threads
for (size_t tile_idx = 0; tile_idx < num_tiles; ++tile_idx) {
size_t start = tile_idx * tile_size;
size_t end = std::min(start + tile_size, num_items);
Request tile_req;
tile_req.op = Op::BATCH_TILE_EXISTS;
tile_req.future_id = batch_future_id;
tile_req.batch = batch_state;
tile_req.start_idx = start;
for (size_t i = start; i < end; ++i) {
tile_req.keys.push_back(keys[i]);
}
enqueue_request(std::move(tile_req));
}
return batch_future_id;
}
uint64_t submit_batch_delete(const std::vector<std::string>& keys) override {
if (keys.empty()) {
throw std::runtime_error("keys list is empty");
}
size_t num_items = keys.size();
auto [batch_future_id, batch_state, num_tiles, tile_size] =
prepare_batch_operation(num_items, Op::BATCH_TILE_DELETE);
// pre-allocate per-key results (1 = deleted, 0 = not found)
batch_state->per_key_results.assign(num_items, 0);
// fan out work to threads
for (size_t tile_idx = 0; tile_idx < num_tiles; ++tile_idx) {
size_t start = tile_idx * tile_size;
size_t end = std::min(start + tile_size, num_items);
Request tile_req;
tile_req.op = Op::BATCH_TILE_DELETE;
tile_req.future_id = batch_future_id;
tile_req.batch = batch_state;
tile_req.start_idx = start;
for (size_t i = start; i < end; ++i) {
tile_req.keys.push_back(keys[i]);
}
enqueue_request(std::move(tile_req));
}
return batch_future_id;
}
std::vector<Completion> drain_completions() override {
// Drain the eventfd that triggered this drain_completions callback
drain_eventfd_();
std::vector<Completion> completions_list;
for (;;) {
Completion c;
{
std::lock_guard<std::mutex> lk(comp_mu_);
if (completions_.empty()) {
signaled_.store(false, std::memory_order_release);
if (!completions_.empty() &&
!signaled_.exchange(true, std::memory_order_acq_rel)) {
notifier_->notify();
}
break;
}
c = std::move(completions_.front());
completions_.pop();
}
completions_list.push_back(std::move(c));
}
return completions_list;
}
void close() override {
if (closed_.exchange(true, std::memory_order_acq_rel)) {
return; // Already closed
}
// Signal all worker threads to stop
stop_.store(true, std::memory_order_release);
req_cv_.notify_all();
for (auto& [key, lane] : lanes_) {
lane->cv.notify_all();
}
// Shutdown all connections (derived class specific)
shutdown_connections();
// Join all worker threads
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
for (auto& [key, lane] : lanes_) {
join_lane_workers(*lane);
}
// Derived cleanup that must only run after workers have stopped.
on_workers_stopped();
// Close wakeup fd
if (notifier_) {
notifier_->close();
}
// Clear queues (no GIL needed - python guarantees buffers stay alive)
{
std::lock_guard<std::mutex> lk(req_mu_);
while (!requests_.empty()) {
requests_.pop();
}
}
for (auto& [key, lane] : lanes_) {
clear_lane_queue(*lane);
}
{
std::lock_guard<std::mutex> lk(comp_mu_);
while (!completions_.empty()) {
completions_.pop();
}
}
}
protected:
// call this at the END of your derived class constructor
void start_workers() {
// Start dedicated per-op lanes for configured keys.
for (auto& [key, count] : worker_pool_config_.per_op_workers) {
auto& lane_ptr = lanes_[key];
if (!lane_ptr) {
lane_ptr = std::make_unique<WorkerLane>();
}
start_lane_workers(*lane_ptr, count);
}
// Start shared pool if any lane key in the registry is not configured.
bool need_shared = false;
for (auto& [key, ops] : lane_registry()) {
if (worker_pool_config_.per_op_workers.find(key) ==
worker_pool_config_.per_op_workers.end()) {
need_shared = true;
break;
}
}
if (need_shared) {
workers_.reserve(static_cast<size_t>(num_workers_));
for (int i = 0; i < num_workers_; i++) {
workers_.emplace_back([this]() { this->worker_loop(); });
}
}
}
virtual ConnectionType create_connection() = 0;
virtual void do_single_get(ConnectionType& conn, const std::string& key,
void* buf, size_t len, size_t chunk_size) = 0;
virtual void do_single_set(ConnectionType& conn, const std::string& key,
const void* buf, size_t len,
size_t chunk_size) = 0;
virtual bool do_single_exists(ConnectionType& conn,
const std::string& key) = 0;
virtual bool do_single_delete(ConnectionType& conn, const std::string& key) {
(void)conn;
(void)key;
return false; // no-op default for backward compat with plugins
}
virtual size_t choose_num_tiles(Op op, size_t num_items) const {
return std::min<size_t>(worker_count_for_op(op), num_items);
}
virtual void do_batch_get(ConnectionType& conn, const Request& req) {
for (size_t i = 0; i < req.keys.size(); ++i) {
try {
do_single_get(conn, req.keys[i], req.buf_ptrs[i], req.buf_lens[i],
req.batch_chunk_num_bytes);
req.batch->per_key_results[req.start_idx + i] = 1;
} catch (const std::exception& e) {
req.batch->per_key_results[req.start_idx + i] = 0;
fprintf(stderr, "[LMCache GET] key %s failed: %s\n",
req.keys[i].c_str(), e.what());
}
}
}
virtual void do_batch_set(ConnectionType& conn, const Request& req) {
for (size_t i = 0; i < req.keys.size(); ++i) {
do_single_set(conn, req.keys[i], req.buf_ptrs[i], req.buf_lens[i],
req.batch_chunk_num_bytes);
}
}
virtual void do_batch_exists(ConnectionType& conn, const Request& req) {
for (size_t i = 0; i < req.keys.size(); ++i) {
bool exists = do_single_exists(conn, req.keys[i]);
req.batch->per_key_results[req.start_idx + i] = exists ? 1 : 0;
}
}
virtual void do_batch_delete(ConnectionType& conn, const Request& req) {
for (size_t i = 0; i < req.keys.size(); ++i) {
try {
bool deleted = do_single_delete(conn, req.keys[i]);
req.batch->per_key_results[req.start_idx + i] = deleted ? 1 : 0;
} catch (const std::exception& e) {
req.batch->per_key_results[req.start_idx + i] = 0;
fprintf(stderr, "[LMCache DELETE] key %s failed: %s\n",
req.keys[i].c_str(), e.what());
}
}
}
virtual void shutdown_connections() {}
virtual void on_workers_stopped() {}
bool is_stopping() const { return stop_.load(std::memory_order_acquire); }
int worker_count_for_op(Op op) const {
const std::string& key = lane_key_for_op(op);
auto it = worker_pool_config_.per_op_workers.find(key);
if (it != worker_pool_config_.per_op_workers.end()) {
return it->second;
}
return num_workers_;
}
private:
struct WorkerLane {
std::mutex mu;
std::condition_variable cv;
std::queue<Request> requests;
std::vector<std::thread> workers;
};
void validate_batch_inputs(const std::vector<std::string>& keys,
const std::vector<void*>& bufs,
const std::vector<size_t>& lens) {
if (keys.size() != bufs.size() || keys.size() != lens.size()) {
throw std::runtime_error("keys, bufs, and lens size mismatch");
}
if (keys.empty()) {
throw std::runtime_error("keys list is empty");
}
}
// returns: (batch_future_id, batch_state, num_tiles, tile_size)
std::tuple<uint64_t, std::shared_ptr<BatchState>, size_t, size_t>
prepare_batch_operation(size_t num_items, Op op) {
size_t num_tiles = choose_num_tiles(op, num_items);
if (num_tiles == 0 || num_tiles > num_items) {
throw std::runtime_error(
"choose_num_tiles must return a value in [1, num_items]");
}
size_t tile_size = (num_items + num_tiles - 1) / num_tiles; // round up
// create shared batch state
uint64_t batch_future_id =
next_future_id_.fetch_add(1, std::memory_order_relaxed);
auto batch_state = std::make_shared<BatchState>();
batch_state->remaining_tiles.store(num_tiles, std::memory_order_relaxed);
batch_state->batch_op = op;
return {batch_future_id, batch_state, num_tiles, tile_size};
}
Request create_tile_request(const std::vector<std::string>& keys,
const std::vector<void*>& bufs,
const std::vector<size_t>& lens, size_t tile_idx,
size_t tile_size, size_t num_items,
uint64_t batch_future_id,
std::shared_ptr<BatchState> batch_state, Op op,
size_t batch_chunk_num_bytes) {
size_t start = tile_idx * tile_size;
size_t end = std::min(start + tile_size, num_items); // clip last tile
Request tile_req;
tile_req.op = op;
tile_req.future_id = batch_future_id;
tile_req.batch = batch_state;
tile_req.batch_chunk_num_bytes = batch_chunk_num_bytes;
tile_req.start_idx = start;
for (size_t i = start; i < end; ++i) {
tile_req.keys.push_back(keys[i]);
tile_req.buf_ptrs.push_back(bufs[i]);
tile_req.buf_lens.push_back(lens[i]);
}
return tile_req;
}
void enqueue_request(Request&& req) {
const std::string& key = lane_key_for_op(req.op);
auto it = lanes_.find(key);
if (it != lanes_.end()) {
{
std::lock_guard<std::mutex> lk(it->second->mu);
it->second->requests.push(std::move(req));
}
it->second->cv.notify_one();
return;
}
{
std::lock_guard<std::mutex> lk(req_mu_);
requests_.push(std::move(req));
}
req_cv_.notify_one();
}
void push_completion(Completion&& c) {
{
std::lock_guard<std::mutex> lk(comp_mu_);
completions_.push(std::move(c));
}
signal_eventfd_();
}
void drain_eventfd_() { notifier_->consume(); }
void signal_eventfd_() {
bool already_signaled = signaled_.exchange(true, std::memory_order_acq_rel);
if (already_signaled) return; // only one signal at a time
notifier_->notify();
}
static const std::unordered_map<std::string, std::vector<Op>>&
lane_registry() {
static const std::unordered_map<std::string, std::vector<Op>> registry = {
{"lookup", {Op::BATCH_TILE_EXISTS}},
{"retrieve", {Op::BATCH_TILE_GET}},
{"store", {Op::BATCH_TILE_SET}},
{"delete", {Op::BATCH_TILE_DELETE}},
};
return registry;
}
static const std::string& lane_key_for_op(Op op) {
static const auto& reverse = []() {
std::unordered_map<Op, std::string> m;
for (auto& [key, ops] : lane_registry()) {
for (Op o : ops) {
m[o] = key;
}
}
return m;
}();
auto it = reverse.find(op);
if (it == reverse.end()) {
throw std::runtime_error("unknown Op type: " +
std::to_string(static_cast<int>(op)));
}
return it->second;
}
void start_lane_workers(WorkerLane& lane, int num_workers) {
lane.workers.reserve(static_cast<size_t>(num_workers));
WorkerLane* lane_ptr = &lane;
for (int i = 0; i < num_workers; i++) {
lane.workers.emplace_back([this, lane_ptr]() {
this->worker_loop_for_queue(lane_ptr->mu, lane_ptr->cv,
lane_ptr->requests);
});
}
}
void join_lane_workers(WorkerLane& lane) {
for (auto& worker : lane.workers) {
if (worker.joinable()) {
worker.join();
}
}
}
void clear_lane_queue(WorkerLane& lane) {
std::lock_guard<std::mutex> lk(lane.mu);
while (!lane.requests.empty()) {
lane.requests.pop();
}
}
void worker_loop() { worker_loop_for_queue(req_mu_, req_cv_, requests_); }
void worker_loop_for_queue(std::mutex& req_mu,
std::condition_variable& req_cv,
std::queue<Request>& requests) {
try {
// create connection (derived class specific)
ConnectionType conn = create_connection();
for (;;) {
Request req;
// 1. grab a request from the submission queue
{
std::unique_lock<std::mutex> lk(req_mu);
req_cv.wait(lk, [&] {
return stop_.load(std::memory_order_acquire) || !requests.empty();
});
if (stop_.load(std::memory_order_acquire) && requests.empty()) {
break; // exit loop
}
req = std::move(requests.front());
requests.pop();
}
Completion comp;
comp.future_id = req.future_id;
// 2. execute the requested operation
try {
switch (req.op) {
case Op::BATCH_TILE_GET:
do_batch_get(conn, req);
comp.ok = true;
break;
case Op::BATCH_TILE_SET:
do_batch_set(conn, req);
comp.ok = true;
break;
case Op::BATCH_TILE_EXISTS:
do_batch_exists(conn, req);
comp.ok = true;
break;
case Op::BATCH_TILE_DELETE:
do_batch_delete(conn, req);
comp.ok = true;
break;
}
} catch (const std::exception& e) {
comp.ok = false;
comp.error = e.what();
// if shutting down, errors are expected
if (stop_.load(std::memory_order_acquire)) {
break; // exit without pushing completion
}
}
// 3. update shared batch state and push completion when done
handle_tile_completion(req, comp);
}
} catch (const std::exception& e) {
fprintf(stderr, "[LMCache Connector Worker Error] %s\n", e.what());
} catch (...) {
fprintf(stderr, "[LMCache Connector Worker Error] Unknown exception\n");
}
}
void handle_tile_completion(const Request& req, const Completion& comp) {
// record failure if any
if (!comp.ok) {
req.batch->any_failed.store(true, std::memory_order_relaxed);
std::lock_guard<std::mutex> lk(req.batch->err_mu);
if (req.batch->first_error.empty()) {
req.batch->first_error = comp.error;
}
}
// check if this is the last tile to complete
uint32_t tiles_left =
req.batch->remaining_tiles.fetch_sub(1, std::memory_order_relaxed) - 1;
if (tiles_left == 0) {
// last tile to finish - emit single completion for entire batch
Completion batch_comp;
batch_comp.future_id = req.future_id;
batch_comp.ok = !req.batch->any_failed.load(std::memory_order_relaxed);
if (!batch_comp.ok) {
std::lock_guard<std::mutex> lk(req.batch->err_mu);
batch_comp.error = req.batch->first_error;
}
// for batch exists and batch get, move per-key results
if (req.batch->batch_op == Op::BATCH_TILE_EXISTS ||
req.batch->batch_op == Op::BATCH_TILE_GET ||
req.batch->batch_op == Op::BATCH_TILE_DELETE) {
batch_comp.result_bytes = std::move(req.batch->per_key_results);
}
push_completion(std::move(batch_comp));
}
}
protected:
int num_workers_;
WorkerPoolConfig worker_pool_config_;
std::atomic<bool> stop_{false};
std::atomic<bool> closed_{false};
std::atomic<uint64_t> next_future_id_{1};
private:
std::unique_ptr<EventNotifier> notifier_;
// treat wakeup fd as a binary wakeup flag:
// true: Python has been signaled (or will be)
// false: Python is asleep, no wakeup pending
std::atomic<bool> signaled_{false};
// submission queue (SQ)
std::mutex req_mu_;
std::condition_variable req_cv_;
std::queue<Request> requests_;
// completion queue (CQ)
std::mutex comp_mu_;
std::queue<Completion> completions_;
std::vector<std::thread> workers_;
// Populated only during start_workers() (single-threaded construction).
// All subsequent accesses are reads — no lock needed in enqueue_request.
std::unordered_map<std::string, std::unique_ptr<WorkerLane>> lanes_;
};
} // namespace connector
} // namespace lmcache