// SPDX-License-Identifier: Apache-2.0 #pragma once #include "connector_interface.h" #include "connector_types.h" #include "event_notifier.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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 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& keys, const std::vector& bufs, const std::vector& 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& keys, const std::vector& bufs, const std::vector& 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& 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& 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 drain_completions() override { // Drain the eventfd that triggered this drain_completions callback drain_eventfd_(); std::vector completions_list; for (;;) { Completion c; { std::lock_guard 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 lk(req_mu_); while (!requests_.empty()) { requests_.pop(); } } for (auto& [key, lane] : lanes_) { clear_lane_queue(*lane); } { std::lock_guard 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(); } 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(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(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 requests; std::vector workers; }; void validate_batch_inputs(const std::vector& keys, const std::vector& bufs, const std::vector& 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, 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(); 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& keys, const std::vector& bufs, const std::vector& lens, size_t tile_idx, size_t tile_size, size_t num_items, uint64_t batch_future_id, std::shared_ptr 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 lk(it->second->mu); it->second->requests.push(std::move(req)); } it->second->cv.notify_one(); return; } { std::lock_guard lk(req_mu_); requests_.push(std::move(req)); } req_cv_.notify_one(); } void push_completion(Completion&& c) { { std::lock_guard 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>& lane_registry() { static const std::unordered_map> 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 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(op))); } return it->second; } void start_lane_workers(WorkerLane& lane, int num_workers) { lane.workers.reserve(static_cast(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 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& 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 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 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 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 stop_{false}; std::atomic closed_{false}; std::atomic next_future_id_{1}; private: std::unique_ptr 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 signaled_{false}; // submission queue (SQ) std::mutex req_mu_; std::condition_variable req_cv_; std::queue requests_; // completion queue (CQ) std::mutex comp_mu_; std::queue completions_; std::vector workers_; // Populated only during start_workers() (single-threaded construction). // All subsequent accesses are reads — no lock needed in enqueue_request. std::unordered_map> lanes_; }; } // namespace connector } // namespace lmcache