299 lines
9.1 KiB
C++
299 lines
9.1 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#include "connector.h"
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
namespace lmcache {
|
|
namespace connector {
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
std::string FSConnector::replace_all(const std::string& str,
|
|
const std::string& from,
|
|
const std::string& to) {
|
|
std::string result = str;
|
|
size_t pos = 0;
|
|
while ((pos = result.find(from, pos)) != std::string::npos) {
|
|
result.replace(pos, from.size(), to);
|
|
pos += to.size();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string FSConnector::key_to_filename(const std::string& key) {
|
|
// Input key format (from _object_key_to_string):
|
|
// Unsalted: <model_name>@<kv_rank_hex>@<chunk_hash_hex>
|
|
// Salted : <model_name>@<kv_rank_hex>@<chunk_hash_hex>@<cache_salt>
|
|
//
|
|
// Output filename (matching fs_l2_adapter.py._object_key_to_filename):
|
|
// Unsalted: <model_name_safe>@0x<kv_rank_hex>@<chunk_hash_hex>.data
|
|
// Salted :
|
|
// <model_name_safe>@0x<kv_rank_hex>@<chunk_hash_hex>@<cache_salt>.data
|
|
//
|
|
// The unsalted 3-field shape is bit-identical to the pre-cache_salt
|
|
// format, so existing cache directories remain valid.
|
|
//
|
|
// NOTE: both model_name and cache_salt are forbidden from containing
|
|
// '@' (invariant enforced on the Python side), so splitting on '@'
|
|
// is unambiguous — no marker, no rsplit.
|
|
|
|
// Split on '@' — must yield 3 (unsalted) or 4 (salted) fields.
|
|
std::vector<std::string> parts;
|
|
size_t start = 0;
|
|
for (size_t pos = 0; pos <= key.size(); ++pos) {
|
|
if (pos == key.size() || key[pos] == KEY_SEP) {
|
|
parts.emplace_back(key.substr(start, pos - start));
|
|
start = pos + 1;
|
|
}
|
|
}
|
|
if (parts.size() != 3 && parts.size() != 4) {
|
|
throw std::runtime_error(
|
|
"FSConnector: malformed key (expected 3 or 4 '@'-separated fields): " +
|
|
key);
|
|
}
|
|
|
|
const std::string& model_name = parts[0];
|
|
const std::string& kv_rank_hex = parts[1];
|
|
const std::string& chunk_hash = parts[2];
|
|
const std::string cache_salt = parts.size() == 4 ? parts[3] : std::string();
|
|
|
|
// Replace '/' with '-SEP-' for filesystem safety
|
|
std::string safe_model = replace_all(model_name, "/", PATH_SLASH_REPLACEMENT);
|
|
|
|
// Emit filename. Salt is appended at the tail so the unsalted shape
|
|
// matches what older builds wrote to disk.
|
|
std::string result;
|
|
result.reserve(safe_model.size() + kv_rank_hex.size() + chunk_hash.size() +
|
|
cache_salt.size() + 32);
|
|
result += safe_model;
|
|
result += KEY_SEP;
|
|
result += "0x";
|
|
result += kv_rank_hex;
|
|
result += KEY_SEP;
|
|
result += chunk_hash;
|
|
if (!cache_salt.empty()) {
|
|
result += KEY_SEP;
|
|
result += cache_salt;
|
|
}
|
|
result += FILE_EXT;
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// read/write helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
static void write_all(int fd, const void* data, size_t len) {
|
|
size_t written = 0;
|
|
const char* ptr = static_cast<const char*>(data);
|
|
while (written < len) {
|
|
ssize_t n = ::write(fd, ptr + written, len - written);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
throw std::runtime_error("write failed: " + std::string(strerror(errno)));
|
|
}
|
|
if (n == 0) {
|
|
throw std::runtime_error("write returned 0");
|
|
}
|
|
written += static_cast<size_t>(n);
|
|
}
|
|
}
|
|
|
|
static size_t read_all(int fd, void* buf, size_t len) {
|
|
size_t total = 0;
|
|
char* ptr = static_cast<char*>(buf);
|
|
while (total < len) {
|
|
ssize_t n = ::read(fd, ptr + total, len - total);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
throw std::runtime_error("read failed: " + std::string(strerror(errno)));
|
|
}
|
|
if (n == 0) break; // EOF
|
|
total += static_cast<size_t>(n);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// FSConnector
|
|
// ---------------------------------------------------------------
|
|
|
|
FSConnector::FSConnector(std::string base_path, int num_workers,
|
|
std::string relative_tmp_dir, bool use_odirect,
|
|
size_t read_ahead_size)
|
|
: ConnectorBase(num_workers),
|
|
base_path_(std::move(base_path)),
|
|
relative_tmp_dir_(std::move(relative_tmp_dir)),
|
|
use_odirect_(use_odirect),
|
|
disk_block_size_(0),
|
|
read_ahead_size_(read_ahead_size) {
|
|
// Create base directory
|
|
std::filesystem::create_directories(base_path_);
|
|
|
|
// Create tmp directory if configured
|
|
if (!relative_tmp_dir_.empty()) {
|
|
auto tmp_path = std::filesystem::path(base_path_) / relative_tmp_dir_;
|
|
std::filesystem::create_directories(tmp_path);
|
|
}
|
|
|
|
// Query disk block size for O_DIRECT
|
|
if (use_odirect_) {
|
|
struct statvfs st;
|
|
if (statvfs(base_path_.c_str(), &st) == 0) {
|
|
disk_block_size_ = st.f_bsize;
|
|
}
|
|
}
|
|
|
|
start_workers(); // IMPORTANT: call at END of constructor
|
|
}
|
|
|
|
FSConnector::~FSConnector() { close(); }
|
|
|
|
WorkerFSConn FSConnector::create_connection() {
|
|
WorkerFSConn conn;
|
|
conn.base_path = base_path_;
|
|
if (!relative_tmp_dir_.empty()) {
|
|
conn.tmp_dir = std::filesystem::path(base_path_) / relative_tmp_dir_;
|
|
}
|
|
conn.use_odirect = use_odirect_;
|
|
conn.disk_block_size = disk_block_size_;
|
|
conn.read_ahead_size = read_ahead_size_;
|
|
return conn;
|
|
}
|
|
|
|
void FSConnector::do_single_get(WorkerFSConn& conn, const std::string& key,
|
|
void* buf, size_t len, size_t chunk_size) {
|
|
std::string filename = key_to_filename(key);
|
|
auto file_path = conn.base_path / filename;
|
|
|
|
int flags = O_RDONLY;
|
|
bool do_odirect = conn.use_odirect;
|
|
if (do_odirect) {
|
|
bool aligned = conn.disk_block_size > 0 && len % conn.disk_block_size == 0;
|
|
if (aligned) {
|
|
#ifdef O_DIRECT
|
|
flags |= O_DIRECT;
|
|
#endif
|
|
} else {
|
|
do_odirect = false;
|
|
}
|
|
}
|
|
|
|
int fd = ::open(file_path.c_str(), flags);
|
|
if (fd < 0) {
|
|
throw std::runtime_error("open for read failed: " + file_path.string() +
|
|
": " + strerror(errno));
|
|
}
|
|
|
|
try {
|
|
size_t n;
|
|
if (conn.read_ahead_size > 0 && len > conn.read_ahead_size) {
|
|
// Trigger filesystem readahead with a small initial
|
|
// read, then read the remainder.
|
|
size_t ra = conn.read_ahead_size;
|
|
size_t n_head = read_all(fd, buf, ra);
|
|
if (n_head < ra) {
|
|
// Short read on the head portion — treat as
|
|
// incomplete
|
|
n = n_head;
|
|
} else {
|
|
size_t n_tail = read_all(fd, static_cast<char*>(buf) + ra, len - ra);
|
|
n = n_head + n_tail;
|
|
}
|
|
} else {
|
|
n = read_all(fd, buf, len);
|
|
}
|
|
if (n != len) {
|
|
throw std::runtime_error("incomplete read for " + file_path.string() +
|
|
": expected " + std::to_string(len) + ", got " +
|
|
std::to_string(n));
|
|
}
|
|
} catch (...) {
|
|
::close(fd);
|
|
throw;
|
|
}
|
|
::close(fd);
|
|
}
|
|
|
|
void FSConnector::do_single_set(WorkerFSConn& conn, const std::string& key,
|
|
const void* buf, size_t len,
|
|
size_t chunk_size) {
|
|
std::string filename = key_to_filename(key);
|
|
auto file_path = conn.base_path / filename;
|
|
|
|
// Skip if already stored on disk
|
|
if (std::filesystem::exists(file_path)) {
|
|
return;
|
|
}
|
|
|
|
// Determine temp file path
|
|
std::filesystem::path tmp_path;
|
|
if (!conn.tmp_dir.empty()) {
|
|
tmp_path = conn.tmp_dir / filename;
|
|
} else {
|
|
tmp_path = file_path;
|
|
tmp_path.replace_extension(TMP_EXT);
|
|
}
|
|
|
|
int flags = O_CREAT | O_WRONLY | O_TRUNC;
|
|
bool do_odirect = conn.use_odirect;
|
|
if (do_odirect) {
|
|
bool aligned = conn.disk_block_size > 0 && len % conn.disk_block_size == 0;
|
|
if (aligned) {
|
|
#ifdef O_DIRECT
|
|
flags |= O_DIRECT;
|
|
#endif
|
|
} else {
|
|
do_odirect = false;
|
|
}
|
|
}
|
|
|
|
int fd = ::open(tmp_path.c_str(), flags, 0644);
|
|
if (fd < 0) {
|
|
throw std::runtime_error("open for write failed: " + tmp_path.string() +
|
|
": " + strerror(errno));
|
|
}
|
|
|
|
try {
|
|
write_all(fd, buf, len);
|
|
} catch (...) {
|
|
::close(fd);
|
|
// Clean up temp file on failure
|
|
std::filesystem::remove(tmp_path);
|
|
throw;
|
|
}
|
|
::close(fd);
|
|
|
|
// Atomic rename: tmp -> final
|
|
std::error_code ec;
|
|
std::filesystem::rename(tmp_path, file_path, ec);
|
|
if (ec) {
|
|
// Try to clean up, but prioritize reporting the original error.
|
|
std::error_code remove_ec;
|
|
std::filesystem::remove(tmp_path, remove_ec);
|
|
throw std::runtime_error("rename failed: " + tmp_path.string() + " -> " +
|
|
file_path.string() + ": " + ec.message());
|
|
}
|
|
}
|
|
|
|
bool FSConnector::do_single_exists(WorkerFSConn& conn, const std::string& key) {
|
|
std::string filename = key_to_filename(key);
|
|
auto file_path = conn.base_path / filename;
|
|
return std::filesystem::exists(file_path);
|
|
}
|
|
|
|
bool FSConnector::do_single_delete(WorkerFSConn& conn, const std::string& key) {
|
|
std::string filename = key_to_filename(key);
|
|
auto file_path = conn.base_path / filename;
|
|
std::error_code ec;
|
|
return std::filesystem::remove(file_path, ec);
|
|
}
|
|
|
|
} // namespace connector
|
|
} // namespace lmcache
|