core: io/sparse_file + io/write_buffer (stage 4)
io/sparse_file — the single O_WRONLY output file (docs/04 §4). open()
posix_fallocate's the full size (falls back to ftruncate on
EOPNOTSUPP/ENOSYS, reported via preallocated()); write_at() pwrites at an
absolute offset, looping short writes and retrying EINTR; sync() is
fdatasync (timer/pause only); advise_dontneed() is
posix_fadvise(DONTNEED); resize() trims a preallocated tail or sizes a
chunked download. errno -> vdm::Error (ENOSPC->disk_full,
EACCES->permission_denied, ENOENT/ENOTDIR/...->path_rejected). No lock on
write_at — POSIX makes each pwrite atomic for a regular file, so N
segment threads writing disjoint ranges is safe (tested, TSan-clean).
io/write_buffer — per-segment accumulate-and-flush buffer, preallocated
at construction; append() only memcpys (no allocation on the write-
callback hot path, docs/04 §8 — asserted by a global-new counter in the
test). Flushes on fill via a caller-supplied FlushFn; a chunk >= capacity
arriving on an empty buffer writes straight through. On a flush error
next_offset() stays at the last durable position. Single-threaded; the
disk-writer-thread handoff is stage 8.
Also: vtest.hpp VT_CHECK_EQ/NE now copy operands (auto, not auto&&) — an
assertion must not outlive a temporary the expression returned a
reference into (ASan caught this on Result<void>{}.error().code).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// vdm/io/sparse_file.cpp
|
||||
|
||||
#include "vdm/io/sparse_file.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace vdm::io {
|
||||
namespace {
|
||||
|
||||
Error errno_to_error(int e) noexcept {
|
||||
switch (e) {
|
||||
case ENOSPC:
|
||||
case EDQUOT:
|
||||
return Error::disk_full;
|
||||
case EACCES:
|
||||
case EPERM:
|
||||
case EROFS:
|
||||
return Error::permission_denied;
|
||||
case ENOENT:
|
||||
case ENOTDIR:
|
||||
case EISDIR:
|
||||
case ENAMETOOLONG:
|
||||
case ELOOP:
|
||||
return Error::path_rejected;
|
||||
default:
|
||||
return Error::io_error;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorInfo sys_error(std::string_view what, int e) {
|
||||
return ErrorInfo(errno_to_error(e), std::string(what) + ": " + std::strerror(e));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SparseFile::~SparseFile() {
|
||||
if (fd_ >= 0)
|
||||
::close(fd_);
|
||||
}
|
||||
|
||||
SparseFile::SparseFile(SparseFile &&o) noexcept
|
||||
: fd_(std::exchange(o.fd_, -1)),
|
||||
preallocated_(std::exchange(o.preallocated_, false)),
|
||||
path_(std::move(o.path_)) {}
|
||||
|
||||
SparseFile &SparseFile::operator=(SparseFile &&o) noexcept {
|
||||
if (this != &o) {
|
||||
if (fd_ >= 0)
|
||||
::close(fd_);
|
||||
fd_ = std::exchange(o.fd_, -1);
|
||||
preallocated_ = std::exchange(o.preallocated_, false);
|
||||
path_ = std::move(o.path_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void SparseFile::reset() noexcept {
|
||||
fd_ = -1;
|
||||
preallocated_ = false;
|
||||
path_.clear();
|
||||
}
|
||||
|
||||
Result<void> SparseFile::open(std::string_view path) {
|
||||
return open(path, OpenOptions{});
|
||||
}
|
||||
|
||||
Result<void> SparseFile::open(std::string_view path, const OpenOptions &opts) {
|
||||
if (fd_ >= 0)
|
||||
return ErrorInfo(Error::internal, "SparseFile already open");
|
||||
|
||||
std::string p(path);
|
||||
int flags = O_WRONLY | O_CREAT | O_CLOEXEC;
|
||||
if (opts.truncate_existing)
|
||||
flags |= O_TRUNC;
|
||||
|
||||
int fd = ::open(p.c_str(), flags, 0644);
|
||||
if (fd < 0)
|
||||
return sys_error("open " + p, errno);
|
||||
|
||||
bool prealloc = false;
|
||||
if (opts.total_size > 0) {
|
||||
if (opts.preallocate) {
|
||||
// posix_fallocate returns the error number directly and does not set errno.
|
||||
int rc = ::posix_fallocate(fd, 0, static_cast<off_t>(opts.total_size));
|
||||
if (rc == 0) {
|
||||
prealloc = true;
|
||||
} else if (rc == EOPNOTSUPP || rc == ENOSYS || rc == EINVAL) {
|
||||
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
|
||||
int e = errno;
|
||||
::close(fd);
|
||||
return sys_error("ftruncate " + p, e);
|
||||
}
|
||||
} else {
|
||||
::close(fd);
|
||||
return sys_error("posix_fallocate " + p, rc);
|
||||
}
|
||||
} else if (!opts.truncate_existing) {
|
||||
// Resuming: make sure the file is at least total_size so pwrite offsets land.
|
||||
if (::ftruncate(fd, static_cast<off_t>(opts.total_size)) != 0) {
|
||||
int e = errno;
|
||||
::close(fd);
|
||||
return sys_error("ftruncate " + p, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fd_ = fd;
|
||||
preallocated_ = prealloc;
|
||||
path_ = std::move(p);
|
||||
return ok();
|
||||
}
|
||||
|
||||
Result<void> SparseFile::write_at(std::uint64_t offset, ConstByteSpan data) {
|
||||
if (fd_ < 0)
|
||||
return ErrorInfo(Error::internal, "write_at on a closed SparseFile");
|
||||
|
||||
const std::byte *p = data.data();
|
||||
std::size_t remaining = data.size();
|
||||
off_t pos = static_cast<off_t>(offset);
|
||||
|
||||
while (remaining > 0) {
|
||||
ssize_t n = ::pwrite(fd_, p, remaining, pos);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return sys_error("pwrite", errno);
|
||||
}
|
||||
if (n == 0)
|
||||
return ErrorInfo(Error::io_error, "pwrite returned 0");
|
||||
p += n;
|
||||
pos += n;
|
||||
remaining -= static_cast<std::size_t>(n);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
Result<void> SparseFile::sync() {
|
||||
if (fd_ < 0)
|
||||
return ErrorInfo(Error::internal, "sync on a closed SparseFile");
|
||||
while (::fdatasync(fd_) != 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return sys_error("fdatasync", errno);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
void SparseFile::advise_dontneed(std::uint64_t offset, std::uint64_t len) noexcept {
|
||||
if (fd_ < 0 || len == 0)
|
||||
return;
|
||||
::posix_fadvise(fd_, static_cast<off_t>(offset), static_cast<off_t>(len), POSIX_FADV_DONTNEED);
|
||||
}
|
||||
|
||||
Result<void> SparseFile::resize(std::uint64_t size) {
|
||||
if (fd_ < 0)
|
||||
return ErrorInfo(Error::internal, "resize on a closed SparseFile");
|
||||
while (::ftruncate(fd_, static_cast<off_t>(size)) != 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return sys_error("ftruncate", errno);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
Result<void> SparseFile::close() {
|
||||
if (fd_ < 0)
|
||||
return ok();
|
||||
int fd = std::exchange(fd_, -1);
|
||||
int rc = ::close(fd);
|
||||
reset();
|
||||
if (rc != 0)
|
||||
return sys_error("close", errno);
|
||||
return ok();
|
||||
}
|
||||
|
||||
} // namespace vdm::io
|
||||
@@ -0,0 +1,57 @@
|
||||
// vdm/io/write_buffer.cpp
|
||||
|
||||
#include "vdm/io/write_buffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace vdm::io {
|
||||
|
||||
WriteBuffer::WriteBuffer(std::uint64_t start_offset, std::size_t capacity, FlushFn flush)
|
||||
: buf_(capacity), base_(start_offset), flush_(std::move(flush)) {
|
||||
assert(capacity > 0 && "WriteBuffer capacity must be > 0");
|
||||
}
|
||||
|
||||
Result<void> WriteBuffer::flush_pending() {
|
||||
if (len_ == 0)
|
||||
return ok();
|
||||
VDM_TRY(flush_(base_, ConstByteSpan(buf_.data(), len_)));
|
||||
base_ += len_;
|
||||
len_ = 0;
|
||||
return ok();
|
||||
}
|
||||
|
||||
Result<void> WriteBuffer::append(ConstByteSpan span) {
|
||||
// On an error return, next_offset() still reflects exactly what is durable; buffered
|
||||
// (non-durable) bytes and any unconsumed tail of `span` are the caller's to abandon —
|
||||
// the segment aborts and resumes/restarts from next_offset().
|
||||
while (!span.empty()) {
|
||||
// Buffer empty and the incoming chunk fills at least a whole buffer: write it
|
||||
// straight through — no memcpy, no allocation, just an extra flush call.
|
||||
if (len_ == 0 && span.size() >= buf_.size()) {
|
||||
VDM_TRY(flush_(base_, span));
|
||||
base_ += span.size();
|
||||
appended_ += span.size();
|
||||
return ok();
|
||||
}
|
||||
|
||||
const std::size_t room = buf_.size() - len_;
|
||||
const std::size_t n = std::min(span.size(), room);
|
||||
std::memcpy(buf_.data() + len_, span.data(), n);
|
||||
len_ += n;
|
||||
appended_ += n;
|
||||
span = span.subspan(n);
|
||||
|
||||
if (len_ == buf_.size())
|
||||
VDM_TRY(flush_pending());
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
|
||||
Result<void> WriteBuffer::flush() {
|
||||
return flush_pending();
|
||||
}
|
||||
|
||||
} // namespace vdm::io
|
||||
Reference in New Issue
Block a user