Reduce EloqKV cold-read handoff overhead - #538
Conversation
Walkthrough
ChangesFetch request pooling and value transfer
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tx_service/src/cc/cc_req_misc.cpp (1)
793-864: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsolidate
FetchRecordCcinitialization intoReset().The parameterized constructor duplicates the field-assignment path that pooled requests now use through
FetchRecordCc() + Reset(). Remove the constructor if it has no call sites, or delegate it to default construction plusReset()so field initialization has a single source of truth.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tx_service/src/cc/cc_req_misc.cpp` around lines 793 - 864, Consolidate FetchRecordCc initialization by removing the parameterized constructor if it has no call sites; otherwise make it delegate to default construction and Reset(). Keep Reset() as the single source of truth for all field assignments, including shard ownership and request state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tx_service/include/cc/cc_req_misc.h`:
- Around line 88-94: Document the default-construction and reset invariants in
the FetchCc and FetchRecordCc declarations: state that default-constructed
instances have a null ccs_, must not invoke methods that dereference it until
Reset() completes, and that Reset() binds the request to the resetting shard
without migration. Add concise comments at both default constructors and the
Reset() declaration, covering the pool-reuse contract without changing behavior.
---
Nitpick comments:
In `@tx_service/src/cc/cc_req_misc.cpp`:
- Around line 793-864: Consolidate FetchRecordCc initialization by removing the
parameterized constructor if it has no call sites; otherwise make it delegate to
default construction and Reset(). Keep Reset() as the single source of truth for
all field assignments, including shard ownership and request state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 334b56e7-ee64-4f2b-8fc3-4834a802fdc3
📒 Files selected for processing (5)
store_handler/data_store_service_client_closure.cpptx_service/include/cc/cc_req_misc.htx_service/include/cc/cc_shard.htx_service/src/cc/cc_req_misc.cpptx_service/src/cc/cc_shard.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
- store_handler/data_store_service_client_closure.cpp
| // in flight. The pool owns the requests and the flat map only indexes the | ||
| // active single-flight fetch for each entry. | ||
| absl::flat_hash_map<LruEntry *, FetchRecordCc *> fetch_record_reqs_; | ||
| CcRequestPool<FetchRecordCc> fetch_record_cc_pool_; |
There was a problem hiding this comment.
Any reason you made this change? the comment itself does not justify the reason. std::unordered_map provides pointer stability.
There was a problem hiding this comment.
to reuse FetchRecordCc.
Context
EloqKV cold reads copied the completed serialized value from
ReadClosureintoFetchRecordCc::rec_str_beforeBackFilldeserialized it into the CCMappayload. Each new single-flight fetch also constructed a
FetchRecordCcinsidean
std::unordered_mapnode and destroyed it when the fetch completed.For 1–4 KiB values, the value copy adds one full-value memcpy and temporarily
keeps two serialized copies alive. The short-lived fetch object and map node
also add allocator traffic, including the first allocation of its requester
vector.
Behavior before and after
Before this change, a successful EloqKV read copied the serialized value into
FetchRecordCc, and every new active fetch allocated map/object state that wasfreed at completion.
After this change:
FetchRecordCc;FetchRecordCcobjects fromCcRequestPool;absl::flat_hash_map<LruEntry *, FetchRecordCc *>indexes active fetcheswithout allocating one node per fetch after the table reaches capacity; and
FetchRecordCcmember reordering reducessizeof(FetchRecordCc)from 384 to368 bytes on the current 64-bit build.
The Redis-visible value, TTL semantics, record timestamp, coalescing behavior,
error handling, range-partition decoding, durability, and recovery behavior are
unchanged.
Allocation accounting
This change does reduce allocations after pool/map warm-up: it reuses the
FetchRecordCcobject, itsrequesters_capacity, and the active-fetch hashtable storage instead of allocating an
unordered_mapnode containing a newobject for each cold fetch.
It does not eliminate the local serialized-value allocation. Moving
ReadClosure::value_intorec_str_gives up the closure's reusable buffer, soa later local EloqStore read allocates a new buffer when filling that closure.
The local-path improvement is one fewer full-value memcpy and one fewer
simultaneously live serialized copy, not one fewer steady-state value-buffer
allocation.
BackFillalso still deserializes into the final CCMap payload, sothat final allocation/copy remains.
For remote reads, cleanup already releases the protobuf response storage.
Moving its string also avoids the separate destination allocation previously
performed by
rec_str_.assign(...).Implementation
ReadClosure::TakeValue()as the explicit consuming API for local andRPC-backed reads.
retains the existing
string_viewflow.FetchRecordCcresettable and pool it perCcShard.are in flight; the flat map is only a non-owning active-fetch index.
capacity. Archive result storage is released to avoid retaining an
unexpectedly large vector indefinitely.
Concurrency and lifetime
The pool and active-fetch map are owned by the same
CcShard. A request nevermigrates between shards, and
Reset()asserts that invariant. Removing a fetcherases the map entry before returning the request to the pool. Although
Free()can run while
FetchRecordCc::Execute()is unwinding, new fetches and resumedrequesters execute serially on the owning shard, so
NextRequest()cannot reusethat object until control returns to the shard loop.
Reopen operations retain the same active request and do not return it to the
pool. The data-store callback has completed before the request is removed, so no
callback can retain a pointer to a freed/reused request.
Test plan
Commands and results:
Cold-read comparison with the same 16-core EloqKV / 16-core memtier setup and
1–4 KiB values:
The approximately +0.04% QPS difference is within run-to-run noise; this test
does not demonstrate a measurable performance gain from request pooling. It
does show that these control-object allocations are not the dominant bottleneck
in the tested workload. The ownership move remains useful because it removes a
full-value copy.
No standalone TCL suite or dedicated remote/range/reopen/failover test was run.
Risk assessment
The main regression surface is pooled request lifetime and reset completeness.
The pool keeps a small baseline of eight
FetchRecordCcobjects per shard andcan retain requester/string capacities reached by prior requests. Large archive
vectors are explicitly released. The flat map reserves 64 active-fetch slots
per shard to avoid hot-path rehashing.
FetchCcnow stores its shard as a pointer so pooled instances can be defaultconstructed; existing non-pooled subclasses continue to initialize it through
the same constructor. This is an internal in-process layout change with no wire,
storage-format, locking, or durability impact.
Rollback plan
Revert this PR. The first restores copying the read result;
the second restores per-fetch construction in
std::unordered_map.Reviewer guide
FetchRecordCallbackandReadClosure::TakeValue()for the ownershiptransfer and local/remote allocation distinction.
CcShard::FetchRecord()andRemoveFetchRecordRequest()forsingle-flight indexing, pool lifetime, and same-shard serialization.
FetchRecordCc::Reset()for complete state reset across retries,reopen, archive reads, and errors.
Follow-up work
Reducing the remaining 1–4 KiB local value-buffer allocation requires a broader
handoff design, such as filling a reusable consumer-owned buffer or pooling the
serialized buffer separately across the asynchronous fetch/backfill lifetime.
Summary by CodeRabbit