mirror of
https://github.com/nvm-sh/nvm.git
synced 2026-08-25 00:00:22 +08:00
[New] nvm install: serialize concurrent installs of the same version
Two `nvm install <same version>` runs could race on the version directory - one removing or replacing it while the other reads or writes it. Take a per-version advisory lock (an atomically-created directory under $NVM_DIR/.cache locks) around the binary/source install, so a second run of the same version waits for the first; installs of different versions never contend. NVM_INSTALL_LOCK_TIMEOUT (seconds, default 600) bounds the wait, after which nvm reports the lock path so a lock left by a killed install can be removed. NVM_INSTALL_LOCK_STALE (minutes, default 0 / off) opts into automatically stealing a lock older than that, for unattended or CI use.
This commit is contained in:
@@ -875,6 +875,10 @@ Additionally, nvm modifies `PATH`, and, if present, `MANPATH` and `NODE_PATH` wh
|
||||
The following environment variables can be set to configure `nvm install`:
|
||||
|
||||
- `NVM_NO_SOURCE_FALLBACK` - when `1`, a failed binary download aborts instead of silently falling back to a (much slower) from-source compile; the persistent equivalent of the `-b` flag, and mutually exclusive with `-s`.
|
||||
- `NVM_INSTALL_LOCK_TIMEOUT` - seconds to wait for a concurrent install of the same version to finish before giving up (default `600`). On timeout, nvm prints the lock path so a lock left behind by a killed install can be removed.
|
||||
- `NVM_INSTALL_LOCK_STALE` - minutes after which an install lock is assumed abandoned and stolen automatically; `0` (the default) never steals.
|
||||
|
||||
`nvm install <version>` takes a per-version advisory lock (a directory under `$NVM_DIR/.cache/locks`), so two shells installing the same version at once cannot corrupt its version directory; installs of *different* versions never block each other.
|
||||
|
||||
|
||||
## Bash Completion
|
||||
|
||||
@@ -3230,6 +3230,80 @@ nvm_cache_dir() {
|
||||
nvm_echo "${NVM_DIR}/.cache"
|
||||
}
|
||||
|
||||
# Turn a version into a filesystem-safe lock name. Versions that reach here are
|
||||
# already restricted to [0-9A-Za-z._+-], but be defensive about anything else.
|
||||
nvm_install_lock_name() {
|
||||
command printf '%s' "${1-}" | command tr -c '0-9A-Za-z._+-' '_'
|
||||
}
|
||||
|
||||
# Acquire an advisory, per-version install lock so two concurrent
|
||||
# `nvm install <same version>` runs cannot race on the same version directory
|
||||
# (one removing/replacing it while the other reads or writes it). The lock is a
|
||||
# directory created with `mkdir`, which is atomic across POSIX filesystems.
|
||||
#
|
||||
# Tunables (env vars):
|
||||
# NVM_INSTALL_LOCK_TIMEOUT seconds to wait for a held lock (default 600)
|
||||
# NVM_INSTALL_LOCK_STALE minutes after which a lock is assumed abandoned
|
||||
# and stolen; 0 (default) never steals
|
||||
#
|
||||
# On success the lock path is recorded in NVM_INSTALL_LOCK for the matching
|
||||
# nvm_release_install_lock.
|
||||
nvm_acquire_install_lock() {
|
||||
local VERSION
|
||||
VERSION="${1-}"
|
||||
if [ -z "${VERSION}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local LOCK_ROOT
|
||||
LOCK_ROOT="$(nvm_cache_dir)/locks"
|
||||
# If the lock directory can't be created, don't block the install over it.
|
||||
command mkdir -p "${LOCK_ROOT}" 2>/dev/null || return 0
|
||||
|
||||
local LOCK
|
||||
LOCK="${LOCK_ROOT}/$(nvm_install_lock_name "${VERSION}")"
|
||||
|
||||
local TIMEOUT
|
||||
TIMEOUT="${NVM_INSTALL_LOCK_TIMEOUT:-600}"
|
||||
local STALE
|
||||
STALE="${NVM_INSTALL_LOCK_STALE:-0}"
|
||||
|
||||
local WAITED
|
||||
WAITED=0
|
||||
local ANNOUNCED
|
||||
ANNOUNCED=0
|
||||
while ! command mkdir "${LOCK}" 2>/dev/null; do
|
||||
# Steal a lock left behind by a crashed install once it is old enough.
|
||||
if [ "${STALE}" != '0' ] && [ -n "$(command find "${LOCK}" -maxdepth 0 -type d -mmin "+${STALE}" 2>/dev/null)" ]; then
|
||||
nvm_err "Removing stale install lock for ${VERSION} (older than ${STALE} minute(s))"
|
||||
command rm -rf "${LOCK}" 2>/dev/null
|
||||
continue
|
||||
fi
|
||||
if [ "${WAITED}" -ge "${TIMEOUT}" ]; then
|
||||
nvm_err "Timed out after ${TIMEOUT}s waiting for another install of ${VERSION} to finish."
|
||||
nvm_err "If no other install is running, remove ${LOCK} and try again."
|
||||
return 1
|
||||
fi
|
||||
if [ "${ANNOUNCED}" -eq 0 ]; then
|
||||
nvm_err "Waiting for another install of ${VERSION} to finish..."
|
||||
ANNOUNCED=1
|
||||
fi
|
||||
command sleep 1
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
|
||||
NVM_INSTALL_LOCK="${LOCK}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Release the lock acquired by nvm_acquire_install_lock, if any.
|
||||
nvm_release_install_lock() {
|
||||
if [ -n "${NVM_INSTALL_LOCK-}" ]; then
|
||||
command rmdir "${NVM_INSTALL_LOCK}" 2>/dev/null || command rm -rf "${NVM_INSTALL_LOCK}" 2>/dev/null || true
|
||||
unset NVM_INSTALL_LOCK
|
||||
fi
|
||||
}
|
||||
|
||||
# args: pattern
|
||||
# Lists versions available in the local cache (not yet installed).
|
||||
# Returns version numbers like "v18.20.4", one per line, sorted.
|
||||
@@ -3933,6 +4007,11 @@ nvm() {
|
||||
fi
|
||||
EXIT_CODE=0
|
||||
else
|
||||
# Serialize concurrent installs of this version so two runs cannot race
|
||||
# on its version directory (one replacing it while the other reads it).
|
||||
if ! nvm_acquire_install_lock "${VERSION}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "_${NVM_OS}" = "_freebsd" ]; then
|
||||
# node.js and io.js do not have a FreeBSD binary
|
||||
@@ -3975,6 +4054,8 @@ nvm() {
|
||||
EXIT_CODE=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
nvm_release_install_lock
|
||||
fi
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ] && ! nvm_validate_install "${VERSION}"; then
|
||||
@@ -4853,6 +4934,7 @@ nvm() {
|
||||
nvm_die_on_prefix nvm_get_make_jobs nvm_get_minor_version \
|
||||
nvm_has_solaris_binary nvm_is_merged_node_version \
|
||||
nvm_is_natural_num nvm_is_version_installed nvm_validate_install \
|
||||
nvm_install_lock_name nvm_acquire_install_lock nvm_release_install_lock \
|
||||
nvm_list_aliases nvm_make_alias nvm_print_alias_path \
|
||||
nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \
|
||||
nvm_sanitize_path nvm_has_colors nvm_process_parameters \
|
||||
@@ -4866,7 +4948,7 @@ nvm() {
|
||||
nvm_write_nvmrc \
|
||||
>/dev/null 2>&1
|
||||
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
|
||||
NVM_CD_FLAGS NVM_BIN NVM_INC NVM_MAKE_JOBS \
|
||||
NVM_CD_FLAGS NVM_BIN NVM_INC NVM_MAKE_JOBS NVM_INSTALL_LOCK \
|
||||
NVM_COLORS INSTALLED_COLOR SYSTEM_COLOR \
|
||||
CURRENT_COLOR NOT_INSTALLED_COLOR DEFAULT_COLOR LTS_COLOR \
|
||||
>/dev/null 2>&1
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
|
||||
cleanup() {
|
||||
[ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}"
|
||||
unset -f die cleanup
|
||||
unset NVM_DIR NVM_INSTALL_LOCK NVM_INSTALL_LOCK_TIMEOUT NVM_INSTALL_LOCK_STALE \
|
||||
version lock OUTPUT EXIT_CODE
|
||||
}
|
||||
|
||||
die() { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
: nvm.sh
|
||||
\. ../../../nvm.sh
|
||||
|
||||
type nvm_acquire_install_lock > /dev/null 2>&1 || die 'nvm_acquire_install_lock is not available'
|
||||
|
||||
NVM_DIR="$(mktemp -d)"
|
||||
[ -n "${NVM_DIR}" ] || die 'unable to create temp NVM_DIR'
|
||||
version='v20.0.0'
|
||||
lock="$(nvm_cache_dir)/locks/$(nvm_install_lock_name "${version}")"
|
||||
|
||||
# An empty version is a no-op that succeeds and takes no lock.
|
||||
unset NVM_INSTALL_LOCK
|
||||
nvm_acquire_install_lock '' || die 'acquiring with an empty version should succeed as a no-op'
|
||||
[ -z "${NVM_INSTALL_LOCK-}" ] || die 'an empty-version acquire should not record a lock'
|
||||
|
||||
# Acquiring a free lock succeeds, creates the lock dir, and records its path.
|
||||
nvm_acquire_install_lock "${version}" || die 'acquiring a free lock should succeed'
|
||||
[ -d "${lock}" ] || die 'acquire should create the lock directory'
|
||||
[ "${NVM_INSTALL_LOCK}" = "${lock}" ] || die 'acquire should record NVM_INSTALL_LOCK'
|
||||
|
||||
# Release it so the following contention checks start from a held-by-someone-else state.
|
||||
command rmdir "${lock}" 2>/dev/null
|
||||
unset NVM_INSTALL_LOCK
|
||||
|
||||
# A lock held by "another process", with no wait budget, fails and names the path.
|
||||
command mkdir -p "${lock}"
|
||||
OUTPUT="$(NVM_INSTALL_LOCK_TIMEOUT=0 nvm_acquire_install_lock "${version}" 2>&1)"; EXIT_CODE=$?
|
||||
[ "${EXIT_CODE}" != "0" ] || die 'acquire should fail when the lock is held and the timeout is 0'
|
||||
case "${OUTPUT}" in
|
||||
*"${lock}"*) ;;
|
||||
*) die "the timeout message should name the lock path; got >${OUTPUT}<" ;;
|
||||
esac
|
||||
[ -d "${lock}" ] || die 'a failed acquire must not remove the held lock'
|
||||
|
||||
# By default, a held lock is never stolen no matter how old it is.
|
||||
touch -t 202001010000 "${lock}" 2>/dev/null || die 'unable to age the lock dir'
|
||||
NVM_INSTALL_LOCK_TIMEOUT=0 nvm_acquire_install_lock "${version}" 2>/dev/null && die 'an old lock must not be stolen unless NVM_INSTALL_LOCK_STALE is set'
|
||||
[ -d "${lock}" ] || die 'the old lock should still be held'
|
||||
|
||||
# With NVM_INSTALL_LOCK_STALE set, a sufficiently old lock is stolen and re-acquired.
|
||||
unset NVM_INSTALL_LOCK
|
||||
NVM_INSTALL_LOCK_STALE=1 NVM_INSTALL_LOCK_TIMEOUT=0 nvm_acquire_install_lock "${version}" || die 'a stale lock should be stolen when NVM_INSTALL_LOCK_STALE is set'
|
||||
[ "${NVM_INSTALL_LOCK}" = "${lock}" ] || die 'stealing a stale lock should acquire it'
|
||||
|
||||
cleanup
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
cleanup() {
|
||||
[ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}"
|
||||
unset -f die cleanup
|
||||
unset NVM_DIR
|
||||
}
|
||||
|
||||
die() { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
: nvm.sh
|
||||
\. ../../../nvm.sh
|
||||
|
||||
type nvm_install_lock_name > /dev/null 2>&1 || die 'nvm_install_lock_name is not available'
|
||||
|
||||
NVM_DIR="$(mktemp -d)"
|
||||
[ -n "${NVM_DIR}" ] || die 'unable to create temp NVM_DIR'
|
||||
|
||||
# A normal version passes through unchanged.
|
||||
[ "$(nvm_install_lock_name 'v20.0.0')" = 'v20.0.0' ] || die "v20.0.0 => >$(nvm_install_lock_name 'v20.0.0')<"
|
||||
|
||||
# Dots, hyphens, plus, and underscores are all preserved.
|
||||
[ "$(nvm_install_lock_name 'iojs-v1.0.0')" = 'iojs-v1.0.0' ] || die "iojs-v1.0.0 => >$(nvm_install_lock_name 'iojs-v1.0.0')<"
|
||||
|
||||
# Path separators and other unsafe characters become underscores.
|
||||
[ "$(nvm_install_lock_name 'lts/*')" = 'lts__' ] || die "lts/* => >$(nvm_install_lock_name 'lts/*')<"
|
||||
[ "$(nvm_install_lock_name 'a b/c')" = 'a_b_c' ] || die "a b/c => >$(nvm_install_lock_name 'a b/c')<"
|
||||
|
||||
# The empty string maps to the empty string (no trailing-newline artifact).
|
||||
[ "$(nvm_install_lock_name '')" = '' ] || die "empty => >$(nvm_install_lock_name '')<"
|
||||
|
||||
cleanup
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
cleanup() {
|
||||
[ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}"
|
||||
unset -f die cleanup
|
||||
unset NVM_DIR NVM_INSTALL_LOCK version lock
|
||||
}
|
||||
|
||||
die() { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
: nvm.sh
|
||||
\. ../../../nvm.sh
|
||||
|
||||
type nvm_release_install_lock > /dev/null 2>&1 || die 'nvm_release_install_lock is not available'
|
||||
|
||||
NVM_DIR="$(mktemp -d)"
|
||||
[ -n "${NVM_DIR}" ] || die 'unable to create temp NVM_DIR'
|
||||
version='v20.0.0'
|
||||
lock="$(nvm_cache_dir)/locks/$(nvm_install_lock_name "${version}")"
|
||||
|
||||
# Releasing when nothing is held is a no-op that succeeds.
|
||||
unset NVM_INSTALL_LOCK
|
||||
nvm_release_install_lock || die 'releasing with no lock held should succeed'
|
||||
|
||||
# After acquiring, releasing removes the lock directory and clears the marker.
|
||||
nvm_acquire_install_lock "${version}" || die 'setup: acquire failed'
|
||||
[ -d "${lock}" ] || die 'setup: lock dir should exist after acquire'
|
||||
nvm_release_install_lock || die 'release should succeed'
|
||||
[ ! -d "${lock}" ] || die 'release should remove the lock directory'
|
||||
[ -z "${NVM_INSTALL_LOCK-}" ] || die 'release should clear NVM_INSTALL_LOCK'
|
||||
|
||||
# Releasing again is a harmless no-op.
|
||||
nvm_release_install_lock || die 'a second release should be a no-op'
|
||||
|
||||
cleanup
|
||||
Reference in New Issue
Block a user