71 Segment Cache
Purpose
The Version 2 SegmentCache will be similar to the current segment
cache in its management of resident segment buffers, access counts and eviction. Its boundary will
be narrower: it manages segments in memory, but it does not perform RAS I/O. Loading and writing are
coordinated above the cache.
A cache lookup is a memory operation, whereas loading a segment requires storage I/O. Keeping these responsibilities separate makes the I/O explicit and keeps the cache interface concerned only with resident memory.
No upward reference to LSS
Neither SegmentCache nor
Segment retains an LSS&. The
LSS is the composition root and coordinator; its lower-level cache and segment objects must not use it
as a service locator to reach the RAS, settings, error state or other LSS modules.
The cache can instead be constructed from the values it actually needs, such as the segment size and
maximum number of resident segments. A segment needs its SegId, its
buffer and the small amount of state required for cache residency. This makes both classes
independently understandable and testable.
class SegmentCache
{
public:
SegmentCache(std::size_t segmentSize, std::size_t maximumResidentSegments);
// ...
};
class Segment
{
public:
SegId GetSegId() const;
std::span<octet_t> GetBuffer();
std::span<const octet_t> GetBuffer() const;
// ...
};
The signatures are illustrative. In particular, the buffer may use an internal view type rather than
std::span. The architectural requirement is that the segment owns
or refers to memory; it does not know how that memory is transferred to or from persistent storage.
Segment does not perform I/O
The following methods on the current Segment will not exist:
void ReadFromRAS();
void WriteToRAS();
void WriteSectionToRAS(int i1, int i2);
A segment is an in-memory object, not an active storage object. Removing these methods also removes
its need to find the RAS through LSS&. Code coordinating a write
obtains a view of the relevant bytes from the segment and submits that view to the RAS. Code
coordinating a read arranges for the RAS to fill the final cache buffer directly.
Cache-only lookup
SegmentCache::GetSegment(SegId segid) only looks for the segment
in the cache. It never reads from the RAS, starts a RAS request, waits for I/O or arranges for another
module to load the segment. If the requested segment is not resident and available, the call reports
a cache miss by returning an empty SegmentAccessor.
SegmentAccessor SegmentCache::GetSegment(SegId segid);
It may be preferable to name this operation FindSegment() or
TryGetResidentSegment(), because those names make the possibility
of a cache miss explicit. Regardless of its final name, lookup has cache-only semantics.
A successful lookup returns an accessor which pins the segment so it cannot be evicted while the caller uses it.
SegmentAccessor
SegmentAccessor represents a pin giving access to a resident
segment. It is a move-only RAII type: constructing it acquires a pin, destroying it releases that pin,
and moving it transfers responsibility for the pin to the destination. Copy construction and copy
assignment are disabled so an accidental copy cannot silently create or duplicate a pin.
class SegmentAccessor
{
public:
SegmentAccessor() = default;
~SegmentAccessor() { Reset(); }
SegmentAccessor(const SegmentAccessor&) = delete;
SegmentAccessor& operator=(const SegmentAccessor&) = delete;
SegmentAccessor(SegmentAccessor&& other) noexcept;
SegmentAccessor& operator=(SegmentAccessor&& other) noexcept;
explicit operator bool() const { return segment_ != nullptr; }
Segment& operator*() const;
Segment* operator->() const;
void Reset();
private:
friend class SegmentCache;
SegmentAccessor(SegmentCache& cache, Segment& segment);
SegmentCache* cache_ = nullptr;
Segment* segment_ = nullptr;
};
An empty accessor represents a cache miss. A non-empty accessor can be passed between functions by move without changing the underlying segment address or releasing its pin:
auto segment = cache.GetSegment(segid);
if (!segment)
StartSegmentLoad(segid);
else
ContinueRead(std::move(segment));
The moved-from accessor becomes empty. Exactly one accessor remains responsible for releasing that
particular pin. If a second independent pin is needed, it must be acquired explicitly from the cache
or through an explicitly named operation such as Clone(); ordinary
copy syntax must not conceal the additional cache access count.
Lifetime and access rules
The SegmentCache must outlive every accessor which refers to it. If
that ordering cannot be guaranteed structurally, accessors and the cache must share a separate
control block whose lifetime extends until the final accessor is destroyed. Move operations should
be noexcept so accessors can be stored and transferred reliably.
If segments can have concurrent readers but mutation requires exclusive access, the implementation should distinguish read-only and mutable access rather than allowing every accessor to return an unrestricted mutable reference. This can use separate accessor types or explicit operations on the cache. Pinning prevents eviction; it does not by itself provide synchronisation for concurrent access to the bytes.
Writing segments
Writing is also coordinated outside Segment and
SegmentCache. The writer determines the RAS offset and byte range,
obtains a view of the segment buffer and passes that view to the storage layer.
A section write uses a subview of the buffer rather than a method which lets the segment call the RAS. The segment buffer must remain alive and unchanged while the storage layer refers to it. Eviction cannot recycle the segment buffer during that interval.
I/O and integrity errors
There is no recovery from a segment read failure within the cache or loading state machine. Any RAS read or write error puts the open LSS into its zombie state, as does an integrity failure discovered while validating a segment. The LSS stops logical processing and does not retry the read, substitute another segment or resume waiting operations.
Responsibilities
The resulting division of responsibilities is:
-
Segmentrepresents a segment identifier, its memory and the small amount of state needed for cache residency. -
SegmentCachemanages lookup, allocation, pinning, access counts, capacity and eviction of segment buffers. - The LSS decides when I/O is required and coordinates it outside the cache.
- The storage layer transfers bytes between persistent storage and buffers supplied by the LSS.
This boundary keeps memory management separate from storage operations and removes hidden coupling from the lowest-level segment classes back to the complete LSS.