Repository navigation
Carry postgresql.auto.conf across a store major upgrade (#4253) - #4280
Conversation
Add the postgresql.auto.conf reader (ParseAutoConf) and the carry step (CarryAutoConfAsync) that copies a pre-upgrade cluster's ALTER SYSTEM settings into the new cluster after pg_upgrade, validating each one against the new binaries before keeping it. Wired into UpgradeDataDirectoryAsync right after the data-directory swap commits, before anything gives the new cluster its first start. Checkpoint: reader + pure tests only. The live carry test and the in-place upgrade test's new assertion follow in the next commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…lace upgrade test (#4253) CarryAutoConfAsync_CarriesAGoodSetting_LeavesOutOneTheNewBinariesReject_AndTheStoreStarts runs on this machine: builds an old data directory with the current runtime, ALTER SYSTEM's work_mem, hand-appends one setting no binaries accept, runs the carry, starts the new cluster, and reads work_mem back with SHOW. The rejected setting is logged, left out, and kept in the pre-upgrade side file; the store starts regardless. The real 17->18 pg_upgrade test (UpgradeInPlace_..._Gated, needs DARLING_TEST_PGRUNTIME_OLD) now also sets work_mem on the old cluster and reads it back on the upgraded one. It compiles here but is gated and does not run on this machine (port 55432 is a Docker container); the nightly runs it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
A per-setting postgres -C probe can throw (TimeoutException past 30s,
or OperationCanceledException on a service stop) instead of returning
a bad exit code. Before this, the loop exited with the new cluster's
postgresql.auto.conf holding whatever the last write left there: one
candidate line nothing had verified, which the post-commit handler
then leaves for the next start to read regardless of the failure.
Wrap the probe loop, the final write and the combined check in a
try/catch. On any exception, reset postgresql.auto.conf to the
header-only file first (CancellationToken.None, since the caller's
token may already be why it threw), log a warning naming the
pre-upgrade copy, then rethrow so the post-commit handler still sees
the failure.
Also validate GUC names in ParseAutoConf against PostgreSQL's
identifier grammar before carrying them: a hand-edited line whose name
holds a quote or a space could split the -C "{name}" argument.
Skipped names are returned to the caller to log as not carried.
Adds a probe-substitutable overload of CarryAutoConfAsync as a test
seam (production still wires DarlingManagedPostgres.RunToolAsync
unchanged), plus tests for both throw shapes and the name validation.
Revert-proofed: removing the try/catch makes both new tests fail with
the file holding the unverified candidate line; restored.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
|
Pushed The fix
Tests (
|
Round-1 security review at 6cc5ae9Scope: Medium 1: setting values go to logs that more accounts can read than the source fileLocation: ALTER SYSTEM stores settings such as On a healthy install, only SYSTEM, Administrators and the service account can read the source file. The logs have more readers:
The skipped-names warning at Precondition: the operator stored a secret with ALTER SYSTEM. Fix: log names only on Medium 2: a setting can pass both probes and still stop the first startLocation: For an ordinary setting,
The next start is the quiesced TimescaleDB update or the service start. The store stays down until someone edits the file by hand. The method summary at Fix: replace the combined Low 1: the reset in the catch can fail and leave a line that no probe acceptedLocation: I checked every exit. Each exception inside the try resets the file to the header with
Fix: put the reset in its own try/catch and rethrow the original exception. If the header write fails, try Also write the header back right after a probe rejects a line, so that a rejected line never waits in the file. For the hard stop, we recommend a marker file that is written before the loop and deleted after Low 2: the pre-upgrade copy has no ACL of its own, and nothing deletes itLocation: Who can read the folder: INTERACTIVE gets traverse on the folder itself only. The data directory has no ACL of its own and inherits the same entries. So on a healthy install, the copy has the same readers as the postgresql.auto.conf in the data directory. The copy does not get a weaker ACL. The gaps:
Fix: call Low 3: the probe does not normalize the data directory pathLocation: The name regex at
But every older caller builds its Fix (hardening only): call Checked, no finding
Not security
|
…, #4280) Fixes all 7 findings from the round-1 security review of the auto.conf carry (#4280 comment 5833383587): redacts setting values and secret-looking reject reasons from every carry log line (Medium 1), replaces the combined -C probe with a real start/stop of the new cluster so a setting that only breaks a real start (ssl = on with no certs) still ends up dropped (Medium 2), makes the catch's file reset fall back to delete-then-throw instead of silently losing the original exception (Low 1), ACLs and lists the pre-upgrade auto.conf copy for --harden-files (Low 2), normalizes a trailing separator in ResolveDataDirectory (Low 3), accepts PostgreSQL's optional "name value" form with no "=" (parse note), and corrects the byte-for-byte doc claim to say it only holds for UTF-8 content (doc note). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Round-1 security review fixes, at 619a2e8 (branch
|
Round-2 review at 619a2e8Scope: commit 619a2e8 only, in There is one High, three Medium and two Low findings. Q3 and Q5 found nothing. Q4 found no security problem, only the parse gap in Low 2. Q2 found the problems in Medium 3 and Low 1, and nothing else. High 1: on a store with network exposure, the trial start skips the SSL setup that the real start runsLocation: The trial start and the real start pass different server options. The trial passes the port, a loopback
A relative Precondition: network exposure is on, and the operator set one of these ssl settings with ALTER SYSTEM. Before this PR, the store started and lost the setting with no warning. With this PR, the store does not start. Fix: make the trial use the SSL options that the real start will use. Pass the SSL part of
The same marker also closes Medium 3. Medium 1: a trial postmaster that does not stop is adopted as the store, still quiescedLocation:
Sequence A, where the start succeeds and the stop fails:
Sequence B, where the start fails while the postmaster is still starting:
Sequence C is a race. The header write at Likelihood: a failed fast stop of an idle new cluster is rare. But the same failure on the quiesced update is the reason why Fix: use the pattern from
Medium 2: an unrelated start failure drops every setting, and the warning blames the settingsLocation: The catch at
A test in this commit already takes this path. Can it hide a real upgrade failure? Not for long. The quiesced update and the real start fail next, and each logs its own reason with the server log tail. But the upgrade alert says Succeeded with no warning, and every setting is gone. The operator also reads a warning that blames the settings. Fix: the private port from Medium 1 removes the port case. Use If the retry also fails, the cause is somewhere else. In that case, say so, name Medium 3: a hard stop during the carry still leaves unverified lines, and round-1 Low 1 is only partly fixedLocation: Round-1 Low 1 made two more recommendations. The first was to write the header back right after a probe rejects a line. The second was a marker file that lets the next start reset the file after a hard stop. This commit does neither. The fix report says that all 7 findings are fixed.
The window is a few seconds, once per major upgrade. The trial start makes the window longer, and during the trial the file holds the full carried set. Fix: use the marker from High 1, and read it at the next start. If it says that the carry did not finish, write the header-only file. Then log the dropped names, and delete the marker. Low 1: if both the reset and the delete fail, the log loses the original causeLocation: The new exception message names the file, the reset error and the delete error. The original exception is only the Fix: add Low 2: the new "name value" split does not match the PostgreSQL scannerLocation:
A carriage return between a name and its value is whitespace to PostgreSQL. But Fix: split and trim only on space and tab, as guc-file.l does. Add a line that cannot be split to Q3, logging: no findingI read every log call in
Q4, the "name value" form: no security findingI read If a line has an Q2, the reset fallback: nothing beyond Medium 3 and Low 1I read the outer catch at
Q5, the pre-upgrade copy: no findingI read
|
Round-2 lane: setup done, design validated, no code changes yet (context budget)Branch I read the full round-2 review, confirmed every finding against the current (post-merge) source, and had a Line-number map (619a2e8 review refs -> current post-merge file)
Validated design (confirmed against source + a second-reviewer pass)Item 1 (Medium 1, trial lifecycle). In Because items 2 and 3 both rewrite this same trial block (SSL options, then a retry-once), the advisor's The Item 2 (High 1, SSL) + its fallback marker (closes Medium 3 too). Factor Fallback marker: a new file in the data directory, e.g. Item 3 (Medium 2, unrelated start failures). The trial uses Test Item 4 (Medium 3, hard stop, uses item 2's marker). At the START of each service start, AFTER the item-1 Item 5 (Low 1). Add Item 6 (Low 2). Four touch points in Item 7 (Q3 hardening). Test/build readiness for whoever picks this up
What's committed vs. notOnly the merge commit ( Recommended next stepRe-dispatch a fresh lane on this same branch with this comment as its brief — it can skip straight to Item 1's |
…ode whitespace guc-file.l, PostgreSQL's own postgresql.auto.conf tokenizer, only treats ' ' and '\t' as whitespace. ParseAutoConf's line trim, its no-'=' name scan, and the name/value trims in the 'name=value' branch all used char.IsWhiteSpace/string.Trim(), so a line separated by e.g. U+00A0 (NBSP) was mis-split as if PostgreSQL would split it there too, when it would not. Those four touch points now trim/scan on ' '/'\t' literally. A 'name value' line (no '=') with no space/tab left to split on used to just `continue`, silently dropping it. It now lands in skippedLines by line number, same as an invalid name, so CarryAutoConfAsync logs it as not carried instead of losing it silently. DecodeAutoConfValue is untouched: RawLine already ships byte-for-byte, so only the split/trim needed to match guc-file.l. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…alified setting
s_secretNameFragments gains "token", "credential" and "auth", so names like
myext.auth_token now match NameMayHoldASecret directly. The rejected-setting
branch also now withholds {Reason} whenever the name contains a dot, not just
when it matches a known fragment: PostgreSQL's own reject reason can repeat the
offending value verbatim, and this class cannot vouch for every extension's own
wording well enough to know none of them ever echoes a value either. The log
message text now names both reasons a reject reason can be withheld for, so it
stays accurate for a dot-qualified name that does not otherwise look secret-shaped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Lane report: #4280 round-2 items 6 and 7 (auto.conf parser, secret-name screen)Branch
Another lane pushed items 1, 3, 5 to the same branch concurrently; Item 6 (Low 2):
|
…4280 item 1) Round-2 review Medium 1: CarryAutoConfAsync's belt-and-braces real start used the caller-supplied port - the store's own configured one - on the assumption it was free because the old cluster on it had already stopped. That assumption is not always true, and a busy port would fail the trial and drop good settings for a reason that had nothing to do with them. The trial now gets its own private port from FindFreeLoopbackPort(), and reuses the existing quiesced-update marker/orphan lifecycle so a trial this call cannot stop is picked up and stopped by the next start's StopQuiescedUpdateOrphanAsync, the same as an interrupted TimescaleDB update. EnsureRunningAsync now also calls StopQuiescedUpdateOrphanAsync unconditionally before IsRunningAsync, since pg_ctl status cannot tell a leftover trial apart from the store's own postmaster on any other port. A failed stop after a successful trial start no longer falls into the outer reset catch, which would otherwise drop settings the trial just verified; the leftover marker carries that failure instead. The trial is now a local function inside CarryAutoConfAsync so the retry in a later item can call it twice. The now-fully-unused `port` parameter is removed from both CarryAutoConfAsync overloads and their one production call site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
item 1) The concurrent secret-fragments lane's new test still called the pre-item-1, port-taking overload. Mechanical: drop the now-removed argument. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…a double failure (#4280 items 3, 5) Item 3 (Medium 2, unrelated start failures): the trial now uses QuiescedStartWaitSeconds (900s) instead of the default 120s. On a trial-start failure, postgresql.auto.conf is reset to header-only first, then the SAME trial is retried once against that empty file. If the retry starts, the carried settings really were the cause - keep the existing warning and add a pg.log pointer. If the retry also fails, the cause is unrelated to the settings, so nothing is dropped or blamed; a new, fixed-message InvalidOperationException naming pg.log (never either attempt's own exception, which can carry the server log tail) is thrown so the post-commit handler reports a warning on the upgrade's outcome instead of a silent success. That throw passes through the outer reset catch on its way out, which re-runs the same idempotent header-only reset and logs its own line again - accepted rather than special-cased around. CarryAutoConfAsync_CarriedSecretNamedSetting_NeverLogsItsValue now expects that throw: "unused-bin-dir" has no real pg_ctl.exe, so both the trial and the retry fail there. Item 5 (Low 1): the double-cleanup-failure exception (reset write failed, then File.Delete also failed) now folds the original exception's own message into its text, alongside the existing reset/delete reasons, so the top-level message is self-contained. Safe to do: every exception that can reach this catch post items 1/3 (a probe timeout/cancellation, a file I/O fault, or item 3's own fixed-message throw - StopClusterConfirmedAsync never throws) is already one this class never lets carry a setting's value or the server log tail, so no exception reaching this point can leak either through the newly-added text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Lane report: #4280 round-2 items 1, 3, 5 (the trial's lifecycle and its failure handling)Branch Item 1: trial lifecycle (Medium 1) - commits
|
…item 0)
ParseAutoConf's name-value form (no '=') built its value with the full-Unicode
Trim(), stripping a leading NBSP that PostgreSQL's own guc-file.l tokenizer
does not treat as separating whitespace. That silently carried "64MB" for a
line that actually held NBSP+"64MB". Trim(' ', '\t') now matches the '=' form
two lines below it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…xed message (#4280 item 0b) TryStartTrialAsync's catch (Exception) also swallowed OperationCanceledException from a service stop mid-trial, turning it into an ordinary "failed start": the header-only retry ran under the same already-cancelled token, failed the same way, and threw the fixed "would not start even with an empty postgresql.auto.conf" message — wrong for a stop that has nothing to do with the carried settings. catch (Exception) when (!cancellationToken.IsCancellationRequested) lets the cancellation propagate; the finally still stops the trial server either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
#4280 item 2) The auto.conf carry trial ran without the store's own SSL options, so a carried setting that only fails to start under SSL (ssl_ca_file naming a missing file is the concrete case) passed the trial and then broke the real start. BuildNetworkPlan's cert generation is now behind a Lazy<NetworkPlan> so UpgradeContext can carry a Func<string> (never the private NetworkPlan/ NetworkMode types) that builds the SAME SSL option string BuildServerRuntimeOptions uses, now factored into BuildSslServerOptions. It rides every trial attempt, the header-only retry included. A new darling-autoconf-carry.state marker (state on line 1, carried NAMES only, never a value) tracks the carry across a crash: "carrying" while candidates are being tried, "trial-passed" once the trial with the carried settings starts. EnsureRunningAsync now retries the real start once, on a header-only auto.conf with the names logged as dropped, when the real start fails right after a trial-passed marker; a second failure throws unwrapped, as before. The marker is deleted at every header-only reset and after the first good real start, the already-running path included. Two source-anchor census tests (QuiescedUpdate_IsWiredBeforeTheStoreOpens..., LegacyMaintenanceWorkMemHeal_RunsBefore...) updated for the new call shapes they scan for; both still check the same ordering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…4280 item 2/4) Covers the states (carrying, trial-passed), that only names ever reach the log (never the 256MB/999MB values), ResetAutoConfCarryAsync's header-only write plus marker delete, and TryDeleteAutoConfCarryMarker (what EnsureRunningAsync calls after a good real start). Uses the helpers directly rather than a full EnsureRunningAsync bootstrap, which needs a second pg-runtime fixture this lane's rig does not have. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Round-2 part 2 (items 0, 0b, 1=item 2, 2=item 4) — pushed to
|
EnsureRunningAsync's real-start fallback caught any exception on a trial-passed carry, including OperationCanceledException from a service stop. That reset postgresql.auto.conf to header-only and dropped settings that had already passed their isolated trial, for a cancellation that was never a start failure. ShouldFallBackToHeaderOnly(marker, cancellationToken) is now the catch clause's own `when` filter: trial-passed and not cancelled is the only true case; trial-passed-but-cancelled, carrying, and null are all false, same as the fallback never running. Darling.Tests.DarlingManagedPostgresTests: 250 total, 0 failed, 2 skipped (unrelated, need DARLING_TEST_PGRUNTIME_OLD). Revert-proved by dropping `&& !cancellationToken.IsCancellationRequested`: the cancelled-case theory row failed as expected (Assert.Equal expected False, actual True), restored after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
… marker (#4280) ResetAutoConfCarryAsync's header-only write could throw (IOException, UnauthorizedAccessException), get logged as a warning, and then fall straight through to log "NOT carried ... reset to header-only" and delete the carry-state marker anyway. The next start then ran the still-carried, unverified settings with no marker left to retry the reset. ResetAutoConfCarryAsync now returns Task<bool>: false only when the header-only write itself failed, in which case neither the "NOT carried" warning nor the marker delete runs. Both callers in EnsureRunningAsync follow: the leftover-"carrying" recovery only clears autoConfCarryMarker when the reset returned true (a failed reset still lets the start proceed, same as before); the real-start fallback rethrows the original start failure when the reset returns false, since a retry against the same unwritable file cannot help. Darling.Tests: DarlingStoreUpgradeTests + DarlingManagedPostgresTests + DarlingStoreUpgradeRevertTests + DocCommentHygieneTests: 423 total, 0 failed, 8 skipped (gated tests that need DARLING_TEST_PGRUNTIME_OLD / _NEWZIP / _PREVIOUS, not part of this rig). New test ResetAutoConfCarryAsync_HeaderOnlyWriteFails_ReturnsFalse_KeepsMarkerAndFile makes postgresql.auto.conf read-only, then asserts false, the marker still on disk, the auto.conf text unchanged, and no captured log line containing "reset to header-only". Revert-proved against the old fall-through body: the new test failed on "a failed header-only write must return false", restored after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…is dropped under SSL (#4280) The round-1 security review's Medium 2 case had no live test: a carried ssl_ca_file naming a missing file passes its own per-setting `-C` probe (that check only reads the config, never touches SSL/certificate setup), so an SSL-off trial starts fine and wrongly keeps it, a landmine for the operator's next SSL-on start. CarryAutoConfAsync's sslServerOptions parameter (wired in an earlier round of this PR) rides the same trial start, so passing a real SSL delegate makes the combined trial actually turn SSL on and catch it. CarryAutoConfAsync_CarriedSslCaFileNamesAMissingFile_SslOnTrialDropsIt_NeverLogsItsValue carries ssl_ca_file pointed at a file that never exists, drives the trial through DarlingManagedPostgres.BuildSslServerOptions with a real cert/key pair from DarlingManagedPostgres.EnsureServerCertificate (the product's own certificate code, not a hand-made file), and asserts: CarriedNames is empty, RejectedNames holds ssl_ca_file, the log line reads "Dropped: ssl_ca_file" and never the missing path, the new postgresql.auto.conf has no ssl_ca_file line, and the new cluster starts for real with ssl_ca_file back at its empty default. Darling.Tests: DarlingStoreUpgradeTests + DarlingManagedPostgresTests + DarlingStoreUpgradeRevertTests + DocCommentHygieneTests: 424 total, 0 failed, 8 skipped (gated tests needing DARLING_TEST_PGRUNTIME_OLD / _NEWZIP / _PREVIOUS, not part of this rig; none newly skipped by this change). Revert-proved by passing sslServerOptions: null instead of the delegate: the trial then runs SSL-off, ssl_ca_file passes and lands in CarriedNames, and Assert.Empty(result.CarriedNames) failed as expected ("Collection was not empty: [\"ssl_ca_file\"]"); restored after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Lane 3 report: items 1-3 (#4280 round 2, part 3)Branch Item 1: a cancellation must not drop the carried settingsCommit
Item 2: a failed reset must not claim success or delete the markerCommit
Item 3: the live SSL test (round-1 review's case)Commit
Build and test totals
Final run, 8 skipped are all pre-existing
Hard rules checkedNo log line, exception message, or marker line holds a setting value or the server log tail in any of the Nothing deferredAll three items are done, in-lane, no follow-up issue needed. |
The member scan's brace walk stops at the property pattern's own closing brace in ShouldFallBackToHeaderOnly's expression body, stranding the trailing cancellation check. Not T-SQL, not a tempdb label, not exception text, so no census reads a site of that kind here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
|
CI fix pushed: Failure: Why the range stops short: the method is expression-bodied: internal static bool ShouldFallBackToHeaderOnly(DarlingStoreUpgrade.AutoConfCarryMarker? marker, CancellationToken cancellationToken) =>
marker is { State: DarlingStoreUpgrade.AutoConfCarryStateTrialPassed } && !cancellationToken.IsCancellationRequested;
What falls outside is a boolean check on the cancellation token: not T-SQL, not a tempdb label, and not an exception-text site (no Change: added one line plus a comment to /* #4253: an expression-bodied member whose body opens with a property pattern
(`marker is { State: ... }`) before the rest of the expression. The walk's brace match closes the
range at the pattern's own closing brace, stranding the trailing `&& !cancellationToken.
IsCancellationRequested`, a boolean check on the token, not T-SQL and not a tempdb label, so no
census reads a site of that kind here. */
"Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs ShouldFallBackToHeaderOnly",Verification:
PR left as draft, not readied, not merged, not armed for auto-merge. 🤖 Generated with Claude Code |
Closes #4253.
Why
A major-version upgrade of the managed store (
DarlingStoreUpgrade, which runspg_upgrade) builds the new cluster withinitdb. Neither step copies the old cluster'spostgresql.auto.conf. So everyALTER SYSTEMsetting an operator made went back to the service's defaults on the next major upgrade, with no warning. The issue's production example wasshared_buffers, raised from 1 GB to 8 GB.What changes
In
Darling/PerformanceMonitor.Darling.Service/:DarlingStoreUpgrade.cs(the carry),DarlingManagedPostgres.cs(recovery at the next start, and the SSL options the trial start shares with the real start), andDarlingCliCommands.cs(--harden-filescovers the pre-upgrade copy).Reading the file:
ParseAutoConfIt follows PostgreSQL's own grammar for this file:
name = valueorname value. Only space and tab count as whitespace, as in PostgreSQL's tokenizer, so a non-breaking space is never taken as a separator.#comments are skipped, including the two header linesALTER SYSTEMwrites.''is an embedded quote and\\is an embedded backslash. An unquoted bare token is accepted too.work_memandtimescaledb.max_background_workersare valid) is skipped. The warning gives its line number, never its text, because that text can be part of a value.Carrying it:
CarryAutoConfAsyncpostgres -C <name> -D <newDataDirectory>. That command parses the whole configuration and exits non-zero on an unknown name or an out-of-range value.conninfo,command,password,passphrase,secret,key,token,credential,auth), because the reason can repeat the value.postgresql.auto.conf.pre-upgradein the new data directory's parent folder, and its ACL is hardened the same way as the service's other private files.--harden-filescovers it too.A trial start before the real one
Some settings pass
postgres -Cbut still stop a real start: an inheritedssl = onwith no certificate files, or anssl_ca_filethat names a missing file. So once the per-setting checks pass, the carry starts the new cluster on a private loopback port and stops it again.postgresql.auto.confgoes back to its header only, and the trial runs once more on that empty file. If the retry starts, the settings were the cause: a warning names each dropped setting and points atpg.log. If the retry fails too, the cause is not the settings, and the step throws a fixed message that points atpg.log, never the server's own error text.When the carry fails partway
postgresql.auto.confback to its header only, and the exception propagates. If that write fails, the file is deleted, since PostgreSQL treats it as optional. If the delete fails too, the error names the file and the manual step.darling-autoconf-carry.state, holds the carry's state (carryingortrial-passed) and the setting names, never a value:carryingmarker at the next start means the carry never finished. The file goes back to its header only, and the dropped names are logged.trial-passed, the file goes back to its header only and the start is retried once. A service stop during that start does not trigger this.Where it runs
It is step 8 of
UpgradeDataDirectoryAsync: right after the data-directory swap commits, and before the new cluster's first real start. That first start is not only the operator-visible one later in startup. The quiet TimescaleDB update (#3908) often runs right after an upgrade, and it starts the new data directory to move the extension. A carry failure lands in the existing post-commit handler, which logs a critical warning and still reports the upgrade as succeeded, on the new major. The issue requires that the carry never stop the store.Service account
postgres -Crefuses to run under an administrator token. The service never runs as LocalSystem (seeinstall-darling.ps1), so the probe runs under the same account as the store itself.How this fits with #4215
#4215 proposes one service-owned config file in place of the marker blocks appended to
postgresql.conf. A hand edit inpostgresql.confis out of scope here. Once #4215's file exists, a hand edit can move into it, and an upgrade step can carry that file the same way. Until then, a hand edit inpostgresql.confis still lost on a major upgrade. This PR restoresALTER SYSTEMsettings, which is what the issue's example hit.Lite
Lite has no managed PostgreSQL store, so there is nothing to mirror.
Review rounds
Test plan
ParseAutoConf: quoting and escaped quotes, comments and the header lines, thename valueform, space-and-tab-only splitting (a non-breaking space is not a separator), invalid names skipped by line number, and a name set twice.-Cbut fails a real start ends header-only and the store starts; a carriedssl_ca_filenaming a missing file is dropped when the trial uses the SSL options, and kept with a null delegate (revert-proven); a cancellation during the trial propagates; with nopostgresql.auto.confat all (where the delete fallback leaves it), the new cluster still starts.ShouldFallBackToHeaderOnly) covers trial-passed, cancelled, carrying and no marker, plus a source pin on the catch; a reset that cannot write keeps the marker and the file (revert-proven).UpgradeInPlace_OldMajorStoreWithRealData_UpgradesAndKeepsEverything_Gated) setswork_membefore the upgrade and reads it back after. It needsDARLING_TEST_PGRUNTIME_OLD, so it skips locally; the nightly runs it.8beb4d74:DarlingStoreUpgradeTests,DarlingManagedPostgresTests,DarlingStoreUpgradeRevertTestsandDocCommentHygieneTests, 424 total, 0 failed, 8 skipped (they need a second runtime).CI fix
The CI run at
8beb4d74failedTsqlConventionGuardTests.TheMemberScan_ReadsEveryDeclarationWhole, which asks the PR body to say which way its list moved. It ARRIVED:ShouldFallBackToHeaderOnlyinDarlingManagedPostgres.csis expression-bodied and opens with a property pattern (marker is { State: ... }), so the member map's brace match ends its range at the pattern's closing brace. What falls outside is&& !cancellationToken.IsCancellationRequested, a check on the token: not T-SQL, not a tempdb label and not exception text, so no census counts a site of that kind there.298be53a(after a merge oforigin/dev) adds the member toKnownTruncatedRangeswith that reason. The product code is unchanged. Report: PR comment 5837870319.CHANGELOG entry
SECTION: Fixed
ENTRY:
ALTER SYSTEMsetting. A raisedshared_buffers, for example, went back to the default with no warning. The upgrade now checks each setting against the new version, test-starts the new store with the settings it accepts, and keeps them if that start works. A setting the new version rejects is left out with a warning that names it. If the test start fails, the store starts on its defaults instead, and a warning names each setting it dropped. The logs name settings but never show their values. The original file is kept aspostgresql.auto.conf.pre-upgradenext to the data directory, so a dropped setting can be re-applied by hand.REF:
[Carry postgresql.auto.conf across a store major upgrade (#4253) #4280]: Carry postgresql.auto.conf across a store major upgrade (#4253) #4280
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ