Skip to content

fix(sheet): isolate column width cache per sheet holder - #1306

Open
yebuguntang wants to merge 3 commits into
apache:mainfrom
yebuguntang:fix/column-width-cache-leak
Open

yebuguntang wants to merge 3 commits into
apache:mainfrom
yebuguntang:fix/column-width-cache-leak

Conversation

@yebuguntang

Copy link
Copy Markdown
Contributor

Summary

fixes #1305

LongestMatchColumnWidthStyleStrategy cached the max column width in a field keyed only by sheetNo. When the same strategy instance was reused across two ExcelWriter instances (or two workbooks), both used sheetNo = 0, so the second workbook's widths could never beat the value cached from the first one. As a result Sheet.setColumnWidth was never called and the second workbook fell back to POI's default column width.

Fix

Move the per-column max-width cache from the strategy instance onto WriteSheetHolder (columnWidthCache). The strategy is now stateless with respect to width tracking, so:

  • reusing one strategy across writers no longer leaks widths between workbooks/sheets;
  • there is no shared mutable state, so the concurrency concerns raised in the issue (synchronized map vs. single-threaded access) become moot.

This follows the same per-holder isolation pattern used for converter state (e.g. #740), keeping related state co-located with the holder that owns it.

Test

Added LongestMatchColumnWidthStyleStrategyTest:

  • separateStrategyInstancesComputeEachWorkbookWidth — control case (separate instances).
  • sharedStrategyInstanceDoesNotLeakWidthAcrossWorkbooks — regression case that fails against the old cache-on-strategy implementation and passes after the fix.

Notes

The two design points discussed in #1305 (strong vs. weak references, thread safety) are no longer relevant because the cache is now per-holder and not shared.

…et holder

Move the per-column maximum width cache from the strategy instance onto
WriteSheetHolder so that a single LongestMatchColumnWidthStyleStrategy
instance can be reused across writers without leaking widths from one
workbook or sheet into another.

The strategy is now stateless with respect to width tracking, which also
removes the shared mutable state discussed in apache#1305 (thread-safety and
reference strength are no longer concerns). This follows the same
per-holder isolation pattern used for converter state (e.g. apache#740).

fixes apache#1305
@yebuguntang yebuguntang changed the title fix: isolate LongestMatchColumnWidthStyleStrategy width cache per sheet holder fix(sheet): isolate column width cache per sheet holder Nov 2, 2026

@Mikkey-f Mikkey-f left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yebuguntang — I reproduced the leak and verified the fix locally (Windows 11, JDK 17).

Root cause: The strategy cache is keyed by sheetNo only, so two ExcelWriters both use key 0. Since a width is only applied when it beats the cached maximum, the second workbook's shorter value never reaches setColumnWidth and stays at POI's default.

Verification

  • Red: with only the two product files reverted to current main (your tests kept), sharedStrategyInstanceDoesNotLeakWidthAcrossWorkbooks fails with expected: <256> but was: <2048>; the separate-instance control test still passes.
  • Green: both tests pass (2/2).
  • Full suite (mvn clean package -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet): 60 + 1,791 tests, 0 failures; spotless:check clean.

Code review: Moving the cache to WriteSheetHolder gives it the right owner — one map per sheet, lifetime tied to the workbook, and it still accumulates the running maximum across doWrite calls on the same sheet. Excluding it from @EqualsAndHashCode matches the existing hasBeenInitializedTable pattern. SimpleColumnWidthStyleStrategy and AbstractHeadColumnWidthStyleStrategy don't hold per-instance state, so there's no sibling copy of this bug.

Two nits (non-blocking)

  1. WriteSheetHolder.columnWidthCache has no javadoc while the surrounding fields do — a one-liner would keep the holder self-documenting.
  2. The new test class javadoc still says "per-instance width cache"; after the fix it's per sheet holder.

LGTM — correct state ownership and a deterministic regression test.

@yebuguntang

Copy link
Copy Markdown
Contributor Author

Thank you @Mikkey-f for the careful review and for taking the time to reproduce the leak and verify the fix — it really helped.

I also overlooked the project's documentation conventions: the new field should have had a javadoc, and the test comment should have been updated along with the code. Lesson learned — I'll pay closer attention to javadoc and to keeping comments in sync in future contributions.

Thanks again for the guidance, much appreciated!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues block the change to isolate column-width tracking per sheet holder.

No open issues 🎉

What changed in this PR

Fixes #1305 by moving column-width tracking from the reusable strategy to each sheet holder, preventing cached widths from leaking across workbooks.

Changes:

  • Adds a per-holder column-width cache, excluded from equality and hash-code calculations.
  • Updates the strategy to lazily initialize and use the holder’s cache.
  • Adds real-file tests covering separate strategy instances and one instance shared across writers.
File Description
fesod-sheet/​src/​test/​java/​org/​apache/​fesod/​sheet/​write/​style/​column/​LongestMatchColumnWidthStyleStrategyTest.java Adds control and regression tests that verify saved workbook widths.
fesod-sheet/​src/​main/​java/​org/​apache/​fesod/​sheet/​write/​style/​column/​LongestMatchColumnWidthStyleStrategy.java Replaces strategy-owned width tracking with the sheet holder’s cache.
fesod-sheet/​src/​main/​java/​org/​apache/​fesod/​sheet/​write/​metadata/​holder/​WriteSheetHolder.java Adds the isolated width cache and excludes it from equality and hashing.

Review effort: Balanced

This branch has not been deployed

No deployments
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.

[Bug] LongestMatchColumnWidthStyleStrategy leaks widths across workbooks

4 participants