PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
scratch_file.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include <algorithm>
9#include <array>
10#include <atomic>
11#include <cerrno>
12#include <cstddef>
13#include <cstdint>
14#include <cstdlib>
15#include <fcntl.h>
16#include <limits>
17#include <memory>
18#include <optional>
19#include <stdexcept>
20#include <string>
21#include <sys/stat.h>
22#include <sys/statvfs.h>
23#include <system_error>
24#include <unistd.h>
25#include <utility>
26#if defined(__linux__)
27#include <sys/vfs.h>
28#endif
29
30namespace pineforge::detail {
31
32// Compatibility name for ledger/phase users of the report-scratch backend.
34
35// Physical-file accounting shared with the command backing, whose syscalls
36// bypass ScratchFile. Construct only in managed mode, before mkstemp. The host
37// confirms the admitted inode before accepting created()'s COMMIT.
39public:
41 : quota_(RunQuota::from_environment()), artifact_(artifact) {
42 static std::atomic<std::uint64_t> generations{0};
43 auto generation = generations.load(std::memory_order_relaxed);
44 for (;;) {
45 if (generation == std::numeric_limits<std::uint64_t>::max())
47 {{"stage", "execution"}, {"reason", "counter_overflow"}, {"setting", "protocol"}},
48 "scratch quota object generation exhausted");
49 if (generations.compare_exchange_weak(generation, generation + 1,
50 std::memory_order_relaxed)) break;
51 }
52 object_ = {static_cast<std::uint64_t>(::getpid()), generation + 1, 0, 0};
53 creation_ = quota_.reserve(object_, {0, 1, 0, 0}, 0, 0,
54 QuotaStage::execution, artifact_);
55 }
56
57 // A failed create proves that none of its reservation was realized.
58 void create_failed() noexcept {
59 const int saved_errno = errno;
60 try { creation_.cancel(); } catch (...) {}
61 errno = saved_errno;
62 }
63 // Return query errno to the caller's established I/O raiser. On a query
64 // or protocol failure the unknown inode remains reserved for host cleanup.
65 int created(int fd) {
66 struct stat info{};
67 int result;
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;
74 (void)quota_rounded_extent(0, block_size_);
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);
78 registered_ = true;
79 creation_.cancel();
80 return 0;
81 }
82 void unlinked() noexcept { unlinked_ = true; }
83 void borrow_descriptor() noexcept { borrowed_ = true; }
84 // Only an exclusive, anonymous backing can prove last-close locally.
85 // Borrowed descriptors/named residue stay with the host lease, even after
86 // this owner closes; unlink or a failed close alone refunds nothing.
87 void closed() noexcept {
88 if (!registered_ || !unlinked_ || borrowed_) return;
89 const int saved_errno = errno;
90 try {
91 quota_.release(object_, {charged_, 1, 0, 0}, charged_, 0,
92 QuotaStage::execution, artifact_);
93 registered_ = false;
94 } catch (...) {}
95 errno = saved_errno;
96 }
97
98 class Growth {
99 public:
100 Growth(ScratchFileQuota& owner, std::uint64_t requested_end)
101 : owner_(owner), realized_(owner.extent_) {
102 owner.quota_.check();
103 const auto requested = quota_rounded_extent(
104 std::max(owner.extent_, requested_end), owner.block_size_);
105 ticket_ = owner.quota_.reserve(owner.object_, {requested - owner.charged_, 0, 0, 0},
106 owner.charged_, requested, QuotaStage::execution, owner.artifact_);
107 }
108 Growth(const Growth&) = delete;
109 Growth& operator=(const Growth&) = delete;
110 ~Growth() noexcept {
111 const int saved_errno = errno;
112 // Preserve the selected I/O exception/errno if reconciliation also
113 // fails. The ticket then conservatively retains unknown growth.
114 try { finish(); } catch (...) {}
115 errno = saved_errno;
116 }
117 void wrote(std::uint64_t end) noexcept { realized_ = std::max(realized_, end); }
118 void truncated(std::uint64_t bytes) noexcept { realized_ = bytes; }
119 void finish() {
120 if (finished_) return;
121 finished_ = true;
122 owner_.extent_ = realized_;
123 const auto actual = quota_rounded_extent(realized_, owner_.block_size_);
124 const auto committed = std::max(owner_.charged_, actual);
125 ticket_.commit({committed - owner_.charged_, 0, 0, 0}, committed,
126 owner_.object_.device, owner_.object_.inode);
127 owner_.charged_ = committed;
128 ticket_.cancel();
129 // A shrink is observed only after the successful sizing syscall.
130 if (actual < committed) {
131 owner_.quota_.release(owner_.object_, {committed - actual, 0, 0, 0},
132 committed, actual, QuotaStage::execution, owner_.artifact_);
133 owner_.charged_ = actual;
134 }
135 }
136
137 private:
138 ScratchFileQuota& owner_;
139 std::uint64_t realized_;
140 QuotaTicket ticket_;
141 bool finished_ = false;
142 };
143
144private:
145 RunQuota quota_;
146 QuotaObject object_;
147 QuotaArtifact artifact_;
148 QuotaTicket creation_;
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;
155};
156
157// Run-owned positional scratch: an anonymous, close-on-exec file for an empty
158// owner, or checked slab copies when bound. Without a slab, open() selects an
159// explicitly attached arena, or the original mkstemp/unlink/pwrite/pread backend.
160// Sparse growth reads as zero in both modes, including reused arena chunks;
161// reads beyond the logical end still fail with EIO. No implicit attachment
162// or RAM fallback. clear() releases the region and its mapped windows.
164public:
165 // Fixed byte window shared by scratch I/O amortisation contracts.
166 static constexpr std::size_t kCacheBytes = 64 * 1024;
167
169 explicit ScratchFile(IoRaiser raiser = IoRaiser::report_sink) : io_raiser_(raiser) {}
170 ScratchFile(const ScratchFile&) = delete;
172 ScratchFile(ScratchFile&& other) noexcept { *this = std::move(other); }
173 ScratchFile& operator=(ScratchFile&& other) noexcept {
174 if (this != &other) {
175 clear();
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);
188 }
189 return *this;
190 }
192
193 // Binding is a begin/reset operation. Keep shared ownership, never a byte
194 // view: every access below goes through the slab's checked copy API.
195 void bind_scratch(const std::shared_ptr<ScratchSlab>& slab, ScratchResource resource) {
196 if (slab) (void)slab->region(resource);
197 clear();
198 slab_ = slab;
199 resource_ = resource;
200 if (slab_) slab_->reset(resource_);
201 }
202 const std::shared_ptr<ScratchSlab>& scratch_owner() const noexcept { return slab_; }
203 ScratchResource scratch_resource() const noexcept { return resource_; }
204 // Optional diagnostic identity; set before open(), including lazy opens.
205 // Default unknown, retained by clear/reopen and moves; file mode ignores
206 // it. ScratchFile remains noncopyable; owners cloning a store reapply it.
207 // Store labels include cohort (cohorts_by_id_), exit_rank
208 // (exit_rank_keys_), erased_minima (IdPairStore data) and its *_idx
209 // file, partial_cycles (consumed_partial_exit_cycles_ data) and
210 // partial_cycles_idx (hash/fold/journal), and successor_roots.
211 // Other labels include curve (equity scratch shared by SpillCurve/read windows)
212 // and position_tail (the separate position spill). Rehash replacement files
213 // inherit their index label before open(); prefixes are never identities.
214 void set_store(ScratchStore store) noexcept { region_.store = store; }
215
216 // Opt in before opening. Only the ledger/phase consumers request this;
217 // existing scratch users keep their established I/O behavior.
218 void bound_cache() {
219 if (fd_ >= 0) throw std::logic_error("scratch cache policy changed after open");
220 bounded_cache_ = true;
221 }
222 void set_directory(const std::string& value) {
223 if (arena_) return;
224 if (fd_ >= 0 && directory_ != value)
225 throw std::logic_error("scratch directory changed after spill");
226 directory_ = value;
227 }
228 void clear() noexcept {
229 if (arena_) arena_->release_region(region_);
230 arena_ = nullptr;
231 if (fd_ >= 0) {
232 const int result = ::close(fd_);
233 if (result == 0 && quota_) quota_->closed();
234 }
235 fd_ = -1;
236 quota_.reset();
237 clean_bytes_read_ = bytes_written_since_sync_ = 0;
238 // resource_ was validated at bind. Clearing reuses the same allocation
239 // and binding; a later empty-owner bind explicitly restores disk mode.
240 if (slab_) slab_->reset(resource_);
241 }
242 bool opened() const noexcept { return fd_ >= 0 || arena_ != nullptr; }
243 bool arena_backed() const noexcept { return arena_ != nullptr; }
244 // Non-borrowing disk-backed file test; descriptor() borrows for real fd use.
245 bool disk_backed() const noexcept { return fd_ >= 0; }
246 int descriptor() const noexcept {
247 if (quota_) quota_->borrow_descriptor();
248 return fd_;
249 }
250 // Physical occupancy diagnostic, not logical row identity. Arena values
251 // include chunk rounding; disk values are the anonymous file's extent.
252 std::uint64_t storage_bytes() const {
253 if (slab_) throw std::logic_error("scratch occupancy requires a file or arena");
254 if (arena_) return region_.chunks.size() * ScratchArena::kChunkBytes;
255 if (fd_ < 0) return 0;
256 struct stat info{};
257 int result;
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);
262 }
263 // Rebase a positional file after its owner has proved a complete head
264 // chunk dead. The owner translates its logical offsets; no borrowed view
265 // escapes. Arena mode returns the chunks through the existing allocator.
266 // Disk mode moves the surviving suffix in fixed windows, then truncates;
267 // it works on disk filesystems without COLLAPSE_RANGE support as well.
268 void release_prefix(std::uint64_t bytes) {
269 if (bytes == 0) return;
270 if (slab_) throw std::logic_error("scratch head release requires a file or arena");
271 if (bytes % ScratchArena::kChunkBytes != 0)
272 io_error(EINVAL, "scratch head release alignment");
273 if (arena_) {
274 arena_io([&] { arena_->release_prefix(region_, bytes); });
275 return;
276 }
277 const auto extent = storage_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);
286 offset += count;
287 }
288 truncate(kept);
289 }
290 void truncate(std::uint64_t bytes) {
291 if (slab_) throw std::logic_error("scratch truncate requires a file or arena");
292 check_extent(bytes, 0, arena_ != nullptr, io_raiser_);
293 if (arena_) {
294 arena_io([&] { arena_->truncate(region_, bytes); });
295 return;
296 }
297 std::optional<ScratchFileQuota::Growth> reservation;
298 if (quota_) reservation.emplace(*quota_, bytes);
299 int result;
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");
303 if (reservation) {
304 reservation->truncated(bytes);
305 reservation->finish();
306 }
307 }
308 void open(const char* prefix) {
309 if (slab_) return;
310 if (opened()) return;
311 if (auto* arena = ScratchArena::attached()) {
313 arena_io([&] { arena->open_region(region_); });
314 arena_ = arena;
315 return;
316 }
317 std::string refusal;
318 int directory_error = 0;
319 std::string name = resolve_scratch_directory(directory_, refusal, directory_error);
320 if (name.empty()) {
321 // Keep the resolver's errno and the established typed raiser for
322 // bounded and unbounded stores alike.
323 io_error(directory_error, refusal.c_str());
324 }
325 const auto create_message = scratch_create_message("scratch create", name, refusal);
326 if (name.back() != '/') name += '/';
327 name += prefix;
328 name += "-XXXXXX";
329 if (mode() == QuotaMode::managed) {
330 const auto artifact = region_.store == ScratchStore::curve
332 quota_ = std::make_unique<ScratchFileQuota>(artifact);
333 }
334 const int opened = ::mkstemp(name.data());
335 if (opened < 0) {
336 const int error = errno;
337 if (quota_) quota_->create_failed();
338 quota_.reset();
339 io_error(error, create_message.c_str());
340 }
341 try {
342 if (quota_) {
343 const int error = quota_->created(opened);
344 if (error != 0) io_error(error, "scratch backing");
345 }
346 } catch (...) {
347 ::close(opened);
348 ::unlink(name.c_str());
349 quota_.reset();
350 throw;
351 }
352 if (::unlink(name.c_str()) != 0) {
353 const int error = errno;
354 ::close(opened);
355 quota_.reset();
356 io_error(error, "scratch unlink");
357 }
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();
363 quota_.reset();
364 io_error(error, "scratch cloexec");
365 }
366 fd_ = opened;
367 if (bounded_cache_) {
368#if defined(__linux__)
369 struct statfs backing{};
370 if (::fstatfs(fd_, &backing) != 0) {
371 const int error = errno;
372 clear();
373 io_error(error, "scratch backing");
374 }
375 // These are memory filesystems, not an external row source.
376 if (scratch_filesystem_is_memory(backing.f_type)) {
377 clear();
378 io_error(ENODEV, "scratch requires disk backing");
379 }
380 const int error = ::posix_fadvise(fd_, 0, 0, POSIX_FADV_RANDOM);
381 if (error != 0) {
382 clear();
383 io_error(error, "scratch random access");
384 }
385#elif defined(__APPLE__)
386 if (::fcntl(fd_, F_NOCACHE, 1) == -1) {
387 const int error = errno;
388 clear();
389 io_error(error, "scratch uncached access");
390 }
391#endif
392 }
393 }
394 // Explicit arena-only path. Reserve the whole chunk directory now, before
395 // the first write; max_bytes may be zero (a sub-chunk arena share).
396 // Ordinary open() and all existing report regions keep their policy.
397 void open_bounded(const char* /*prefix*/, std::uint64_t max_bytes) {
398 if (slab_)
399 throw std::logic_error("bounded scratch requires an arena region");
400 if (opened()) {
401 if (!arena_ || region_.max_bytes != max_bytes)
402 throw std::logic_error("bounded scratch policy changed after open");
403 return;
404 }
406 if (!arena) throw std::logic_error("bounded scratch requires an attached arena");
407 check_extent(max_bytes, 0, true, io_raiser_);
409 arena_io([&] { arena->open_region(region_, max_bytes); });
410 arena_ = arena;
411 }
412 // Monotone, zero-filled growth, for sparse hash indexes and similar
413 // ftruncate users. Does not shrink or open the file. Arena mode claims
414 // chunks without a sizing syscall; file mode uses fstat + ftruncate.
415 // A bounded caller can supply the arena size required for this extent.
416 // Only an exhaustion uses it; zero preserves report users' chunk demand.
417 void ensure_size(std::uint64_t bytes, std::uint64_t needed_bytes_lower_bound = 0) {
418 if (slab_) {
419 slab_->extend(resource_, bytes);
420 return;
421 }
422 check_extent(bytes, 0, arena_ != nullptr, io_raiser_);
423 if (!opened()) throw std::logic_error("scratch growth before open");
424 if (arena_) {
425 arena_io([&] { arena_->ensure_size(region_, bytes, needed_bytes_lower_bound); });
426 return;
427 }
428 std::optional<ScratchFileQuota::Growth> reservation;
429 if (quota_) reservation.emplace(*quota_, bytes);
430 struct stat status{};
431 int result;
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();
436 return;
437 }
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");
441 if (reservation) {
442 reservation->truncated(bytes);
443 reservation->finish();
444 }
445 }
446 static void check_extent(std::uint64_t offset, std::size_t length, bool arena = false,
448 const auto maximum = static_cast<std::uint64_t>(std::numeric_limits<off_t>::max());
449 if (offset > maximum || length > maximum - offset) {
450 if (raiser == IoRaiser::auxiliary)
451 throw AuxiliaryScratchIoError(EFBIG, "scratch offset exhausted");
452 throw ReportScratchIoError(EFBIG, "scratch offset exhausted",
453 {{"reason", arena ? "arena_io" : "scratch_io"}});
454 }
455 }
456 // One positional transfer for adapters that already own a retry loop and
457 // observe actual short transfers (IdPairScratchFile's fixture counters).
458 // File mode returns the syscall result/errno, including EINTR; arena/slab
459 // mode copies the same prefix or throws its existing typed failure.
460 // No descriptor escape: even fixture prefix writes select the arena.
461 ssize_t write_some(std::uint64_t offset, const void* data, std::size_t length) {
462 check_extent(offset, length, arena_ != nullptr, io_raiser_);
463 // This window-edge cap is inert for current users: IdPairScratchFile
464 // does not enable bound_cache(), and cache-bound users call write().
465 const auto count = std::min(length, bounded_cache_
466 ? kCacheBytes - static_cast<std::size_t>(offset % kCacheBytes)
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); });
470 else {
471 std::optional<ScratchFileQuota::Growth> reservation;
472 if (quota_) reservation.emplace(*quota_, length == 0 ? 0 : offset + length);
473 prepare_write();
474 const auto chunk = std::min(count, bounded_cache_
475 ? kCacheBytes - bytes_written_since_sync_
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();
481 }
482 if (written > 0 && bounded_cache_)
483 bytes_written_since_sync_ += static_cast<std::uint32_t>(written);
484 return written;
485 }
486 return static_cast<ssize_t>(count);
487 }
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_);
490 // This window cap is likewise inert for current users: IdPairScratchFile
491 // leaves bound_cache() off, and cache-bound users call read().
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); });
496 else {
497 // Retry a failed discard before admitting another read batch.
498 if (bounded_cache_ && clean_bytes_read_ == kCacheBytes) discard_clean_cache();
499 const auto chunk = std::min(count, bounded_cache_
500 ? kCacheBytes - clean_bytes_read_
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();
506 }
507 return received;
508 }
509 return static_cast<ssize_t>(count);
510 }
511 void write(std::uint64_t offset, const void* data, std::size_t length) {
512 if (slab_) {
513 slab_->write_at(resource_, offset, data, length);
514 return;
515 }
516 check_extent(offset, length, arena_ != nullptr, io_raiser_);
517 if (arena_) {
518 arena_io([&] { arena_->write(region_, offset, data, length); });
519 return;
520 }
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) {
526 prepare_write();
527 const auto chunk = std::min(length - written, bounded_cache_
528 ? kCacheBytes - bytes_written_since_sync_
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);
537 }
538 if (reservation) reservation->finish();
539 }
540 void read(std::uint64_t offset, void* data, std::size_t length) const {
541 if (slab_) {
542 slab_->read_at(resource_, offset, data, length);
543 return;
544 }
545 check_extent(offset, length, arena_ != nullptr, io_raiser_);
546 if (arena_) {
547 arena_io([&] { arena_->read(region_, offset, data, length); });
548 return;
549 }
550 auto* bytes = static_cast<unsigned char*>(data);
551 std::size_t received = 0;
552 while (received < length) {
553 // Retry a failed discard before admitting another read batch.
554 if (bounded_cache_ && clean_bytes_read_ == kCacheBytes) discard_clean_cache();
555 const auto chunk = std::min(length - received, bounded_cache_
556 ? kCacheBytes - clean_bytes_read_
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();
566 }
567 }
568 }
569
570private:
571 template<class Operation> void arena_io(Operation operation) const {
572 try { operation(); }
573 catch (const ReportScratchIoError& error) {
574 const auto& failure = error.run_failure();
575 // The arena's capacity/backing contract is unchanged. I/O raised
576 // for an auxiliary table retains that table's code and errno.
577 if (io_raiser_ == IoRaiser::auxiliary
578 && failure.code == RunFailureCode::report_sink_failed
579 && failure.args && *failure.args == "{\"reason\":\"arena_io\"}")
580 throw AuxiliaryScratchIoError(error);
581 throw;
582 }
583 }
584 [[noreturn]] void io_error(int error, const char* operation) const {
585 if (io_raiser_ == IoRaiser::auxiliary)
586 throw AuxiliaryScratchIoError(error, operation);
587 throw ReportScratchIoError(error, operation, {{"reason", "scratch_io"}});
588 }
589 // Bound traffic between whole-file discards: at most kCacheBytes of reads
590 // and kCacheBytes of unsynced writes per file, independent of offset span.
591 // With RANDOM disabling read-ahead, the current users on 4 KiB OS pages
592 // have much smaller per-window footprints: TradeLedger's 168 B records
593 // and short strings touch at most about 391 pages; ScratchSequence's
594 // whole 64 KiB windows touch at most about 17 pages. The universal
595 // worst-case bound across the read and unsynced-write windows is
596 // 2*kCacheBytes OS pages for one-byte accesses to distinct pages:
597 // 512 MiB at 4 KiB pages, not 128 KiB of physical cache. DONTNEED retains
598 // any still-dirty pages; the write counter bounds those until the next
599 // sync and discard.
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");
605#endif
606 clean_bytes_read_ = 0;
607 }
608 void prepare_write() {
609 if (!bounded_cache_ || bytes_written_since_sync_ < kCacheBytes) return;
610#if defined(__linux__)
611 int result;
612 do { result = ::fdatasync(fd_); } while (result != 0 && errno == EINTR);
613 if (result != 0) io_error(errno, "scratch flush cache window");
614#endif
615 // Only the successful byte count triggers sync; a far-back update
616 // followed by a tail append cannot flush once per offset change.
617 discard_clean_cache();
618 bytes_written_since_sync_ = 0;
619 }
620 int fd_ = -1;
621 // Group policy scalars in existing padding: the fixed fold-segment
622 // arrays embed this type and retain their unchanged 32 KiB assertion,
623 // including ScratchArena::Region::max_bytes for bounded command scratch.
624 IoRaiser io_raiser_ = IoRaiser::report_sink;
625 std::string directory_;
626 // Physical backing only; logical table records and hash folds are shared
627 // by both backends. No region base/payload pointer is retained here.
628 std::shared_ptr<ScratchSlab> slab_;
630 bool bounded_cache_ = false;
631 ScratchArena* arena_ = nullptr;
632 ScratchArena::Region region_;
633 // Each transfer is capped at the remaining 64 KiB cache window. These
634 // counters stay in [0, kCacheBytes]; two 32-bit counters leave room for
635 // the quota pointer without enlarging the original unmanaged footprint.
636 mutable std::uint32_t clean_bytes_read_ = 0;
637 std::uint32_t bytes_written_since_sync_ = 0;
638 // All per-file quota state lives behind this one pointer, allocated only
639 // for a managed open. Empty in unmanaged mode: no allocation or traffic.
640 std::unique_ptr<ScratchFileQuota> quota_;
641};
642} // namespace pineforge::detail
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
void truncated(std::uint64_t bytes) noexcept
void wrote(std::uint64_t end) noexcept
ScratchFileQuota(QuotaArtifact artifact)
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