55 Fully asynchronous LSS

Status: unimplemented proposal. This proposal makes storage I/O asynchronous throughout the LSS, including at its public API, so an LSS does not require a collection of threads which block while I/O is in progress.

Motivation

The LSS already separates most background work into LazyWriter, LazyFlusher, LazyCheckPointer and LazyCleaner. Their clients normally signal that work is needed and continue; they do not need the work to finish before making progress. These components are therefore natural asynchronous tasks rather than reasons to dedicate threads to an LSS.

The current asynchronous task wrappers make invocation asynchronous, but the invoked function can still occupy an io_context thread for the entire duration of a blocking file operation. Instead, an operation should submit I/O, return to the executor and resume when its completion is delivered. This is particularly important on platforms such as WebAssembly where threads may be unavailable or undesirable and storage is naturally exposed through asynchronous interfaces.

Transaction commit remains an in-memory operation

Closing an LSS transaction does not provide per-transaction synchronous durability. It commits the transaction to the in-memory log and need not perform any I/O. This fast path should remain immediate, even when represented by an asynchronous public API: an operation which has no reason to suspend may complete immediately.

The proposed design removes FlushWhenClose(). Waiting for a storage flush on every selected transaction would complicate the normal execution model for a facility that is not central to the intended use of the LSS. Applications interested in persistence should instead observe the asynchronous durability progress described below.

Asynchronous storage interface

An asynchronous counterpart or replacement for IRAS should report the completion of reads, writes, resizing and persistence barriers without blocking the calling thread. The exact C++ representation could be an awaitable or a completion handler; the important property is that the LSS can suspend an operation and resume it on its serialized executor.


struct IAsyncRAS
{
    virtual AsyncResult<RASSize> GetSize() = 0;
    virtual AsyncResult<void> Read(octet_t* buffer, RASOffset offset, ssize_t size) = 0;
    virtual AsyncResult<void> Write(const octet_t* buffer, RASOffset offset, ssize_t size) = 0;
    virtual AsyncResult<void> SetSize(RASSize size) = 0;
    virtual AsyncResult<void> Flush() = 0;
};

This is illustrative rather than a proposed final signature. Buffers passed to an operation must remain alive until its completion. The segment queues already provide much of the ownership needed for outstanding segment writes.

Platform implementations can map the interface to native asynchronous facilities, WebAssembly storage promises, or, as a compatibility fallback, blocking file operations performed by a shared worker facility. The fallback may use threads, but the LSS itself does not require a set of threads per store.

Existing lazy activities

LazyWriter
Submit writes for queued segment ranges, retain each segment until completion, and advance the written log position across the contiguous prefix of successful writes.
LazyFlusher
Periodically request writer progress and, when required by the persistence policy, submit a storage persistence barrier. It should not occupy an executor thread while that barrier is pending.
LazyCheckPointer
Construct check-point information in memory, request that the required log prefix be written, and continue with the root-block-division write after the prerequisite completions have arrived.
LazyCleaner
Perform cleaning incrementally, resuming its reads and writes from completion notifications and yielding between units of work.

These activities can share one serialized LSS executor. Storage operations may be outstanding concurrently, while all changes to logical LSS state are applied in a well-defined order on that executor. This can also replace a significant amount of mutex-based coordination.

Asynchronous public API

The public LSS API should not synchronously wait for I/O. Operations which may need storage include opening and recovery, loading an uncached segment, applying back-pressure when no reusable segment buffer is available, explicit maintenance operations, and closing the store. Such operations should complete asynchronously. Transaction operations which touch only resident memory can complete immediately through the same API.

Closing an LSS must itself be asynchronous. It stops the creation of new work, allows or cancels outstanding operations according to a documented policy, and completes only when no completion can subsequently access the LSS. I/O errors must be reported to the affected operation and retained as store state where they also affect later operations.

Durability notification by TSN

Rather than making a transaction wait for durability, the LSS should notify an application when a prefix of committed transactions is known to be durable. Since transactions are serialized, this can be represented by a monotonically increasing transaction serial number (TSN):


using DurableTxnCallback = std::function<void(TSN durableTsn)>;

void SetDurableTxnCallback(DurableTxnCallback callback);
TSN GetDurableTSN() const;

A notification of TSN n means that every committed transaction with a TSN less than or equal to n is known to be recoverable after the documented class of storage failure. A single notification can therefore cover many transactions. GetDurableTSN() allows a caller to handle the case where the TSN of interest became durable before it registered its interest or processed a notification.

The durable watermark advances only over a contiguous log prefix, even if asynchronous writes complete out of order. Completion of a buffered write is not necessarily evidence of durability under abrupt power loss. The watermark must be advanced according to the persistence guarantee of the underlying RAS, normally after the applicable asynchronous flush or barrier has completed.

A check point is not itself the definition of transaction durability: recovery can scan valid log data following the last valid check point. Check-point publication nevertheless has a strict dependency. All log data and metadata referenced by a new root-block division must have completed the required writes before that division is submitted. Expressing the root-block write as a continuation of those completions makes this ordering explicit without blocking a thread.

Back-pressure and ordering

Asynchronous I/O does not imply unbounded buffering. When the segment cache cannot supply another buffer, the transaction operation requesting one should remain pending until writer progress releases a segment. This applies back-pressure without blocking a thread. Cancellation and shutdown must not release buffers which are still referenced by outstanding I/O.

Writes may be issued concurrently where their logical independence permits it, but completion order must not be confused with publication order. Written and durable log positions, reusable buffers, check-point state and the durable TSN are advanced only when their respective contiguous prerequisites have completed successfully.