Repository navigation
Re-derive managed-store Postgres sizing on a hardware change (Fixes #2845) - #2850
Conversation
…2845) Every conf heal block v1-v7 is keyed on "is this marker absent?", which is answered once in a cluster's life. Nothing re-derived when the machine was replaced underneath it, so all three monitoring boxes kept effective_cache_size at 11.86 GB (75% of 16 GB) after being resized to 31.5 GB. The v8 block keys on a fingerprint of the derivation inputs instead of a version, so the two triggers compose: a version marker heals a formula change, this heals a hardware change. It compares the LAST fingerprint in the conf rather than any, because postgresql.conf resolves duplicates by last-occurrence-wins -- a host resized 16 -> 32 -> 16 GB carries both, and a Contains test would find the stale first one and skip, latching the 32 GB block in force on a box that no longer has 32 GB. DeriveWorkerSettings is extracted so v2 and v8 share one worker formula and cannot drift. Re-stating those counts is also what makes v2's "never goes stale as collectors are added" claim true; it was true of the formula and false of its marker-keyed application. shared_buffers and work_mem are excluded structurally, not by trusting their caps: shared_buffers because the 1 GB cap is the live Windows error-487 mitigation (#1559) and raising it is a reviewed formula decision rather than something a resize propagates, work_mem because the formula would double it to 63 MB while every measurement above 31 MB on the store's heaviest read is worse, and because a per-sort per-connection ceiling follows from the query mix, not from the machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
…ardware-change # Conflicts: # CHANGELOG.md
| var hypertableCount = TimescaleSupport.HypertableCount; | ||
| var v8RamBytes = GetTotalPhysicalMemoryBytes(); | ||
| var v8Fingerprint = BuildHardwareFingerprint(v8RamBytes, hypertableCount); | ||
| if (!ConfHasCurrentHardwareFingerprint(conf, v8Fingerprint)) |
There was a problem hiding this comment.
GetTotalPhysicalMemoryBytes() (used for v8RamBytes just above) has three fallback tiers: real RAM, then gcTotal (the GC's live view of available memory) if positive, then the fixed MemoryFallbackRamBytes (4 GB) only as a last resort. BuildHardwareFingerprint's own normalization (totalPhysicalMemoryBytes > 0 ? totalPhysicalMemoryBytes : MemoryFallbackRamBytes) only catches the "≤ 0" case — it never sees a raw 0 here, since GetTotalPhysicalMemoryBytes() already guarantees a positive return before the value reaches the fingerprint.
That leaves the middle tier unguarded. gcTotal is a live snapshot, not a stable hardware property — it can differ from one GlobalMemoryStatusEx failure to the next, and from the true physical RAM. If that Win32 call fails intermittently:
- Each failure can fingerprint under a different
gcTotalvalue, soConfHasCurrentHardwareFingerprintnever matches, and v8 appends a new block every time this happens — the "append a block on every alternating start" append-loop theHardwareFingerprint_NonPositiveRam_MatchesTheFallbackItDerivedUndertest/docstring explicitly says this design prevents, but only for the literal-zero case. - Worse, that appended block's
effective_cache_size/maintenance_work_memget derived from the GC fallback figure (typically well under true RAM) and take effect immediately on that very start (per the "Reload semantics" note above), so a transient memory-query blip can silently degrade production Postgres sizing until the next start happens to read RAM successfully again.
Unlike v3/v5/v7 (marker-gated, so a bad fallback reading only ever "sticks" once), v8 re-evaluates on every start for the life of the cluster, so this "rare" Win32 failure now gets unbounded chances to trigger it. Consider routing GetTotalPhysicalMemoryBytes()'s fallback tiers through the same fixed MemoryFallbackRamBytes sentinel (or otherwise making the fallback fingerprint-stable) so a transient query failure can't produce a novel fingerprint.
There was a problem hiding this comment.
Good catch, and it's a real hole in exactly the place the docstring claimed to cover — fixed in d7d0c52.
You're right that the > 0 normalization never sees the middle tier: GetTotalPhysicalMemoryBytes already guarantees a positive return, so the guard was dead for the case that matters. And the value it returns there is GC.GetGCMemoryInfo().TotalAvailableMemoryBytes, which is a live snapshot rather than a hardware property — so it can differ between blips, which is precisely why a value-based guard cannot fix this. Routing the fallback through the fixed sentinel would have worked, but I went further, because I think the stronger statement is available:
A non-authoritative reading is not evidence that the hardware is unchanged — it is the absence of evidence either way. So v8 now does nothing without a trustworthy reading, rather than re-deriving from a stable-but-wrong sentinel. That kills both halves of what you identified: no novel fingerprint, and no sizing derived from a figure below true RAM. It also means the failure mode is "the last known-good block stays in force", which is the correct behaviour under uncertainty.
Concretely:
TryGetAuthoritativePhysicalMemoryBytessplit out — true only on theGlobalMemoryStatusExsuccess path.GetTotalPhysicalMemoryByteskeeps its existing best-effort tiers unchanged, so v3/v5/v7 behave exactly as before (for a first-ever provision, some number beats none — your point that a marker-gated block can only stick once is what makes that trade fine there and not here).ShouldAppendHardwareSizing(conf, ramReadingIsAuthoritative, expectedFingerprint)exposes the whole decision as a pure function, so the property is pinnable without a data directory.- The call site logs a Warning and skips when the reading isn't authoritative.
Proven red by mutating the gate back out (=> !ConfHasCurrentHardwareFingerprint(...), i.e. the pre-review behaviour): authoritative-gate: an untrusted reading appends NOTHING for any value (append-loop) fails and everything else stays green — 16 passed, 1 FAILED, restored to 17/0. The pin iterates several plausible GC-fallback values rather than one, since the defect is specifically that the fallback is not a single value.
Your framing of "v8 re-evaluates on every start, so this rare failure now gets unbounded chances" is the part I had not reasoned through — the marker-gated blocks made a best-effort guess safe in a way it stops being once the check is continuous.
|
Reviewed the v8 hardware-fingerprint conf-healing logic in Scope check: this is Darling-only (bundled/managed PostgreSQL instance sizing) with no Lite counterpart — Lite has no bundled Postgres to manage, so no parity gap here. What looks solid:
One correctness gap flagged inline: the v8 fingerprint is built from whatever |
GetTotalPhysicalMemoryBytes falls back to GC.GetGCMemoryInfo()'s live TotalAvailableMemoryBytes before it reaches the fixed 4 GB sentinel, so normalising only the "<= 0" case never sees the middle tier. That value varies between calls and sits below true physical RAM, so an intermittently failing GlobalMemoryStatusEx would mint a novel fingerprint on each blip and append a block every time -- and unlike the marker-gated v1-v7 this check runs on every start, so the rare failure gets unbounded chances. The appended block would also derive effective_cache_size from the low guess, with immediate effect. Guarding on the value cannot fix a varying fallback. v8 now takes the authoritative reading or does nothing: absence of a reading is not evidence the hardware is unchanged, and leaving the last known-good block in force beats re-deriving production sizing from a number we could not read. ShouldAppendHardwareSizing exposes the whole decision as a pure function so the property is pinned without a data directory. Proven red by dropping the gate: "an untrusted reading appends NOTHING for any value" fails, everything else stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
| /// <summary> | ||
| /// True when the MOST RECENT fingerprint in the conf matches the current hardware — the test that | ||
| /// decides whether <see cref="BuildHardwareSizingConfAppend"/> needs to run (#2845). | ||
| /// | ||
| /// <para>Deliberately NOT <c>conf.Contains(fingerprint)</c>. postgresql.conf takes the LAST occurrence | ||
| /// of a setting, so what is in force is whatever the newest block said. A host resized 16 -> 32 -> 16 GB | ||
| /// would, under a Contains test, find its original 16 GB fingerprint still present and skip — leaving | ||
| /// the 32 GB block as the last occurrence and therefore still in force on a box that no longer has | ||
| /// 32 GB. Comparing only the last fingerprint makes the check ask the question that matches the file's | ||
| /// own semantics, and is what lets this converge instead of latching.</para> | ||
| /// </summary> | ||
| /// <summary> | ||
| /// Whether the v8 block should be appended on this start (#2845) — the whole decision as one pure | ||
| /// function so the property can be pinned without a data directory. | ||
| /// | ||
| /// <para>Two conditions, and the FIRST is the one that is easy to get wrong: the RAM reading must be | ||
| /// authoritative. A non-authoritative reading is not evidence that the hardware is unchanged, it is the | ||
| /// absence of evidence either way — and re-deriving production sizing from a number we could not read is | ||
| /// worse than leaving the last good block in force. It also stops a flapping Win32 call from minting a | ||
| /// novel fingerprint on every blip and appending a block each time, which a value-only guard cannot do | ||
| /// because the fallback it would guard against is a live, varying quantity rather than a fixed | ||
| /// sentinel.</para> | ||
| /// </summary> | ||
| public static bool ShouldAppendHardwareSizing(string conf, bool ramReadingIsAuthoritative, string expectedFingerprint) | ||
| => ramReadingIsAuthoritative && !ConfHasCurrentHardwareFingerprint(conf, expectedFingerprint); | ||
|
|
||
| public static bool ConfHasCurrentHardwareFingerprint(string conf, string expectedFingerprint) |
There was a problem hiding this comment.
Doc-comment mix-up: the /// <summary> block starting at line 636 ("True when the MOST RECENT fingerprint in the conf matches the current hardware...", describing the LastIndexOf/last-occurrence-wins rationale) reads like it documents ConfHasCurrentHardwareFingerprint, but there's no code line between it and the next /// <summary> block (line 647, "Whether the v8 block should be appended..."), so both attach to ShouldAppendHardwareSizing as one XML doc with two <summary> elements. ConfHasCurrentHardwareFingerprint — the method with the subtler last-occurrence-wins logic that this PR is centrally about — ends up with no doc comment at all, and ShouldAppendHardwareSizing's doc has a stray/duplicate <summary>.
Given CONTRIBUTING.md's C# style calls for XML docs on public APIs, worth moving the first block down onto ConfHasCurrentHardwareFingerprint.
There was a problem hiding this comment.
Right, and thank you — fixed in e3791ab, and DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks caught it independently in the same CI round, pointing at the same two <summary> openings.
Cause was exactly as you describe: I anchored the insertion on the signature line of ConfHasCurrentHardwareFingerprint, so the new block landed between that method's own summary and its member. Its test message warns specifically not to resolve this by deleting the first summary because it is usually a displaced block whose real member has been left undocumented — which is what happened here, and to the method this PR is centrally about. Moved the new member below the complete host body instead, so each doc sits above what it describes.
One thing that came out of re-reading it: BuildHardwareFingerprint's doc had also become untrue. It claimed the non-positive normalisation is what stops a failed RAM read appending on every alternating start. It is not — and relying on it was the bug you found in the other thread, since the GC fallback arrives as a varying positive. That paragraph now says where the stability actually comes from and warns against relying on the normalisation for it.
|
Reviewed the diff. This PR is Darling-only ( Correctness walkthrough:
Left one inline comment: a doc-comment mix-up where two No security, SQL-injection, or missing-index-DMV concerns — this PR doesn't touch T-SQL at all. |
DocCommentHygieneTests caught it: the review fix inserted ShouldAppendHardwareSizing anchored on the SIGNATURE of ConfHasCurrentHardwareFingerprint, so the new doc landed between that method's own summary and its member -- two stacked summaries, and the host member left undocumented. Exactly the displaced-block case the test's message warns not to resolve by deleting the first summary. Moved the new member below the complete host body so each doc sits above the member it describes. Also corrects a paragraph the review fix had made untrue: BuildHardwareFingerprint's doc claimed its non-positive normalisation is what keeps a failed RAM read from appending on every alternating start. It is not, and relying on it was the bug -- GetTotalPhysicalMemoryBytes falls back to a live GC figure before the fixed sentinel, so a failed read arrives as a varying positive this guard cannot see. Stability comes from ShouldAppendHardwareSizing refusing to act without an authoritative reading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
|
Reviewed the diff (CHANGELOG.md, Correctness
Lite/Darling parity
Security/Performance
Style
Test coverage (constraint pins on never-emit |
conf is captured once at the top of EnsureConfAppended, before v1-v7 may append. That is safe only because none of them writes a line carrying ConfHardwareFingerprintPrefix. A future version block that did would be invisible to this check, which would then quietly stop detecting hardware changes -- the exact failure class this PR exists to fix, one level up. Non-blocking today; recorded so it cannot become blocking silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
|
Thanks — acting on the non-blocking flag rather than just noting it, in You're right that the read-once pattern is benign today only because no v1–v7 block emits a line carrying Recorded it at the capture site, naming the invariant, the consequence if it breaks, and the remedy (re-read the file rather than adding a block above). No behaviour change: re-reading unconditionally would add a file read per start to defend against a hypothetical, and the comment puts the constraint where someone adding a v9 will actually see it. On the parity point — agreed, and worth being explicit since |
| { | ||
| var ram = totalPhysicalMemoryBytes > 0 ? totalPhysicalMemoryBytes : MemoryFallbackRamBytes; | ||
| return FormattableString.Invariant( | ||
| $"{ConfHardwareFingerprintPrefix}ram_mb={ram / (1024L * 1024L)} hypertables={hypertableCount}"); |
There was a problem hiding this comment.
The fingerprint is an exact-MB comparison, and v8 (unlike v1-v7) runs on every start, not just once. GlobalMemoryStatusEx's ullTotalPhys isn't guaranteed bit-identical across reboots on every host class this targets — Hyper-V Dynamic Memory / balloon-driver VMs in particular can report a different "current" physical memory total from one boot to the next even with no operator-intended resize, and a handful of these monitoring boxes are cloud VMs per the PR description. If that happens, ShouldAppendHardwareSizing sees a legitimate, "authoritative" reading that simply differs by a few MB from the last one, and will treat it as a hardware change on every single restart: appending a fresh v8 block, re-logging "Appended v8 hardware sizing..." each time, and growing postgresql.conf by 7 lines per restart forever. Functionally harmless (last-occurrence-wins), but it defeats the "converges immediately" invariant the doc comment relies on, and produces unbounded file growth / log noise on any host whose reported RAM isn't perfectly stable across boots.
Worth considering quantizing RAM before fingerprinting (e.g., round to the nearest GB, or the same granularity the derived settings are keyed on) so a few-MB reboot-to-reboot jitter doesn't read as a hardware change, the same way the rest of this PR is careful to distinguish "the machine changed" from "the reading is noisy."
There was a problem hiding this comment.
Real defect, and fixed in a46988e — thank you.
You've identified the thing this PR should have caught itself: the whole design is about distinguishing "the machine changed" from "the reading is unreliable", and I handled the second only in its loud form (a failed read) while leaving the quiet form (a successful but jittery read) to be treated as a resize. The evidence was already in front of me — the fleet reports 31.5 GB on a nominally 32 GB host — so the reported total is demonstrably not the nominal figure, and I had no basis for assuming it is stable to the megabyte across reboots.
Quantized to the nearest GB, and applied to both the fingerprint and the derivation rather than the fingerprint alone. That buys a stronger invariant than just suppressing the churn: a v8 block is now exactly reproducible from the fingerprint written above it, so the two can never disagree about the machine they describe. A GB also has the property you're implicitly relying on — far above any plausible jitter, far below the smallest real resize this class sees (4 -> 8 GB), so it cannot mask a genuine change. There's a pin asserting exactly that, since a quantization that hid real resizes would be a worse bug than the one it fixes.
Scoped to the v8 path only: v3/v5/v7 keep deriving from the raw reading, so nothing shifts on a store that never reaches v8.
Proven red by dropping the quantization — jitter: 31.5GB / 32GB / 32GB-7MB all read as the SAME machine and jitter: block is exactly reproducible from its fingerprint both fail, 18 passed / 2 FAILED, restored to 20/0.
Incidentally the rounding is arguably more correct on its own merits: ullTotalPhys is always slightly under the nominal size because of firmware reservation, so the nearest GB is closer to the quantity the sizing formulas conceptually mean.
| /// see. Stability comes from <see cref="ShouldAppendHardwareSizing"/> refusing to act at all without an | ||
| /// authoritative reading.</para> | ||
| /// </summary> | ||
| public static string BuildHardwareFingerprint(long totalPhysicalMemoryBytes, int hypertableCount) |
There was a problem hiding this comment.
Minor consistency nit: BuildHardwareFingerprint, ConfHasCurrentHardwareFingerprint (line 650) and ShouldAppendHardwareSizing (line 675) are all public, but they're decision/derivation helpers in the same spirit as DeriveMemorySettings and DeriveWorkerSettings, which this PR (correctly) keeps internal. Tests already reach internal members via InternalsVisibleTo, so nothing requires these to be public — making them internal would keep the class's public surface limited to the Build*ConfAppend entry points, matching the established pattern in this file.
There was a problem hiding this comment.
Agreed and done in a46988e. BuildHardwareFingerprint, ConfHasCurrentHardwareFingerprint, ShouldAppendHardwareSizing and BuildHardwareSizingConfAppend are all internal now, matching DeriveMemorySettings/DeriveWorkerSettings and keeping the public surface to the Build*ConfAppend entry points callers outside the class actually use.
public was unconsidered rather than deliberate on my part — I mirrored the older Build*ConfAppend members without asking whether these needed the same reach, and they don't: EnsureConfAppended is the only production caller and the tests reach internals via InternalsVisibleTo already.
One knock-on worth recording: my out-of-repo verification harness reflects into this class and was binding BindingFlags.Public | BindingFlags.Static, so it silently resolved nothing after the change and would have reported the API as absent rather than failing on behaviour. Updated to include NonPublic, and re-ran to confirm all 20 checks still pass against the real built assembly rather than trusting the compile.
|
Reviewed the v8 hardware re-derivation change (
One more thing that doesn't anchor to a diff line: Everything else checks out well: the shared |
…ardware-change # Conflicts: # CHANGELOG.md
|
Reviewed the diff (CHANGELOG, Scope check: this is Darling-only (the bundled managed PostgreSQL store's Correctness: traced through the v8 fingerprint mechanism carefully — the "compare the last occurrence, not Nothing jumped out as a functional bug. The property-based tests (fingerprint stability across a failed read, resize-back, non-authoritative appends-nothing, shared_buffers/work_mem never emitted, worker-formula parity with v2) cover the edge cases I'd otherwise have asked about. Minor, non-blocking: No security, injection, or secrets-handling concerns — this only formats fixed-shape strings into a local conf file already owned by the service account. |
Two review findings. RAM jitter. The fingerprint is an exact comparison and v8 runs on every start, so any wobble in the reported total reads as a hardware change: a fresh seven-line block appended per restart, forever, defeating the "converges immediately" invariant the design rests on. ullTotalPhys is not guaranteed bit-identical across reboots -- balloon/Dynamic-Memory guests especially -- and these are cloud VMs. The fleet's own readings are already non-round: 31.5 GB on a nominally 32 GB host. Quantizing to the nearest GB sits far above any plausible jitter and far below the smallest real resize this class sees (4 -> 8 GB), so it cannot mask a genuine change. Applied to BOTH the fingerprint and the derivation, so a block is exactly reproducible from the fingerprint above it, and to the v8 path only -- v3/v5/v7 keep deriving from the raw reading. Visibility. BuildHardwareFingerprint, ConfHasCurrentHardwareFingerprint, ShouldAppendHardwareSizing and BuildHardwareSizingConfAppend are derivation and decision helpers in the same spirit as DeriveMemorySettings and DeriveWorkerSettings, so they match those and go internal, keeping the public surface to the Build*ConfAppend entry points the callers actually use. Proven red by dropping the quantization: the jitter pin and the reproducible-from-fingerprint pin both fail, everything else stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
| else if (ShouldAppendHardwareSizing(conf, v8Authoritative, v8Fingerprint)) | ||
| { | ||
| File.AppendAllText(confPath, BuildHardwareSizingConfAppend(v8RamBytes, hypertableCount)); | ||
| var v8Settings = DeriveMemorySettings(v8RamBytes); |
There was a problem hiding this comment.
BuildHardwareSizingConfAppend(v8RamBytes, hypertableCount) on the line above internally quantizes RAM (DeriveMemorySettings(QuantizeRam(totalPhysicalMemoryBytes))) before writing effective_cache_size / maintenance_work_mem into the conf. This line then recomputes DeriveMemorySettings(v8RamBytes) with the raw, unquantized reading purely for the log message.
Near a GB boundary these disagree. E.g. a 31.5 GB reading quantizes to 32 GB for the actual write (effective_cache_size = 24576MB per the HardwareSizingConfAppend_EmitsHostDerivedSettings test), but this log line would report EffectiveCache = 24192MB (75% of the raw 31.5 GB) — a value that was never written to postgresql.conf. Same mismatch for maintenance_work_mem (1638MB written vs. ~1612MB logged).
Given how much this PR leans on "the block is exactly reproducible from its own fingerprint" as an invariant, the log message should reflect the same quantized value that was actually persisted:
var v8Settings = DeriveMemorySettings(QuantizeRam(v8RamBytes));(QuantizeRam is internal static in this class already, so it's reachable here.)
There was a problem hiding this comment.
Correct, and it's a defect I introduced two commits ago — fixed in the push above.
Your framing is the right one: the PR leans on "the block is exactly reproducible from its own fingerprint", and a log line reporting numbers that appear nowhere in the file breaks that in the place an operator would actually look. It's the same failure class the PR exists to fix — a record that disagrees with reality while looking authoritative — reintroduced by the fix for the previous round.
I took your suggested change one step further rather than applying it literally. DeriveMemorySettings(QuantizeRam(v8RamBytes)) fixes the two settings but leaves RamMb in the same message still logging the raw figure, so a 31.5 GB host would report "host RAM 32256 MB -> effective_cache_size 24576MB" — internally inconsistent in a different way. Quantizing once at the call site and passing that value down makes the logged values necessarily the written ones by construction, rather than by two call sites agreeing to apply the same transform:
var v8QuantizedRam = QuantizeRam(v8RamBytes);
File.AppendAllText(confPath, BuildHardwareSizingConfAppend(v8QuantizedRam, hypertableCount));
var v8Settings = DeriveMemorySettings(v8QuantizedRam);QuantizeRam is idempotent, so the callee still quantizes and still lands on the same answer — the internal call stays as the guarantee for any other caller.
Harness still 20/20 against the real built assembly.
|
Reviewed. This is a well-scoped, thoroughly self-reviewed change (the branch already carries four internal review/fixup commits addressing quantization jitter, non-authoritative RAM reads, and doc placement). One remaining issue found, left as an inline comment:
Other notes:
|
The quantization commit left the log line deriving from the raw reading while the block was written from the quantized one, so near a GB boundary they disagreed: a 31.5 GB host writes effective_cache_size 24576MB and logged 24192MB, a number appearing nowhere in postgresql.conf. Quantize once at the call site and pass it down, so the logged values are necessarily the written ones. QuantizeRam is idempotent, so the callee still quantizes and still agrees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
ReviewScope: Correctness — traced the full v8 path (
Nothing else in the diff calls Lite/Darling parity — no drift. Process note, not a defect: this PR's own commit history (6 commits) already walks through and fixes exactly the subtle failure modes a reviewer would look for — RAM-reading jitter across reboots (quantization), a non-authoritative/GC-fallback reading masquerading as a hardware change, a misplaced XML-doc block, and a log-line/written-value mismatch near a GB boundary. I re-derived each of those independently while reading and didn't find anything left over. No findings to post inline. Missing-index DMV recommendations are out of scope per project convention and don't apply here regardless. |
9616367 to
1672131
Compare
…ardware-change # Conflicts: # CHANGELOG.md
| /// </summary> | ||
| [Fact] | ||
| public void HardwareFingerprint_RamChange_TriggersRederivation() | ||
| { |
There was a problem hiding this comment.
Nit: this method's opening brace/body are indented one level deeper than every other test in the file (8 spaces instead of 4 for the brace, and the body doesn't gain the usual extra 4 spaces past it). Doesn't affect behavior, just inconsistent with the surrounding style.
There was a problem hiding this comment.
Fixed in 3180397 — whitespace only, one line. You were right that it was the brace: it sat at 8 spaces, so the body at 8 read as flush with it rather than nested. Correcting the brace to 4 makes the existing body indentation correct without touching it.
I checked the property rather than just this one site: every parameterless test method in the file now opens its brace at 4 spaces (33 of them), so this was the only instance.
ReviewScope: this PR only touches Lite/Darling parity: No parity issue — managed-Postgres bootstrap/conf-healing is a Darling-only concept (Lite runs on DuckDB and has no equivalent Correctness:
Security: No new I/O surface beyond the existing Performance: Negligible — one extra No blocking issues found. Left one inline nit (test method indentation, purely cosmetic). |
HardwareFingerprint_RamChange_TriggersRederivation opened its brace at 8 spaces where every other test in the file uses 4. Whitespace only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
|
Reviewed the diff (CHANGELOG + Darling's Correctness — traced the load-bearing invariants:
Parity — this is Darling-only (managed PostgreSQL instance sizing); Lite embeds DuckDB and has no Security — no new input handling, file paths are all internal ( Minor/non-blocking observation: Nothing else stood out. The mutation-tested pins described in the PR body (quantization jitter, resize-back-to-previous-size, non-authoritative reading, shared_buffers/work_mem exclusion) line up with what the implementation actually does. |
Fixes #2845.
The defect
Conf heal blocks v1-v7 each ask "is this marker absent?" — a question answered once in a cluster's life. So every one of them re-derives on a formula change and none on a hardware change. All three monitoring boxes were resized 16 GB -> 31.5 GB and kept
effective_cache_sizeat 11.86 GB, which is 75% of the RAM they no longer had.The fix
A v8 block keyed on a fingerprint of the derivation inputs (RAM + hypertable count) rather than a version. The two triggers are orthogonal and compose — a version marker still heals "we changed our mind about the formula", this heals "the machine changed underneath it".
Precedence is unambiguous even though both write the same file: v8 calls the same helpers the version blocks do (
DeriveMemorySettings, andDeriveWorkerSettingsextracted here), so it can only ever differ from them by being fresher, never by disagreeing.It compares the last fingerprint, not any. A host resized 16 -> 32 -> 16 GB carries both; a
Containstest would find the stale first one and skip, leaving the 32 GB block still winning by last-occurrence-wins on a box that no longer has 32 GB. The check has to ask the questionpostgresql.confitself answers.What it emits
effective_cache_sizemaintenance_work_memtimescaledb.max_background_workers/max_worker_processesRe-stating the worker pair is what finally makes v2's "never goes stale as collectors are added" claim true — that was true of the formula and false of its marker-keyed application.
The omissions are the load-bearing part
shared_buffers— excluded structurally, not by trusting the cap. The 1 GB cap is the Windows error-487 mitigation (Managed store: cap shared_buffers at 1GB for the co-located reality (v5) + MaxPoolSize=24 — heals the Windows 487 spawn failures #1559, pgsql-bugs BUG #14050 / #18954) and the condition is live on this fleet: 4–205 occurrences/day across the three boxes, zerocould not fork— the retry path holding is exactly the margin a larger segment would spend.min(25% RAM, 1 GB)is already 1 GB above 4 GB so a resize cannot move it; the reason to leave it out is the future one. If the cap is ever raised deliberately, that is a reviewed formula change, not something a resize propagates to production on its own.work_mem— excluded. The formula would take it 31 -> 63 MB, and the only measurements above 31 MB on this store's heaviest read are worse:PlanRegressionSqlat default 26,565 ms, 31 MB 25,617 ms, 512 MB 59,323 ms. 63 MB is not 512 MB and nobody has measured it — which is the point: the evidence that exists points the wrong way. More fundamentally it is the wrong kind of setting for this block. Everything here is a property of the machine;work_memis a per-sort, per-connection ceiling that follows from the query mix. The hardware changed; the sort behaviour did not.max_parallel_workers— untouched. This class has never set it; it sits at the PostgreSQL default of 8 regardless of cores. Deriving it from cores is a plausible want on a 16-core host but it is a behaviour change rather than a staleness fix, and it multiplies the memory story above — each parallel worker gets its ownwork_memfor its share of a node, so raising parallelism raises peak sort memory on exactly the query that already degrades with more of it. Its own evidence, its own PR. This is also why the fingerprint records RAM and hypertables and not cores: with nothing core-derived to re-state, a core-only change has no work to do, and fingerprinting it would append a block of identical values on every resize.Reload semantics
effective_cache_sizeandmaintenance_work_memare SIGHUP-reloadable; the two worker settings are restart-only. The append runs beforepg_ctl starton a service-owned start, so in practice the whole block takes effect on that very start — same as v3 and v7. Nothing is applied to production by this PR; it lands and takes effect at the next natural restart.Red-first evidence
Darling.Testsisnet10.0-windows, so the property was proven with anet10.0reflection harness against the real builtPerformanceMonitor.Darling.Service.dll, mutating the fix into each defect its pins guard:LastIndexOf->Containsresize-back: 32->16 GB still reports STALEshared_buffersv8 NEVER emits shared_bufferswork_memv8 NEVER emits work_memEach mutation fails exactly one pin, and the two positive controls (
BuildMemorySizingConfAppendresolving, and the v3 block still emitting its ownshared_buffers) pass in every run — so a red is the defect, not a harness that stopped resolving methods.Two things the harness caught before they shipped, both worth recording:
"work_mem = "assertion is a substring of"maintenance_work_mem = ", so the unanchored pin was red against correct code. Both the xUnit pin and the harness now anchor on the newline that starts every setting line, with a positivemaintenance_work_memassertion as the guard.🤖 Generated with Claude Code
https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy