Widen compaction service counts to full kNumOfReasons#14924
Conversation
…s array (#14807) Summary: The "counts" array in CompactionServiceResult (one compaction count per CompactionReason) is non-critical information. During cross-process remote compaction the primary and the remote worker can run different RocksDB versions and therefore disagree on kNumOfReasons, so the serialized counts array can have a different length than the reader expects. The plain OptionTypeInfo::Array parser rejects any element-count mismatch, which fails deserialization of the entire result over this non-critical field. This makes deserialization of "counts" size-tolerant via SizeTolerantDeserializeArray, which overrides only the parse callback: extra trailing elements are dropped and missing trailing elements are zero-filled, so a length mismatch no longer fails deserialization. Serialization and comparison are inherited from Array<T, kSize> unchanged. The serialized width is intentionally kept at kNumOfReasons - 1 here, so this change does not alter what is written -- only the read path becomes tolerant. Widening the serialized width to the full kNumOfReasons is done as a separate follow-up change, which must not land until this tolerant read has been deployed to every primary (including rollback targets), so that no reader still rejects a wider result. == Test plan == **Legend** CompactionReason is an enum whose last entry is kNumOfReasons (currently 21). CompactionStats::counts is an int[kNumOfReasons] holding one count per reason; it is serialized into the remote compaction result and read back by the primary. - "narrow" = kNumOfReasons - 1 counts (omits the newest reason); what current code writes. - "wide" = kNumOfReasons counts (includes the newest reason). - strict reader: accepts only its exact expected element count; any other count fails deserialization (the plain OptionTypeInfo::Array behavior). - tolerant reader: accepts any count -- more elements than expected are dropped, fewer are zero-filled; never fails on a count mismatch (this change, SizeTolerantDeserializeArray). **Unit test** CompactionServiceTest.CompatCheckCountsWidthTolerance -- verifies that deserializing "counts" tolerates a serialized element count equal to, one greater than, or one less than the reader's expected width (extras dropped, missing zero-filled), and that the binary serializes exactly the expected number of elements. **Manual cross-version verification (ldb remote_compaction)** Test 1: baseline (unchanged behavior) -- expect-narrow strict-read primary reads narrow counts written by a worker ``` $ strict_narrow remote_compaction_worker --db=$DB --job_dir=$JOB & $ strict_narrow remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4913 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 2: expect-narrow tolerant-read primary reads the narrow counts a worker writes ``` $ strict_narrow remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_narrow remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4912 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 3: expect-narrow tolerant-read primary reads the wide counts a worker writes (extra reason dropped) ``` $ tolerant_wide remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_narrow remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4915 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 4: expect-wide, tolerant primary reads the narrow counts a worker writes (missing reason zero-filled) ``` $ strict_narrow remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4911 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 5: expect-wide, tolerant primary reads a wide counts a worker write ``` $ tolerant_wide remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4917 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Differential Revision: D106321320
Summary: Widen the serialized "counts" array in CompactionServiceResult from kNumOfReasons - 1 back to the full kNumOfReasons, so the count for the newest CompactionReason is included in the remote compaction result. The width was previously reduced by one to match readers that expected the narrower array; restoring it changes what is written. This is safe only because deserialization of "counts" is now size-tolerant (a reader expecting fewer elements drops the extra, one expecting more zero-fills the missing). It must not land until that size-tolerant read has been deployed to every primary, including rollback targets so should be 1-2 releases ahead; otherwise a primary can still or fallback to running the strict reader would reject the wider result and fail deserialization of this field. == Test plan == **Legend** CompactionReason is an enum whose last entry is kNumOfReasons (currently 21). CompactionStats::counts is an int[kNumOfReasons] holding one count per reason; it is serialized into the remote compaction result and read back by the primary. - "narrow" = kNumOfReasons - 1 counts (omits the newest reason); what current code writes. - "wide" = kNumOfReasons counts (includes the newest reason). - strict reader: accepts only its exact expected element count; any other count fails deserialization (the plain OptionTypeInfo::Array behavior). - tolerant reader: accepts any count -- more elements than expected are dropped, fewer are zero-filled; never fails on a count mismatch (this change, SizeTolerantDeserializeArray). **Unit test** CompactionServiceTest.CompatCheckCountsWidthTolerance -- verifies that deserializing "counts" tolerates a serialized element count equal to, one greater than, or one less than the reader's expected width (extras dropped, missing zero-filled), and that the binary serializes exactly the expected number of elements. **Manual cross-version verification (ldb remote_compaction)** Test 1: expect-narrow tolerant-read primary reads wide counts a worker write (extra reason dropped) ``` $ tolerant_wide remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_narrow remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4915 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 2: expect-wide tolerant-read primary reads narrow counts a worker writes (missing reason zero-filled) ``` $ strict_narrow remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4911 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Test 3: expect-wide tolerant-read primary reads wide counts a worker writes ``` $ tolerant_wide remote_compaction_worker --db=$DB --job_dir=$JOB & $ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB remote_compaction_worker: OK (4917 bytes result) remote_compaction_primary: OK $ grep 'remote compaction result' $DB/LOG [default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst ``` => PASS: the primary parsed and installed the remote result. Differential Revision: D111068809
|
@hx235 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D111068809. |
✅ clang-tidy: No findings on changed linesCompleted in 309.7s. |
Codex Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.🟡 Codex Code ReviewAuto-triggered after CI reached the early-review threshold — reviewing commit bbb1d1e ❌ Codex review failed before producing findings. ℹ️ About this responseGenerated by Codex CLI. Limitations:
Commands:
|
Claude Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.✅ Claude Code ReviewAuto-triggered after CI reached the early-review threshold — reviewing commit bbb1d1e SummaryThis PR correctly addresses a real serialization compatibility problem in remote compaction by introducing High-severity findings (0): No high-severity findings. Full review (click to expand)Findings🔴 HIGHNone. 🟡 MEDIUMM1. Unnecessary
|
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---|---|---|---|
| Remote compaction (primary reads) | YES | YES -- tolerant parse handles width mismatch | None |
| Remote compaction (worker writes) | YES | YES -- serializes full kNumOfReasons | None |
| Old primary + new worker | YES | Deserialization may fail with strict reader, BUT fallback to local compaction handles this gracefully (line 192-209) | Deployment ordering matters (see PR description) |
| New primary + old worker | YES | Tolerant reader zero-fills the missing element | None |
| TEST_Equals comparison | YES | Comparison uses kSize=kNumOfReasons for both sides; if one side was deserialized from narrow data, missing entries are zero-filled, so comparison is consistent | None |
| Local compaction | NO | counts[] serialization is only used in remote compaction path | None |
| ReadOnly DB | NO | No compaction | None |
Deployment safety: The PR description correctly identifies that this change should land 1-2 releases after the tolerant reader, so that rollback targets always have the tolerant reader. The fallback-to-local logic provides a safety net even if deployment ordering is violated -- the primary will fall back to local compaction if deserialization fails.
Positive Observations
-
Clean design: Overriding only the parse callback while inheriting serialize/compare from
Array<T, kSize>is elegant and minimally invasive. It avoids duplicating serialization logic. -
Good test coverage: The test covers three key scenarios (same width, wider peer, narrower peer) and verifies both the format (element count) and the semantics (correct values read back).
-
Graceful degradation: Even without the tolerant reader deployed everywhere, the existing fallback-to-local-compaction logic (lines 192-209 in
compaction_service_job.cc) ensures that a deserialization failure doesn't cause data loss or corruption -- it just triggers a local re-compaction. -
Correct use of
ParseVector: Using the existingParseVectorinfrastructure rather than writing custom parsing logic reduces the chance of parsing bugs and maintains consistency with the codebase.
ℹ️ About this response
Generated by Claude Code.
Review methodology: claude_md/code_review.md
Limitations:
- Claude may miss context from files not in the diff
- Large PRs may be truncated
- Always apply human judgment to AI suggestions
Commands:
/claude-review [context]— Request a code review/claude-query <question>— Ask about the PR or codebase
🟡 Codex Code ReviewAuto-triggered after CI passed — reviewing commit bbb1d1e ❌ Codex review failed before producing findings. ℹ️ About this responseGenerated by Codex CLI. Limitations:
Commands:
|
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit bbb1d1e SummaryClean, well-scoped change that correctly solves the cross-version serialization compatibility problem for the High-severity findings (0): No high-severity findings. Full review (click to expand)Findings🔴 HIGHNone. 🟡 MEDIUMM1.
|
| Context | Affected? | Safe? | Notes |
|---|---|---|---|
| Old primary (strict reader) + New worker (wider write) | YES | YES | Deserialization fails; fallback to local compaction (line 192-210) |
| New primary (tolerant reader) + Old worker (narrow write) | YES | YES | Zero-fills missing count |
| ReadOnly DB | NO | N/A | No compaction |
| WritePreparedTxnDB | YES (if remote compaction) | YES | counts is stats-only |
| TEST_Equals comparison | YES | YES | Both sides use same binary's kSize |
Type safety of C-array ↔ std::array cast: The existing OptionTypeInfo::Array<T, kSize> (options_type.h:392) already casts void* to std::array<T, kSize>* for C-array fields. The new code follows the exact same pattern. No new risk.
Serialization symmetry: Write uses inherited SerializeArray<int, kNumOfReasons> (21 elements). Read uses ParseVector<int> + copy loop. Round-trip is correct for same-version peers.
Positive Observations
- Good incremental approach: Deploying tolerant readers first, then widening the write, is the correct strategy for cross-version compatibility.
- Test design: Well-structured, testing same/wider/narrower scenarios and pinning serialized width.
- Minimal blast radius: Only the parse function is overridden; serialize and compare are inherited unchanged.
- Clean TODO removal: The old workaround comment is properly resolved.
ℹ️ About this response
Generated by Claude Code.
Review methodology: claude_md/code_review.md
Limitations:
- Claude may miss context from files not in the diff
- Large PRs may be truncated
- Always apply human judgment to AI suggestions
Commands:
/claude-review [context]— Request a code review/claude-query <question>— Ask about the PR or codebase
Summary:
Widen the serialized "counts" array in CompactionServiceResult from kNumOfReasons - 1 back to the full kNumOfReasons, so the count for the newest CompactionReason is included in the remote compaction result.
The width was previously reduced by one to match readers that expected the narrower array; restoring it changes what is written. This is safe only because deserialization of "counts" is now size-tolerant (a reader expecting fewer elements drops the extra, one expecting more zero-fills the missing). It must not land until that size-tolerant read has been deployed to every primary, including rollback targets so should be 1-2 releases ahead; otherwise a primary can still or fallback to running the strict reader would reject the wider result and fail deserialization of this field.
== Test plan ==
Legend
CompactionReason is an enum whose last entry is kNumOfReasons (currently 21). CompactionStats::counts is an int[kNumOfReasons] holding one count per reason; it is serialized into the remote compaction result and read back by the primary.
Unit test
CompactionServiceTest.CompatCheckCountsWidthTolerance -- verifies that deserializing "counts" tolerates a serialized element count equal to, one greater than, or one less than the reader's expected width (extras dropped, missing zero-filled), and that the binary serializes exactly the expected number of elements.
Manual cross-version verification (ldb remote_compaction)
Test 1: expect-narrow tolerant-read primary reads wide counts a worker write (extra reason dropped)
=> PASS: the primary parsed and installed the remote result.
Test 2: expect-wide tolerant-read primary reads narrow counts a worker writes (missing reason zero-filled)
=> PASS: the primary parsed and installed the remote result.
Test 3: expect-wide tolerant-read primary reads wide counts a worker writes
=> PASS: the primary parsed and installed the remote result.
Differential Revision: D111068809