DO NOT MERGE (managed store sizing): re-derive work_mem after a resize and keep one v8 block (#4207) - #4225
Merged
Conversation
…ace (#4207) The v8 block (#2845) re-derives effective_cache_size, maintenance_work_mem and the worker settings after a resize, but not work_mem, so a resized store keeps the old hardware's work_mem indefinitely. Field stores stayed at the v3 block's 31 MB after a resize where DeriveMemorySettings now gives 62-64 MB, and one store spilled 33 TB to temp files since creation. The v8 block was also appended fresh on every fingerprint change instead of replacing the block it superseded, and because the fingerprint includes the worker count, which moves with the hypertable count, each field store had grown three copies. - BuildHardwareSizingConfAppend now emits work_mem from the same DeriveMemorySettings call as the other three settings. - ReplaceOrAppendHardwareSizingBlock rewrites the first existing v8 block in place and drops every other copy, so a fingerprint change costs one rewritten block, never a growing file. No existing block still falls back to a plain append. - EnsureConfAppended's v8 step re-reads the conf immediately before rewriting, so it cannot clobber a v1-v7 heal that just landed earlier in the same call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 05:23
This was referenced Sep 25, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 25, 2026
ShouldAppendHardwareSizing only fired on a fingerprint mismatch, so a store whose hardware has not changed since its last v8 write never retriggers the heal - even when the conf still carries duplicate v8 blocks from the append-not-replace bug, or when the surviving block's content predates a formula change (work_mem rejoining the block in #4207 itself). Add two more conditions, gated the same way on an authoritative RAM reading: more than one v8 block present, or the newest block's rendered text (CRLF-normalised) differing from what this build would write for the same inputs. Log which of the three reasons fired. Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4207.
Why
The v8 hardware-sizing block (#2845) re-derives sizing after a host resize. It re-states
effective_cache_size,maintenance_work_mem,timescaledb.max_background_workersandmax_worker_processes. It never re-statedwork_mem. So after a RAM resize,work_memstayed at whatever the v3 block computed for the old hardware, forever. On the field stores that is 31 MB, computed at 16 GiB.DeriveMemorySettingsat the reported 33,788,809,216 bytes (nominally "31.5 GiB", actually 31.47 GiB) now gives 62 MB. One store spilled 33 TB to temp files since creation. Another spilled 7 TB.work_memwas excluded from v8 on purpose in #2845. That exclusion rested on a measurement that does not apply here. The comparison found a regression at 512 MB, which is 8x this formula's own 64 MB ceiling. Nothing in this codebase writes that value. Nobody had measured the value the formula actually derives. #4207 measures the cost of leaving it out instead: 33 TB and 7 TB ofpg_stat_database.temp_bytes, on hosts sized for the RAM they now have.Second, smaller defect: the v8 block was appended fresh every time its fingerprint changed. The fingerprint includes the worker count, and the worker count moves with the hypertable count. Every field store now carries three v8 blocks (worker counts 72, 73, 74), and
postgresql.confgrows by one block on every schema change from here.What changes
Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs:BuildHardwareSizingConfAppendnow emitswork_mem, from the sameDeriveMemorySettingscall that already derives the other three settings.shared_buffersstays excluded. That exclusion is structural, not measurement-based. See the doc comment.ReplaceOrAppendHardwareSizingBlockrewrites the v8 block in place instead of appending. It finds every existing v8 block (FindHardwareSizingBlockSpans), rewrites the first one with the fresh content, and drops every later copy outright. No existing block still falls back to a plain append, unchanged from today. A block has no end marker of its own today.FindHardwareSizingBlockEnddefines the end marker from how every block in this file is written. EachBuild*ConfAppendopens with its own leading blank line and has no blank line inside itself. So the first blank line after the marker is always the start of whatever comes next. The doc comment states that rule and its one known edge case: an operator line spliced in with no blank line.EnsureConfAppended's v8 step re-readspostgresql.conffrom disk immediately before rewriting, rather than reusing the snapshot taken at the top of the method. v1-v7 above it can append their own healing blocks straight to disk in the same call. Reusing the stale snapshot loses them silently.ConfMarkerV8/BuildHardwareSizingConfAppendto record the reversal onwork_mem. Updated the log line to report the derivedwork_memvalue and how many stale copies, if any, were collapsed.Manual overrides keep winning, unchanged.
postgresql.auto.conf(ALTER SYSTEM) is read afterpostgresql.confin its entirety, and always wins regardless of anything here. A hand edit insidepostgresql.confwins by last-occurrence.Rewriting at the position of the first v8 block preserves that. Collapsing can only move the v8 lines earlier in the file, never later. Anything that used to lose to the last block was already losing. Anything that used to beat it still does.
Test plan
Darling.Testsbuilds with 0 warnings.DarlingManagedPostgresTests.cs, proven red against the pre-fix source once. Temporarily reverted only the source file and rebuilt against the new tests:HardwareSizingConfAppend_EmitsAllFourMemorySettings(pin: all fourMemorySettingsvalues present)HardwareSizingConfAppend_MeasuredFieldRam_EmitsWorkMem62Mb(the exact field byte count. The issue's rough "63 MB" is 31.5 GiB's raw RAM/512 without theQuantizeRamstep. The reported bytes quantize down to 31 GB and derive to 62 MB. This is noted in both the test and the source doc comment.)HardwareSizingConfAppend_EmitsHostDerivedSettings(extended with the work_mem assertion)ReplaceOrAppendHardwareSizingBlock_TwoSuccessiveFingerprintChanges_LeaveExactlyOneBlock(pin: two successive changes leave exactly one block)ReplaceOrAppendHardwareSizingBlock_ThreeExistingBlocks_CollapseToOneAtTheFirstPosition(pin: three existing blocks collapse to one, at the first block's position)ReplaceOrAppendHardwareSizingBlock_LinesOutsideBlocks_AreByteIdenticalAfterRewrite(pin: bytes outside any v8 span, including an operator line sitting between two blocks, survive untouched)ReplaceOrAppendHardwareSizingBlock_NoExistingBlock_AppendsOne(fallback path unchanged)EnsureConfAppended_ThreeExistingV8Blocks_CollapseToOneOnTheNextHeal(the field shape end to end, through the real heal path, not just the pure function)DarlingManagedPostgresTests,StartupCommandTimeoutTests,DocCommentHygiene: 254 total, 0 failed.Darling.Testssuite, once: 13767 total, 0 errors, 0 failed. 690 skipped (gated onDARLING_TEST_PGRUNTIME/_OLD, not set in this run). 1 not run (an unrelated gated theory inDarlingStoreUpgradeTests). The known flake (ServerListAndSummaryPlanShapeTests.TheShippedReads_TouchFarFewerChunks...) did not fail.DARLING_TEST_PGRUNTIME-gated E2E tests were not run:Bootstrap_EndToEnd_..., the maintenance_work_mem formula lands too low for compression throughput: +70% measured from raising it, plateaus by 1.5 GB #1777 and job_history is silently empty on clusters predating ~2026-08-17, because its GUC lives in the one postgresql.conf block EnsureConfAppended cannot heal #3175 conf-propagation tests, the PostgreSQL 17 cap tests. No rig was started for this lane. They exercise the realpg_ctlstart path rather than the v8 block itself. The new pins cover the v8 behavior directly. One goes through the realEnsureConfAppendedpath against a fabricated three-block conf.This never touches a live store. The change reaches the field stores only through a build and an install. That is Erik's call, not this PR's.
CHANGELOG entry
SECTION: Fixed
ENTRY:
work_memafter a hardware resize, and stop piling up duplicate sizing blocks ([DO NOT MERGE (managed store sizing): re-derive work_mem after a resize and keep one v8 block (#4207) #4225]). After a RAM resize, a managed store'swork_memstayed at the value computed for the old hardware indefinitely. The block that re-derives sizing after a resize never re-statedwork_mem. It now does. The same block was also appended fresh every time its derivation inputs changed, instead of replacing the block it superseded. A store accumulated several copies over its lifetime. It now rewrites the existing block in place.REF:
[DO NOT MERGE (managed store sizing): re-derive work_mem after a resize and keep one v8 block (#4207) #4225]: DO NOT MERGE (managed store sizing): re-derive work_mem after a resize and keep one v8 block (#4207) #4225
Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ