Repository navigation
Store upgrade keeps pg-runtime-prev in place instead of deleting and recreating it (prep for #4052) - #4097
Merged
Conversation
erikdarlingdata
marked this pull request as ready for review
September 24, 2026 00:45
This was referenced Sep 24, 2026
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.
Why
Issue #4052 narrows the Darling service account's rights on its install tree: the service keeps Modify on
pg-runtime\andpg-runtime-prev\themselves, but gets only Read & Execute on the install ROOT, so it can no longer create or delete folders directly under the root.DarlingStoreUpgrade.TryAdvanceRuntimeAsyncdeletes and recreatespg-runtime-prev(PreviousRuntimeRootFor(runtimeRoot), a sibling ofpg-runtimeunder the root) before rescuing the current runtime into it, andTryDeleteDirectory(previousRoot)at three cleanup sites does the same thing on success/failure paths. Under the narrowed grant, the delete succeeds (Modify on the folder itself covers that) but the recreate then fails (no rights on the root to add a new child there) — so a runtime rescue fails and the bundled PostgreSQL runtime upgrade stops applying on every host once the narrower ACL ships.This PR makes the service empty
pg-runtime-previn place instead of deleting and recreating the folder. It is correct on today's (wider) ACLs too — no behavior change except that the folder itself persists (empty) across a rescue/cleanup instead of being deleted and remade.What changed
Darling/PerformanceMonitor.Darling.Service/DarlingStoreUpgrade.cs:internal static void EmptyDirectory(string path)— deletes every child file/subfolder recursively, leaves the folder itself; creates the folder only if it's missing, and re-throwsUnauthorizedAccessExceptionwrapped with guidance that the installer/upgrade script must be re-run to pre-create it under the narrowed root.internal static void TryEmptyDirectory(string path)— best-effort wrapper matchingTryDeleteDirectory's never-throws contract, for the cleanup call sites.TryAdvanceRuntimeAsync(~line 1000): replacedif (Directory.Exists(previousRoot)) Directory.Delete(...); Directory.CreateDirectory(previousRoot);withEmptyDirectory(previousRoot);.TryDeleteDirectory(previousRoot)cleanup calls (in the "cannot rescue, defer" catch, the failed-extract catch, andRevertRuntime's post-revert cleanup) withTryEmptyDirectory(previousRoot).TryDeleteDirectoryitself unchanged and still used forpgsql.failedstaging paths (those live INSIDEpg-runtime, which keeps Modify on the root of that subtree — a different shape than the sibling-under-root problem).Step 2 grep: other create/delete/move directly under the install root
Grepped
Darling/PerformanceMonitor.Darling.ServiceandDarling/PerformanceMonitor.Darling.StorageforDirectory.{Delete,CreateDirectory,Move}/File.{Delete,Move}.PerformanceMonitor.Darling.Storagehas none. In.Service, everything else found is a different shape — not a delete/recreate of a folder directly under the install root:DarlingCliCommands.cs:1006Directory.CreateDirectory(targetDirectory)— the viewer-config export directory, which defaults to a subfolder besidedarling.json(or an operator-supplied path via--output), not the install root itself.DarlingCliCommands.cs:1427,1447File.Delete(path)— best-effort cleanup of one file left by a failed viewer export, inside that same export directory.DarlingManagedPostgres.cs:2255Directory.CreateDirectory(ParentOf(_dataDirectory))— creates the data-directory's parent under%ProgramData%\PerformanceMonitorDarling\pg, not under the service's install tree at all (it's a separate ProgramData path, always writable by the service).DarlingFileLoggerProvider.cs:98Directory.CreateDirectory(_logDirectory)— same%ProgramData%\...\logspath, not the install root.DarlingManagedRoles.cs:1589/1712/1716— compose-container credential directory management (ComposeStoreCredentialDirectory), a container-only path unrelated to a Windows install root.DarlingLogHashKeyFile.cs,DarlingManagedRoles.cs(role/key file writes) — files inside directories already covered above (log-hash key directory, credential directories), not root-level creates/deletes.DarlingStoreUpgrade.cs's otherDirectory.Move/Directory.Deletecalls (pgsql swap/revert,.failedstaging, retained rollback data-directory copies) all operate on paths insidepg-runtimeor beside the data directory (a ProgramData path), not directly under the service's install root.None of these match the "delete-then-recreate a folder that is a direct sibling under the narrowed install root" shape that #4052 is fixing. No further fixes made in this lane; nothing else needs one.
Tests
Darling/Darling.Tests/DarlingStoreUpgradeTests.cs:RuntimeAdvance_LeavesPgRuntimePrevInPlace_RatherThanDeletingAndRecreatingIt: proves the rescue leaves the samepg-runtime-prevfolder in place (creation time unchanged) rather than deleting/recreating it, and that a stale file inside it is gone afterward.RuntimeAdvance_APreviousRuntimeItCanClear_IsReplacedAndTheSwapProceeds(existing) already asserted the stale file is gone after a successful rescue — left as-is; it still passes under the newEmptyDirectorypath.Darling/Darling.Tests/DarlingStoreUpgradeRevertTests.cs:RevertRuntime_Reverts_RecordsTheBlock_AndSaysSo: updated the post-revert assertion from "the folder is gone" to "the folder exists and is empty" (matching the newTryEmptyDirectorycleanup behavior).Other existing assertions of the shape
Assert.False(Directory.Exists(PreviousRuntimeRootFor(...)))(lines 961, 1183, 1994, 2026 inDarlingStoreUpgradeTests.cs) are all on paths where the rescue never ran at all (no-stamp branch / TimescaleDB guard / downgrade refusal) —pg-runtime-prevwas never created in those fixtures, so those assertions are unaffected and still correct.Ran targeted classes iteratively (
DarlingStoreUpgradeTests,DarlingStoreUpgradeRevertTests) confirming: no new failures introduced (verified by diffing failures against a pre-change build of the same classes on this branch).Full suite (once, at the end)
214 failures — consistent with the ~215 Windows-only baseline this Mac rig has on
dev(macOS lacks real Windows ACL/process semantics the tests exercise; confirmed by running the same suites against unmodifieddevand seeing the identical 5 + 9 failures in the two classes this PR touches). No failure in either touched class is new:DarlingStoreUpgradeTestshas the same 5 pre-existing failures ondevand on this branch (117 total on this branch vs 116 ondev, the +1 being this PR's new passing test);DarlingStoreUpgradeRevertTestshas the same 9 pre-existing failures on both.What I did NOT verify
UnauthorizedAccessExceptionunder Read & Execute-only root) is proven only by the code path and by the existing.NETDirectory/FileAPI semantics, not by a live ACL test._Gatedsuffix) — none touch this code path; they were skipped as usual (noDARLING_TEST_PGRUNTIME_*env vars set).Part of #4052: service-side prep only. The install-script ACL narrowing itself is PR #4090, which must merge after this one. (Worded "Part of" on purpose: the merge watcher closes any issue a body says it closes.)
CHANGELOG entry