22#include <sys/statvfs.h>
23#include <system_error>
42 static std::atomic<std::uint64_t> generations{0};
43 auto generation = generations.load(std::memory_order_relaxed);
45 if (
generation == std::numeric_limits<std::uint64_t>::max())
47 {{
"stage",
"execution"}, {
"reason",
"counter_overflow"}, {
"setting",
"protocol"}},
48 "scratch quota object generation exhausted");
50 std::memory_order_relaxed))
break;
52 object_ = {
static_cast<std::uint64_t
>(::getpid()),
generation + 1, 0, 0};
53 creation_ = quota_.reserve(object_, {0, 1, 0, 0}, 0, 0,
59 const int saved_errno = errno;
60 try { creation_.cancel(); }
catch (...) {}
68 do { result = ::fstat(fd, &info); }
while (result != 0 && errno == EINTR);
69 if (result != 0)
return errno;
70 struct statvfs filesystem{};
71 do { result = ::fstatvfs(fd, &filesystem); }
while (result != 0 && errno == EINTR);
72 if (result != 0)
return errno;
73 block_size_ = filesystem.f_bsize;
75 object_.device =
static_cast<std::uint64_t
>(info.st_dev);
76 object_.inode =
static_cast<std::uint64_t
>(info.st_ino);
77 creation_.commit({0, 1, 0, 0}, 0, object_.device, object_.inode);
88 if (!registered_ || !unlinked_ || borrowed_)
return;
89 const int saved_errno = errno;
91 quota_.release(object_, {charged_, 1, 0, 0}, charged_, 0,
101 : owner_(owner), realized_(owner.extent_) {
102 owner.quota_.
check();
104 std::max(owner.extent_, requested_end), owner.block_size_);
105 ticket_ = owner.quota_.
reserve(owner.object_, {requested - owner.charged_, 0, 0, 0},
111 const int saved_errno = errno;
114 try {
finish(); }
catch (...) {}
117 void wrote(std::uint64_t
end)
noexcept { realized_ = std::max(realized_,
end); }
118 void truncated(std::uint64_t bytes)
noexcept { realized_ = bytes; }
120 if (finished_)
return;
122 owner_.extent_ = realized_;
124 const auto committed = std::max(owner_.charged_, actual);
126 owner_.object_.device, owner_.object_.inode);
131 owner_.quota_.release(owner_.object_, {committed - actual, 0, 0, 0},
133 owner_.charged_ = actual;
139 std::uint64_t realized_;
141 bool finished_ =
false;
149 std::uint64_t block_size_ = 0;
150 std::uint64_t extent_ = 0;
151 std::uint64_t charged_ = 0;
152 bool registered_ =
false;
153 bool unlinked_ =
false;
154 bool borrowed_ =
false;
174 if (
this != &other) {
176 fd_ = std::exchange(other.fd_, -1);
177 quota_ = std::move(other.quota_);
178 directory_ = std::move(other.directory_);
179 slab_ = std::move(other.slab_);
180 resource_ = other.resource_;
181 arena_ = std::exchange(other.arena_,
nullptr);
182 region_ = std::move(other.region_);
183 other.region_.size = 0;
184 io_raiser_ = other.io_raiser_;
185 bounded_cache_ = other.bounded_cache_;
186 clean_bytes_read_ = std::exchange(other.clean_bytes_read_, 0);
187 bytes_written_since_sync_ = std::exchange(other.bytes_written_since_sync_, 0);
196 if (slab) (void)slab->region(
resource);
200 if (slab_) slab_->reset(resource_);
202 const std::shared_ptr<ScratchSlab>&
scratch_owner() const noexcept {
return slab_; }
219 if (fd_ >= 0)
throw std::logic_error(
"scratch cache policy changed after open");
220 bounded_cache_ =
true;
224 if (fd_ >= 0 && directory_ !=
value)
225 throw std::logic_error(
"scratch directory changed after spill");
229 if (arena_) arena_->release_region(region_);
232 const int result = ::close(fd_);
233 if (result == 0 && quota_) quota_->closed();
237 clean_bytes_read_ = bytes_written_since_sync_ = 0;
240 if (slab_) slab_->reset(resource_);
242 bool opened() const noexcept {
return fd_ >= 0 || arena_ !=
nullptr; }
247 if (quota_) quota_->borrow_descriptor();
253 if (slab_)
throw std::logic_error(
"scratch occupancy requires a file or arena");
255 if (fd_ < 0)
return 0;
258 do { result = ::fstat(fd_, &info); }
while (result != 0 && errno == EINTR);
259 if (result != 0)
io_error(errno,
"scratch extent");
260 if (info.st_size < 0)
io_error(EIO,
"scratch extent");
261 return static_cast<std::uint64_t
>(info.st_size);
269 if (bytes == 0)
return;
270 if (slab_)
throw std::logic_error(
"scratch head release requires a file or arena");
272 io_error(EINVAL,
"scratch head release alignment");
274 arena_io([&] { arena_->release_prefix(region_, bytes); });
278 if (bytes > extent)
io_error(EINVAL,
"scratch head release extent");
279 std::array<unsigned char, kCacheBytes> window;
280 const auto kept = extent - bytes;
281 for (std::uint64_t offset = 0; offset < kept;) {
282 const auto count =
static_cast<std::size_t
>(
283 std::min<std::uint64_t>(window.size(), kept - offset));
284 read(bytes + offset, window.data(), count);
285 write(offset, window.data(), count);
291 if (slab_)
throw std::logic_error(
"scratch truncate requires a file or arena");
294 arena_io([&] { arena_->truncate(region_, bytes); });
297 std::optional<ScratchFileQuota::Growth> reservation;
298 if (quota_) reservation.emplace(*quota_, bytes);
300 do { result = ::ftruncate(fd_,
static_cast<off_t
>(bytes)); }
301 while (result != 0 && errno == EINTR);
302 if (result != 0)
io_error(errno,
"report scratch truncate");
304 reservation->truncated(bytes);
305 reservation->finish();
308 void open(
const char* prefix) {
313 arena_io([&] {
arena->open_region(region_); });
318 int directory_error = 0;
323 io_error(directory_error, refusal.c_str());
326 if (name.back() !=
'/') name +=
'/';
332 quota_ = std::make_unique<ScratchFileQuota>(
artifact);
334 const int opened = ::mkstemp(name.data());
336 const int error = errno;
337 if (quota_) quota_->create_failed();
339 io_error(error, create_message.c_str());
343 const int error = quota_->created(
opened);
344 if (error != 0)
io_error(error,
"scratch backing");
348 ::unlink(name.c_str());
352 if (::unlink(name.c_str()) != 0) {
353 const int error = errno;
358 if (quota_) quota_->unlinked();
359 if (::fcntl(
opened, F_SETFD, FD_CLOEXEC) == -1) {
360 const int error = errno;
361 const int result = ::close(
opened);
362 if (result == 0 && quota_) quota_->closed();
367 if (bounded_cache_) {
368#if defined(__linux__)
369 struct statfs backing{};
370 if (::fstatfs(fd_, &backing) != 0) {
371 const int error = errno;
378 io_error(ENODEV,
"scratch requires disk backing");
380 const int error = ::posix_fadvise(fd_, 0, 0, POSIX_FADV_RANDOM);
383 io_error(error,
"scratch random access");
385#elif defined(__APPLE__)
386 if (::fcntl(fd_, F_NOCACHE, 1) == -1) {
387 const int error = errno;
389 io_error(error,
"scratch uncached access");
399 throw std::logic_error(
"bounded scratch requires an arena region");
401 if (!arena_ || region_.max_bytes != max_bytes)
402 throw std::logic_error(
"bounded scratch policy changed after open");
406 if (!
arena)
throw std::logic_error(
"bounded scratch requires an attached arena");
409 arena_io([&] {
arena->open_region(region_, max_bytes); });
417 void ensure_size(std::uint64_t bytes, std::uint64_t needed_bytes_lower_bound = 0) {
419 slab_->extend(resource_, bytes);
423 if (!
opened())
throw std::logic_error(
"scratch growth before open");
425 arena_io([&] { arena_->ensure_size(region_, bytes, needed_bytes_lower_bound); });
428 std::optional<ScratchFileQuota::Growth> reservation;
429 if (quota_) reservation.emplace(*quota_, bytes);
432 do { result = ::fstat(fd_, &
status); }
while (result != 0 && errno == EINTR);
433 if (result != 0)
io_error(errno,
"scratch size");
434 if (
status.st_size >=
static_cast<off_t
>(bytes)) {
435 if (reservation) reservation->finish();
438 do { result = ::ftruncate(fd_,
static_cast<off_t
>(bytes)); }
439 while (result != 0 && errno == EINTR);
440 if (result != 0)
io_error(errno,
"scratch resize");
442 reservation->truncated(bytes);
443 reservation->finish();
448 const auto maximum =
static_cast<std::uint64_t
>(std::numeric_limits<off_t>::max());
449 if (offset > maximum || length > maximum - offset) {
453 {{
"reason",
arena ?
"arena_io" :
"scratch_io"}});
462 check_extent(offset, length, arena_ !=
nullptr, io_raiser_);
465 const auto count = std::min(length, bounded_cache_
467 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
468 if (slab_) slab_->write_at(resource_, offset,
data, count);
469 else if (arena_) arena_io([&] { arena_->write(region_, offset,
data, count); });
471 std::optional<ScratchFileQuota::Growth> reservation;
472 if (quota_) reservation.emplace(*quota_, length == 0 ? 0 : offset + length);
474 const auto chunk = std::min(count, bounded_cache_
476 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
477 const ssize_t written = ::pwrite(fd_,
data, chunk,
static_cast<off_t
>(offset));
478 if (reservation && written >= 0) {
479 if (written > 0) reservation->wrote(offset +
static_cast<std::uint64_t
>(written));
480 reservation->finish();
482 if (written > 0 && bounded_cache_)
483 bytes_written_since_sync_ +=
static_cast<std::uint32_t
>(written);
486 return static_cast<ssize_t
>(count);
488 ssize_t
read_some(std::uint64_t offset,
void*
data, std::size_t length)
const {
489 check_extent(offset, length, arena_ !=
nullptr, io_raiser_);
492 const auto count = std::min(length, bounded_cache_ ?
kCacheBytes
493 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
494 if (slab_) slab_->read_at(resource_, offset,
data, count);
495 else if (arena_) arena_io([&] { arena_->read(region_, offset,
data, count); });
498 if (bounded_cache_ && clean_bytes_read_ ==
kCacheBytes) discard_clean_cache();
499 const auto chunk = std::min(count, bounded_cache_
501 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
502 const auto received = ::pread(fd_,
data, chunk,
static_cast<off_t
>(offset));
503 if (received > 0 && bounded_cache_) {
504 clean_bytes_read_ +=
static_cast<std::uint32_t
>(received);
505 if (clean_bytes_read_ ==
kCacheBytes) discard_clean_cache();
509 return static_cast<ssize_t
>(count);
511 void write(std::uint64_t offset,
const void*
data, std::size_t length) {
513 slab_->write_at(resource_, offset,
data, length);
516 check_extent(offset, length, arena_ !=
nullptr, io_raiser_);
518 arena_io([&] { arena_->write(region_, offset,
data, length); });
521 std::optional<ScratchFileQuota::Growth> reservation;
522 if (quota_ && length != 0) reservation.emplace(*quota_, offset + length);
523 const auto* bytes =
static_cast<const unsigned char*
>(
data);
524 std::size_t written = 0;
525 while (written < length) {
527 const auto chunk = std::min(length - written, bounded_cache_
529 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
530 const ssize_t count = ::pwrite(fd_, bytes + written, chunk,
531 static_cast<off_t
>(offset + written));
532 if (count < 0 && errno == EINTR)
continue;
533 if (count <= 0)
io_error(count < 0 ? errno : EIO,
"scratch write");
534 written +=
static_cast<std::size_t
>(count);
535 if (reservation) reservation->wrote(offset + written);
536 if (bounded_cache_) bytes_written_since_sync_ +=
static_cast<std::uint32_t
>(count);
538 if (reservation) reservation->finish();
540 void read(std::uint64_t offset,
void*
data, std::size_t length)
const {
542 slab_->read_at(resource_, offset,
data, length);
545 check_extent(offset, length, arena_ !=
nullptr, io_raiser_);
547 arena_io([&] { arena_->read(region_, offset,
data, length); });
550 auto* bytes =
static_cast<unsigned char*
>(
data);
551 std::size_t received = 0;
552 while (received < length) {
554 if (bounded_cache_ && clean_bytes_read_ ==
kCacheBytes) discard_clean_cache();
555 const auto chunk = std::min(length - received, bounded_cache_
557 :
static_cast<std::size_t
>(std::numeric_limits<ssize_t>::max()));
558 const ssize_t count = ::pread(fd_, bytes + received, chunk,
559 static_cast<off_t
>(offset + received));
560 if (count < 0 && errno == EINTR)
continue;
561 if (count <= 0)
io_error(count < 0 ? errno : EIO,
"scratch read");
562 received +=
static_cast<std::size_t
>(count);
563 if (bounded_cache_) {
564 clean_bytes_read_ +=
static_cast<std::uint32_t
>(count);
565 if (clean_bytes_read_ ==
kCacheBytes) discard_clean_cache();
571 template<
class Operation>
void arena_io(Operation operation)
const {
573 catch (
const ReportScratchIoError& error) {
574 const auto& failure = error.run_failure();
579 && failure.args && *failure.args ==
"{\"reason\":\"arena_io\"}")
580 throw AuxiliaryScratchIoError(error);
584 [[noreturn]]
void io_error(
int error,
const char* operation)
const {
586 throw AuxiliaryScratchIoError(error, operation);
587 throw ReportScratchIoError(error, operation, {{
"reason",
"scratch_io"}});
600 void discard_clean_cache()
const {
601 if (!bounded_cache_)
return;
602#if defined(__linux__)
603 const int error = ::posix_fadvise(fd_, 0, 0, POSIX_FADV_DONTNEED);
604 if (error != 0)
io_error(error,
"scratch discard cache");
606 clean_bytes_read_ = 0;
608 void prepare_write() {
609 if (!bounded_cache_ || bytes_written_since_sync_ <
kCacheBytes)
return;
610#if defined(__linux__)
612 do { result = ::fdatasync(fd_); }
while (result != 0 && errno == EINTR);
613 if (result != 0)
io_error(errno,
"scratch flush cache window");
617 discard_clean_cache();
618 bytes_written_since_sync_ = 0;
625 std::string directory_;
628 std::shared_ptr<ScratchSlab> slab_;
630 bool bounded_cache_ =
false;
631 ScratchArena* arena_ =
nullptr;
632 ScratchArena::Region region_;
636 mutable std::uint32_t clean_bytes_read_ = 0;
637 std::uint32_t bytes_written_since_sync_ = 0;
640 std::unique_ptr<ScratchFileQuota> quota_;
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
QuotaTicket reserve(QuotaObject, QuotaAmounts, std::uint64_t old_extent, std::uint64_t new_extent, QuotaStage, QuotaArtifact)
static RunQuota from_environment()
static ScratchArena * attached() noexcept
static constexpr std::uint64_t kChunkBytes
Growth(ScratchFileQuota &owner, std::uint64_t requested_end)
Growth & operator=(const Growth &)=delete
Growth(const Growth &)=delete
void truncated(std::uint64_t bytes) noexcept
void wrote(std::uint64_t end) noexcept
ScratchFileQuota(QuotaArtifact artifact)
void borrow_descriptor() noexcept
void create_failed() noexcept
ssize_t write_some(std::uint64_t offset, const void *data, std::size_t length)
ScratchFile(IoRaiser raiser=IoRaiser::report_sink)
ssize_t read_some(std::uint64_t offset, void *data, std::size_t length) const
void truncate(std::uint64_t bytes)
void bind_scratch(const std::shared_ptr< ScratchSlab > &slab, ScratchResource resource)
bool arena_backed() const noexcept
static void check_extent(std::uint64_t offset, std::size_t length, bool arena=false, IoRaiser raiser=IoRaiser::report_sink)
void ensure_size(std::uint64_t bytes, std::uint64_t needed_bytes_lower_bound=0)
static constexpr std::size_t kCacheBytes
void open(const char *prefix)
bool disk_backed() const noexcept
void set_store(ScratchStore store) noexcept
void write(std::uint64_t offset, const void *data, std::size_t length)
ScratchFile & operator=(const ScratchFile &)=delete
ScratchFile(ScratchFile &&other) noexcept
void set_directory(const std::string &value)
ScratchFile & operator=(ScratchFile &&other) noexcept
void open_bounded(const char *, std::uint64_t max_bytes)
bool opened() const noexcept
ScratchResource scratch_resource() const noexcept
std::uint64_t storage_bytes() const
const std::shared_ptr< ScratchSlab > & scratch_owner() const noexcept
int descriptor() const noexcept
ScratchFile(const ScratchFile &)=delete
void read(std::uint64_t offset, void *data, std::size_t length) const
void release_prefix(std::uint64_t bytes)
std::string resolve_scratch_directory(const std::string &configured, std::string &refusal, int &error, bool &proven_memory, Probe probe)
std::uint64_t quota_rounded_extent(std::uint64_t extent, std::uint64_t block_size)
ReportScratchIoError ScratchIoError
bool scratch_filesystem_is_memory(std::uint64_t type) noexcept
std::string scratch_create_message(const char *operation, const std::string &directory, const std::string &selection_note)
QuotaMode mode() noexcept
@ run_quota_configuration_invalid