Skip to content

fix(storage): a spilled value over ~4 MB reads back instead of OverflowBroken (moon#1201) - #1203

Merged
TinDang97 merged 1 commit into
mainfrom
fix/1201-overflow-broken
Sep 24, 2026
Merged

TinDang97 merged 1 commit into
mainfrom
fix/1201-overflow-broken

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

Fixes #1201.

Bug. A value too big for one 4 KB page is stored as a chain of overflow pages, each holding about 4 KB. build_overflow_chain writes chains of any length. read_overflow_chain refused any chain longer than a fixed 1000 pages (about 4.03 MB), so a larger spilled value was written intact but could never be read back. The key stayed indexed and every read failed with OverflowBroken, which clients see as IOERR.

In the v0.8.10 release soak this happened to consumer-group streams: the pending list keeps growing without XACK, so the streams passed 4 MB.

  • IOERR on 3966 XADD runs and 3966 XREADGROUP runs.
  • On disk the failing chains are intact, with 1003 to 1007 pages each.

Not a regression. The 1000-page limit dates from PR #43. v0.8.9 has the same bug, but there the failed read was a silent miss (XLEN 0, GET nil). #1004 turned that miss into an IOERR, which is why it only now shows up as an error.

Fix. The walk is bounded by the file's own page count. A chain that doesn't loop visits each page at most once, so it can't be longer than the file; a looping chain always exceeds that count. No on-disk format change: files written by any earlier version read back.

Tests (red on unfixed code, green with the fix, red again with only the bound reverted):

  • kv_page::test_overflow_chain_longer_than_1000_pages_reads_back_1201
  • cold_read::test_cold_read_overflow_chain_longer_than_1000_pages_1201: spills 4.5 MB and reads it back.
  • kv_page::test_overflow_chain_cycle_is_rejected_1201: the cycle guard still works.

Repro (1 shard, --maxmemory 48mb, AOF on, a 20k-entry stream with a 20k-entry pending list, plus a 4.5 MB string):

Build Stream 4.5 MB string
main IOERR on XLEN, XADD and XREADGROUP IOERR
fixed XLEN 20000, stream intact GET returns the exact bytes
v0.8.9 XLEN 0, XADD silently starts a new stream nil

Known, separate: reading a multi-MB cold value back into memory can push the server over maxmemory, which is the #1156 class.

…owBroken

The cold-tier overflow-chain reader (`read_overflow_chain`) bounded its
walk with a fixed MAX_OVERFLOW_PAGES = 1000 cycle guard (~4.03 MB of
payload at 4032 B/page). The spill writer (`build_overflow_chain`) has no
size cap, so every value above that size was written intact and indexed,
then refused on every read as `OverflowBroken`: IOERR to XADD, XREADGROUP,
GET, and the index entry kept for a retry that could never succeed.

The cycle guard is now the file's own page count. An acyclic chain inside
the file visits each page at most once, so it can never be longer than the
file; a cycle always exceeds it. No on-disk format change.

Evidence:
- Soak (RC3) failing files, chains walked on disk: heap-4889612 1007
  pages, heap-4897151 1004, heap-5026381 1003. All intact, all just over
  1000. Every failing file is 4.1-4.15 MB. The streams are consumer-group
  streams read without XACK, so the PEL grows past MAXLEN trimming.
- e2e repro (1 shard, 48mb maxmemory, a 20k-entry stream with a 20k PEL
  forced to spill): main = IOERR on XLEN/XADD/XREADGROUP, 4 OverflowBroken
  lines. Fixed = XLEN 20000, XADD/XREADGROUP OK, 0 unreadable. v0.8.9 =
  XLEN 0, XADD makes a new stream, XREADGROUP NOGROUP. v0.8.9 answered the
  same failed read as a silent miss (#1004 later made it IOERR), so the cap
  is pre-existing (ff51135, PR #43) and not a regression in this window.
- Red/green/mutation: the new kv_page and cold_read tests fail on main with
  OverflowBroken, pass with the fix, and fail again when only the bound goes
  back to 1000. A cycle-rejection test pins the guard.

Fixes #1201
author: Tin Dang
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5024eeca-ff88-4f3d-b53f-0bde478697ad

📥 Commits

Reviewing files that changed from the base of the PR and between 843611f and 67d8239.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/persistence/kv_page.rs
  • src/storage/tiered/cold_read.rs
 ___________________________________________________________________________________________
< Optimism is an occupational hazard of programming; feedback is the treatment. - Kent Beck >
 -------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TinDang97
TinDang97 marked this pull request as ready for review September 24, 2026 07:32
@TinDang97
TinDang97 merged commit a4b94fd into main Sep 24, 2026
23 checks passed
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

TinDang97 added a commit that referenced this pull request Sep 24, 2026
Merges main at 32a7a02 (RC4) into the release branch. That brings two
cold-tier fixes the release soak exposed:
- #1201 (PR #1203): a spilled value over ~4 MB read back as OverflowBroken
  / IOERR.
- #1202 (PR #1204): a MOON.SPILLED cut record could be dropped under AOF
  backpressure.

Release notes:
- Count 147 -> 149 PRs; date 2026-09-24.
- New COLD TIER evidence paragraph.
- #1215 disclosed as a known, pre-existing limitation deferred to
  v0.8.11 under the slip rule (format-level fix): a deleted cold-tier key
  resurrects after an AOF rewrite + restart (v0.8.9 has it too).
  Workaround: --disk-offload disable.

The tree outside CHANGELOG.md, README.md, RELEASES.md,
docs/roadmap/ROADMAP.md and the version bump is identical to RC4.

Refs: #1154
author: Tin Dang
TinDang97 pushed a commit that referenced this pull request Sep 24, 2026
…w fix wave

Conflicts: writer_task.rs keeps main's moon#1158 post-drain release of
AOF_REWRITE_IN_PROGRESS with WS6's aof_buf_writer (8 MiB, moon#1187);
CHANGELOG.md keeps both sides' Fixed entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JyGvRHGpDdG2GVKwwvsBj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cold-tier read of a spilled stream fails with OverflowBroken: key indexed, data unreadable (IOERR to clients) under sustained mixed load

1 participant