Repository navigation
perf(storage): use direct awaitables for value and extent reads - #313
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughValue and extent reads now use awaitables to manage buffer waits, I/O, validation, and retries. The change adds tests for these paths and updates architecture documentation. A recovery test now checks specifically for the record-scan fallback message. ChangesStorage read awaitables
Recovery log assertion
Priority: ⬇️ Low Estimated code review effort: 4 (Complex) | ~45 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant CallerTask
participant LoadValueAwaitable
participant RegisteredBufferPool
participant DiskIO
participant LoadValueSlow
CallerTask->>LoadValueAwaitable: Start value read
LoadValueAwaitable->>RegisteredBufferPool: Acquire read buffer
RegisteredBufferPool-->>LoadValueAwaitable: Assign buffer lease
LoadValueAwaitable->>DiskIO: Submit record read
DiskIO-->>LoadValueAwaitable: Complete read
LoadValueAwaitable->>LoadValueSlow: Retry after relocation
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This change moves value and extent reads onto direct awaitables in the caller's coroutine. Review found no confirmed defects that block merging. As a follow-up, the owner may want to confirm that cross-owner extent reads always resume their caller through the runtime bridge. Pre-merge checks |
|
Context
Ordinary value reads enter
LoadValueandLoadValueLocalTasks before submitting I/O. External values additionally create routing and physical-read Tasks for each extent. These wrapper frames can be replaced with awaitables that suspend the existing caller directly.Behavior before and after
Callers retain
co_await LoadValue(...)andco_await LoadValueLocal(...). Both functions return a shared ordinary C++ awaitable. Local inline reads and staged copies create no loader coroutine frames, including when the bounded read-buffer pool is exhausted. This applies to GET, writes, collection readers, replication, and backup.Grouped values enter
LoadGroupedValuedirectly without aLoadValueSlowforwarding frame. Whole-extent and slice reads share another awaitable: local routing and physical reading create no child task, and cross-owner routing uses the existing runtime task bridge without a surrounding routing coroutine.Multi-step representation loading and relocation retries retain lazy helper Tasks. Logical reads follow same-mutation relocations before returning; physical reads report stale locations to their enclosing retry loop. Transaction admission and device routing remain unchanged.
Implementation
LoadValueandLoadValueLocalordinary factories forLoadValueAwaitable; GET keeps a single loader call.LoadValueLocalWaiting.ReadExtentAwaitable, preserving the physical pin across buffer waiting, I/O, and validation. Slice reads validate the entire extent before copying requested bytes. Remove the unusedReadExtentIntowrapper.falling back to record scan, excluding unrelated kernel receive-buffer fallback messages.Design decisions and alternatives
Awaitable state resides in the existing caller's frame; eliminating child frames does not eliminate the state itself. This does not depend on compiler coroutine-allocation elision. Pool continuations use the runtime's existing scheduled-work mechanism on the same worker without changing the Bycorf submodule. Cross-owner dispatch retains the runtime's Task bridge, and complex graph/relocation flows retain their orchestration Tasks.
Documentation and comments
Updated
docs/architecture/04-storage-and-recovery.mdto describe direct value/extent reads, pool continuation ownership, and manifest-order external assembly. API and implementation comments cover handoff lifetime, scheduling, physical pins, grouped argument lifetime, and slice validation.Test plan
Passed all 832 unit tests, including 13 awaitable tests and 10 buffer-pool tests, plus all four storage end-to-end suites. Focused tests exercise FIFO handoff, background continuation, pool-to-disk submission, population invalidation before completion and after scheduling, relocation during buffer waiting, extent slice CRC before copying, pin/lease cleanup, and error handling. Existing end-to-end coverage includes physical-owner reassignment and indirect/grouped/external representations.
Also passed concurrent GET/SET/MULTI, FLUSHDB, and cold recovery using a temporary copy of an existing test script against this branch's binary:
git show perf/remote-get-awaitable:tests/remote_get_e2e.py > /tmp/lavik-direct-get-e2e.py env NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost PYTHONPATH=/home/ubuntu/lavik/tests python3 /tmp/lavik-direct-get-e2e.py build_debug/lavikThat additional script comes from a separate local branch and is not part of this PR.
Risk assessment
The sensitive changes are pin/lease lifetime, buffer continuation handoff, and selecting retry continuations before returning to the caller. Tests cover these boundaries and affected storage representations. Shutdown retains the runtime's existing drain-before-destruction requirement. Validation used io_uring; SPDK hardware was unavailable. Throughput of the latest head has not been measured here; the target host and billion-key dataset are unavailable in this environment. Earlier target-host results for bc13523 do not validate the throughput of this head or isolate the cost of completion-side parsing.
Rollback plan
Revert this PR. No configuration, wire-format, or durable-format changes.
Reviewer guide
Start with the value/extent factories and awaitables in
read.cpp, thenAcquireReadAwaiter::SuspendWithand pool release inbuffer_pool.cpp, and the focused tests. The branch is based onmainand does not include the separate connection-worker remote-read feature.Follow-up work
Compare throughput and latency against
mainon the target hardware.