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:

  • Segment represents a segment identifier, its memory and the small amount of state needed for cache residency.
  • SegmentCache manages 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.