Skip to content

fix: bound VortexWriter's global-dict retained memory to avoid OOM on huge files#278

Merged
dfa1 merged 1 commit into
mainfrom
fix/parquet-importer-oom
Jul 18, 2026
Merged

fix: bound VortexWriter's global-dict retained memory to avoid OOM on huge files#278
dfa1 merged 1 commit into
mainfrom
fix/parquet-importer-oom

Conversation

@dfa1

@dfa1 dfa1 commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • VortexWriter buffers a global-dict candidate column's raw chunk data in memory from its first chunk until close() — a shared dictionary can only be built once every chunk has been seen.
  • On a huge, wide file (found via a real-world Raincloud corpus import: 18.5M rows, 38 string columns), any column that looks low-cardinality early but never gets demoted keeps its entire column pinned in the heap; with dozens of such columns the total reaches several GB and the import throws OutOfMemoryError — independent of how much heap is available (memory scales with file size × column count, not with a bounded chunk size).
  • Fix: an aggregate retained-bytes budget (256 MB) across all buffering dict-candidate columns. Crossing the budget demotes the largest-retained columns (flush buffered chunks as ordinary per-chunk segments, drop from future dict candidacy) until back under budget.

Test plan

  • New regression test GlobalDictUtf8Test#retainedBytesBudgetExceeded_utf8_demotesToPerChunkChunkedLayout reproduces the bug shape at small scale via a test-only budget seam, asserts the demoted column lands as a plain Chunked layout (not Dict) and that data round-trips correctly across the demotion.
  • ./mvnw test -pl writer -am — 1705 tests, 0 failures

🤖 Generated with Claude Code

… huge files

VortexWriter buffers a global-dict candidate column's raw chunk data in
memory from its first chunk until close() -- a shared dictionary can only
be built once every chunk has been seen. On a huge, wide file (e.g. an
18.5M-row / 38-string-column real-world Parquet import) any column that
looks low-cardinality on an early chunk but never gets demoted keeps its
entire column pinned in the heap; with dozens of such columns the total
reaches several GB and the import throws OutOfMemoryError, independent of
how much heap is available (memory scales with file size x column count,
not with a bounded chunk size).

Add an aggregate retained-bytes budget (256 MB, GLOBAL_DICT_MAX_RETAINED_BYTES)
across all buffering dict-candidate columns. Each chunk appended to a
candidate updates a per-column and running-total estimate (estimateRetainedBytes);
crossing the budget demotes the largest-retained columns -- flushing their
already-buffered chunks as ordinary per-chunk segments and dropping them from
future global-dict candidacy -- until back under budget. This bounds writer
memory by the budget rather than by total file size, while still giving most
columns their shared dictionary in the common case.

Regression test (GlobalDictUtf8Test#retainedBytesBudgetExceeded_utf8_demotesToPerChunkChunkedLayout)
reproduces the bug shape at small scale via a test-only budget seam
(setDictRetainedBudgetForTest): a column with genuinely low, constant
cardinality (so the existing cardinality-ratio fallback never fires) whose
raw bytes alone cross a lowered budget, forcing mid-file demotion. Asserts
the demoted column lands as a plain per-chunk Chunked layout (not Dict) and
that every value still round-trips correctly across the demotion boundary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfa1
dfa1 merged commit fb4b6be into main Jul 18, 2026
6 checks passed
@dfa1
dfa1 deleted the fix/parquet-importer-oom branch July 20, 2026 20:59
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.

1 participant