fix: use staged installation to prevent partial installs appearing in asdf list#2245
Open
RishiAhuja wants to merge 1 commit intoasdf-vm:masterfrom
Open
fix: use staged installation to prevent partial installs appearing in asdf list#2245RishiAhuja wants to merge 1 commit intoasdf-vm:masterfrom
RishiAhuja wants to merge 1 commit intoasdf-vm:masterfrom
Conversation
…leanup of incomplete installations
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.
Problem
When
asdf install <plugin> <version>is interrupted mid-way — by Ctrl+C(SIGINT), SIGKILL, a dropped network connection, or a power outage — the
partially installed version directory is left behind on disk. Because
asdf listdetects installed versions simply by checking whichsubdirectories exist under
$ASDF_DATA_DIR/installs/<plugin>/, theincomplete installation is reported as if it were valid:
The user is then forced to manually run
asdf uninstall nodejs 22.14.0before they can retry the install. This is confusing because the version
was never successfully installed.
Reported and discussed in: #2184
Root Cause
InstallOneVersionininternal/versions/versions.gocreated the finalinstall directory (e.g.
installs/nodejs/22.14.0/) withos.MkdirAllbefore running the plugin's install callback. If the process was
killed at any point after that
MkdirAllcall, the directory remained ondisk.
IsInstalledandInstalledboth useos.Stat/os.ReadDironthe installs directory, so they reported the partial directory as a
complete installation.
Solution: Staged Installation with Atomic Rename
This PR implements the staged installation pattern, which is the
standard approach used by package managers (npm, cargo, homebrew, apt)
to solve exactly this class of problem.
Before:
After:
The final directory (
22.14.0/) never exists while an install is inprogress. It only appears, atomically, after the install has fully
succeeded.
os.Rename()within the same filesystem is an atomic POSIXoperation — it either completes fully or not at all.
This approach is strictly more robust than signal handling because it
also covers SIGKILL and power loss, which cannot be caught by any process.
How does it look now?
Changes
internal/installs/installs.goincompleteSuffix = ".incomplete"constant.Installed()— now skips any subdirectory whose name ends with.incomplete, so interrupted installs are never surfaced byasdf list.StagingPath()— new function that returnsInstallPath(...) + ".incomplete". This is the directory the plugininstall script writes into.
CleanIncomplete()— new function that removes the stagingdirectory if it exists. Safe to call when no such directory is present.
Called at the start of every install attempt to clean up stale
directories left behind by previous crashes.
internal/versions/versions.goChanges are entirely within
InstallOneVersion:stagingDirviainstalls.StagingPath()alongside theexisting
installDiranddownloadDir.installs.CleanIncomplete()before starting to wipe any stalestaging directory from a prior interrupted install.
stagingDir(notinstallDir) asASDF_INSTALL_PATHin theenvironment given to the plugin's download and install callbacks.
Plugin scripts are unaffected — they simply write to
$ASDF_INSTALL_PATHas before.
stagingDir(notinstallDir) withos.MkdirAll.stagingDir(same cleanupbehaviour as before, now correctly targeting the staging path).
os.Rename(stagingDir, installDir)to atomically promote the staged directory to the final install path.
If the rename itself fails (very rare edge case), cleans up the
staging directory and returns an error.
internal/installtest/installtest.goUpdated the test helper
InstallOneVersionto mirror the samestaging-and-rename flow as the production code, so tests that rely on
this helper produce installations consistent with the new behaviour.
Tests
internal/installs/installs_test.go(new test cases)TestIsInstalled/returns false for incomplete installation.incompletedirectory does not makeIsInstalledreturntrueTestInstalled_FiltersIncomplete/does not list incomplete installationsInstalled()excludes.incompletedirs from its resultsTestStagingPath/returns install path with .incomplete suffixStagingPath()produces the correct stringTestCleanIncomplete/removes incomplete staging directoryCleanIncomplete()recursively deletes the staging directoryTestCleanIncomplete/succeeds even when no incomplete directory existsCleanIncomplete()is safe to call when nothing existsinternal/versions/versions_test.go(new test cases)TestInstallOneVersion/staging directory does not exist after successful install22.14.0.incomplete/is gone and22.14.0/exists with correct contentsTestInstallOneVersion/staging directory is cleaned up when install failsTestInstallOneVersion/cleans up stale incomplete directory from previous failed install.incompletedir (simulating a prior crash) is cleaned up and the new install succeedsBehaviour by Failure Mode
RunCallback. Staging dir cleaned up. Final dir never created.asdf list. Cleaned up automatically on next install attempt.Testing Locally
Compatibility
ASDF_INSTALL_PATHvariable passed toplugin scripts now points to the staging directory during installation
instead of the final directory. The path format is otherwise identical
and the variable name is unchanged. Plugins that write to
$ASDF_INSTALL_PATH(which is all of them) continue to work withoutmodification.
.incompletedirectories already sitting on disk from previousversions of asdf will be invisible to
asdf listand will beautomatically removed on the next
asdf installattempt for thatversion.