Repository navigation
Releases: PurdueRCAC/uv-manager
Release list
uv-manager 0.6.1
Fixed
A rank robbed of its fresh provisioning lock now retakes it instead of dying.
uvm_acquire_lock wins mkdir, then records ownership with a write that 0.6.0 made fatal on any
failure. That fatality is correct for a filesystem fault — a holder that cannot prove ownership leaks
its lock for a full stale window — but the redirect has two failure modes and 0.6.0 treated them
alike. A rank whose fresh lock directory was removed by a losing stale-breaker hit the same die and
exited 1 with empty stdout, so VER=$(uv --version) on a cold node came back empty and true.
The owner write now sits inside the acquire loop and the lock directory decides what happened. A
directory still standing is a genuine fault and still dies, carrying the errno the shell's own
diagnostic names. A directory already gone means this rank was robbed, and it retakes the lock under a
literal bound of three that never resets, adding no time to the existing UVM_LOCK_TIMEOUT accounting
and printing nothing on a successful retake.
Measured against 0.6.0 on an identical harness — a planted dead-holder lock, 64 ranks per round:
| ranks | failures | |
|---|---|---|
| 0.6.0 | 768 | 1 |
| 0.6.1 | 5760 | 0 |
Branch instrumentation over 1920 of those ranks caught three real robberies, all three retaken, none
routed to the fatal branch. The absent, waited and broke counters that bound the surrounding
wait loop are byte-identical to 0.6.0 on message, exit code, note count and timing.
Known residual. The retake decides between a lost race and a filesystem fault by testing whether
the lock directory is still standing, because a failed redirect leaves the shell no errno a branch can
read. A rank winning mkdir in the gap between the failed write and that test would still be
misclassified. The window is a single fork-free builtin, and 5760 ranks of the regime that produces
robberies found none of it, so this is narrower than what 0.6.0 ships rather than new.
Upgrading
No configuration change. No new environment variable, no new subcommand, and the provisioning
discipline is still mkdir. Sites deploying by git clone of a tag should re-clone 0.6.1; sites
tracking a moving main pick it up on their next pull. The Lmod modulefile derives its prefix from
myModuleVersion() and needs no edit, though a site that deploys per-version module files still has
to add one.
Also in this tag
34 [harness] commits to .agents/, the spec-driven factory this repository is developed with. They
change nothing a site deploys. The full design record for the fix is retained under
spec/lock-acquire-retake/.
uv-manager 0.6.0
Hardening for the provisioning lock, which coordinates uv installation across concurrent
invocations on a shared filesystem. One defect in it was reachable at shipped defaults on ordinary
many-rank job launches; the rest needed a long hold or a misconfiguration.
Eighteen commits since 0.5.0, but only one changes anything a site deploys. The other seventeen are
the .agents/ factory and its records.
Fixes
A released lock is no longer misdiagnosed as a broken filesystem. uvm_acquire_lock decided why
mkdir failed by looking at the filesystem again afterwards. That inference is unsound — the lock is
gone either because the mount is unwritable or because a holder released in the interim — and it
killed ranks with check permissions and quota on a healthy filesystem. Contention is now told from
failure by persistence: the waiter retries and reports the fault only after a bounded number of
attempts have each found the lock absent. Measured in 64-way concurrent cold bursts: 0 of 1280
ranks failing, against 21 of 1280 on 0.5.0. This is the one that affects everybody.
A lock is released only by the process that holds it. Release matched on the path, so a holder
whose lock had been broken as stale removed the breaker's lock instead — leaving two processes
provisioning one tree with no mutual exclusion between them. uvm_acquire_lock now records a
host/pid/nonce line in the lock's owner file and removes the directory only while that line is
still its own.
A live holder's lock is no longer broken as stale. A detached refresher rewrites the owner file
every UVM_LOCK_STALE/10 seconds, so a hold longer than the threshold survives. Age is read from the
owner file rather than the directory, whose mtime does not track writes to files inside it.
No lock survives an exec. exec replaces the process image and the EXIT trap never runs. All
four exec sites now release first.
A stale lock that cannot be removed no longer defeats UVM_LOCK_TIMEOUT. A denied break used to
retry immediately, spinning without bound and re-announcing itself every iteration.
The timeout message names the holder and a recovery command that works. It prints the recorded
owner line, states that the pid belongs to the host recorded beside it rather than the reader's,
and advises rm -f '<lock>/owner' && rmdir '<lock>'. The old advice was a bare rmdir, which
reports Directory not empty for every lock whose holder got as far as claiming it.
Breaking change for a misconfigured site
UVM_LOCK_TIMEOUT must now be less than UVM_LOCK_STALE, and the inversion is refused rather than
acted on. A waiter that outlives the stale threshold breaks the lock it is waiting for. A site
running an inverted pair today will get a hard failure at provisioning time with a message naming
both variables and the seconds it judged.
Both knobs are now read as decimal seconds. UVM_LOCK_STALE=0600 previously meant 384 seconds,
because bash reads a leading zero as octal; it now means 600. Check any zero-padded values in your
site configuration.
Known issue
A rank that wins the lock and has its directory removed by a losing stale-breaker exits 1 with empty
stdout. It needs a pre-existing abandoned lock plus concurrent contention; measured at 0 of 2304
ranks on the ordinary cold-start path, and 3–4 of 1280 against a deliberately planted abandoned
lock. 0.5.0 does not fail this way, but loses far more ranks to the defect fixed above. The remedy is
scoped and queued as the next cycle.
Upgrading
Sites tracking a moving main already have this. Sites deploying a pinned tag need to clone 0.6.0
into a new prefix. The Lmod modulefile derives its prefix from myModuleVersion() and needs no edit;
a site that deploys per-version modulefiles has to add one.
git clone and rsync -a preserve the bin/ symlinks that make uv, uvx, uvm and
uv-manager one script. See README.md § For administrators: deployment.
🤖 Generated with Claude Code
uv-manager 0.5.0
uv-manager doctor detects the damage a purge actually causes
Doctor's file-level walk was driven by the glob *.dist-info/RECORD. RECORD is read only on
uninstall — the coldest file in a distribution, and so an early casualty of an atime-ordered scratch
purge — which meant a distribution whose manifest was taken first did not match the glob and was
invisible rather than suspicious. Gutting an environment and removing RECORD with it produced
OK no damage detected and exit 0.
The walk now runs from the dist-info directory. A missing manifest is a finding in its own right,
and a tool directory that has lost pyvenv.cfg is too — reported with the reason no automated repair
is safe for that class, since uv falls back to the base interpreter and writes outside the tree the
wrapper owns.
Behavior change for automation
A tool directory missing uv-receipt.toml now exits 0. It prints an advisory instead of a WARN
that set the exit status. Such a tool works perfectly well when invoked, so the old behavior held
doctor at exit 1 permanently over a working tool, and the only route back to 0 was to delete it.
Failures set the status; advice does not. A job prologue keyed on uv-manager doctor can rely on that
distinction now.
If you have automation that treats any non-zero doctor status as "rebuild this tree", it will fire
less often. If you have automation that depends on a receipt-less tool failing the check, it will stop
firing.
Remedies that repair
The printed remediation block named three commands, two of which did not repair a damaged tree and one
of which was destructive: uv-manager install with no argument re-resolves latest from the network
and repoints current, silently overriding a site's UVM_PIN. It is gone. The block now names
uv tool upgrade --all --reinstall --no-cache and uv python install --reinstall, and explains why
--no-cache carries the repair — uv runs no integrity check on its unpacked archive-v0 store, so a
rebuild that reuses the cache reinstalls the same damage.
The block is emitted through a heredoc, so uv-manager doctor | head no longer prints
printf: write error: Broken pipe.
Faster
The walk no longer forks per file. On a 42-tool tree: 198 subprocesses down to 42, and roughly 2.7×
faster wall-clock. README.md proposes doctor for job prologues, where that is real.
Documented detection floor
README.md § Scratch purge now states what doctor cannot see, rather than reading as a complete
list. A distribution deleted entirely leaves no ground truth, because uv-receipt.toml records the
top-level request — jupyterlab — not the 91 distributions it resolves to. Managed interpreters carry
no manifest at all, so the check there is whether import json, os, ssl still succeeds, which it does
with email/, xml/ and unittest/ gone. A clean report means nothing detectable is wrong, not that
the tree is whole.
Deployment
No configuration change and no modulefile change: the Lmod example derives its prefix from
myModuleVersion(), so it is version-agnostic. A site that deploys per-version module files still
needs to add one for 0.5.0.
Deploy by cloning the tag — git clone and rsync -a preserve the bin/ symlinks that make uv,
uvx and uvm one script. See README.md § For administrators: deployment for the cp caveat.
Sites tracking a moving main pick this up on their next pull; sites pinned to a tag need to be told
to re-clone 0.5.0.
Verification
Three adversarial review cycles with executed evidence; no criterion was taken on trust. Every repair
idiom in the new remediation text was measured against real uv 0.12.4 rather than assumed. The
rewritten RECORD walk was proven to reach a byte-identical verdict to 0.4.1 on a 42-tool fixture,
including pathological manifests (no trailing newline, CRLF, quoted embedded commas, and paths
containing $, *, spaces and backslashes). Detection was confirmed read-only: a 331-path manifest of
the state tree is byte-identical across a run, and no lock is taken.
Full record: spec/doctor-detection-gaps/.
Also in this release
Twelve [harness] commits to .agents/, the spec-driven factory this repository develops itself with.
Nothing a site deploys or runs.
uv-manager 0.4.1
What changed for sites
uv, uvx and uvm stop doing metadata work on every invocation. uvm_export_env issued a
six-directory mkdir -p inside a umask 077 subshell on every call, defended in a comment as "a
handful of metadata operations". Measured, it is a fork, an exec, and — under GNU coreutils — one
EEXIST-failing mkdir(2) per path component of every operand, to resolve six directories that
already exist. The count grows with the depth of UVM_ROOT, so a deep scratch path costs more, not
less.
It now runs behind a six-way [[ -d ]] guard, placed outside the subshell. Six shell builtin tests
cost 15 µs and spawn nothing.
Measured against real uv 0.12.4: wrapper overhead above the exec'd binary falls from about 6.9 ms
to about 5.15 ms, and two of the three warm-path process creations are gone. Every measurement is
macOS on APFS — the cluster figure is unmeasured, and the part that transfers is the syscall
reduction rather than the milliseconds. A failing mkdir RPC cannot be served from a parallel
filesystem's client attribute cache, where a stat often can, which is why this matters more at rank
scale than the local numbers suggest.
Behavior is unchanged. A missing state directory is still created, under umask 077. Modes are
still not repaired — mkdir -p never chmodded an existing directory either. Parity with 0.4.0 was
driven across about fifteen state configurations, a 40-way concurrency race, three levels of wrapper
re-entrancy, and the tool/python non-exec path.
Documentation
- The
README.mddesign note records this as a rejection reversed by measurement, rather than
dropping the entry. - Documented wrapper overhead corrected from roughly 7 ms to roughly 5 ms.
- The
cp -rsymlink claim in the deployment notes corrected: GNU coreutils preserves symlinks under
-r,-Rand-aalike, while macOS and the BSDs dereference under lowercase-r. Use
git cloneorrsync -a.
Internal
Twenty-one [harness] commits to .agents/, the spec-driven factory this project is developed with.
Nothing a site deploys.
Upgrading
No action beyond the deploy. No new environment variable, no new subcommand, no configuration change.
The Lmod modulefile derives its prefix from myModuleVersion() and needs no edit; a site that
deploys per-version modulefiles has to add one for 0.4.1.
Sites tracking a moving main pick this up automatically. Sites pinned to a tag must re-clone:
git clone --branch 0.4.1 https://github.com/PurdueRCAC/uv-manager /apps/external/uv/0.4.1
git clone and rsync -a preserve the bin/ symlinks, which are what make the four entry points one
script.
The design record for this change is retained at spec/purge-resilient-run/.
uv-manager 0.4.0
Fixed
Trampolines now honor UVM_PLATFORM (#5).
A site can set UVM_PLATFORM to split its state tree more finely than uname -m does — glibc skew
between login and compute images, musl, x86-64 microarchitecture levels. The wrapper honored that
override; the trampolines it generates did not, because their /bin/sh body hardcoded
a=$(uname -m). The two disagreed on every invocation: uv tool install ruff wrote the executable
under the override key, the trampoline on PATH looked under uname -m, and every tool a user
installed exited 127 naming an architecture the operator never configured — for a binary one
directory away.
The generated body now resolves ${UVM_PLATFORM:-$(uname -m)}, the same expression uvm_init uses,
evaluated on the executing node from the invoker's environment. The trampoline directory stays
architecture-neutral, so one PATH entry set on a login node is still correct on a compute node of
another type.
Who is affected. Only sites that set UVM_PLATFORM. Under the default uname -m the two
expressions always agreed, so nothing changes for you.
Upgrading. Trampolines generated by an earlier version carry the old body until they are
rewritten. Any uv tool or uv python command resyncs them; uv-manager trampolines does it
explicitly.
Pre-existing since the trampoline mechanism was introduced.
Documentation
- Every comment and user-facing document audited for prose drift.
- The
is not installed for architecturetroubleshooting entry now says the quoted name is the
platform key, which a site may have overridden. etc/uv-manager.conf.examplewarns thatUVM_PLATFORMmust be computed on the executing node: a
login-node value inherited as a literal throughsbatch --export=ALLsends the wrapper and every
trampoline into another node's tree.- Reworded the
uvm_candidatesexplanation (#4) and corrected a README example snippet.
Internal
Twenty-one [harness] commits to .agents/, the spec-driven factory this repository is developed
with. Nothing a site deploys.
Deploying
Sites that clone a tag into /apps/external/uv/<version> need to clone 0.4.0 and repoint the
modulefile; nothing picks this up on its own. Sites tracking main already have it. Deploy with
git clone or rsync -a, so the bin/{uv,uvx,uvm} symlinks that make the four entry points one
script survive the copy.
The Lmod modulefile derives its prefix from myModuleVersion() and needs no edit.
uv-manager 0.3.0
First tagged release. Versions 0.1.0 and 0.2.0 existed in the script but were never tagged, so this
tag also establishes the convention: a bare X.Y.Z matching the string uv-manager --version prints.
Breaking
The wrapper's six configuration variables are renamed from UV_MANAGER_* to UVM_* (#2).
| was | now |
|---|---|
UV_MANAGER_ROOT |
UVM_ROOT |
UV_MANAGER_PIN |
UVM_PIN |
UV_MANAGER_PLATFORM |
UVM_PLATFORM |
UV_MANAGER_INSTALL_URL |
UVM_INSTALL_URL |
UV_MANAGER_LOCK_TIMEOUT |
UVM_LOCK_TIMEOUT |
UV_MANAGER_LOCK_STALE |
UVM_LOCK_STALE |
Same meanings, same defaults, same precedence. UV_MANAGER_* sat inside uv's own namespace, where a
site operator could not tell an Astral variable from one this wrapper invented — and uv already
ships a UV_LOCK_TIMEOUT of its own, so the collision was not hypothetical.
There is no compatibility shim. A legacy name is now simply unset. If you have UV_MANAGER_ROOT
in a modulefile, a job script, or a Globus Compute endpoint configuration, rename it. The failure mode
if you do not is loud rather than silent: the wrapper exits non-zero with cannot determine where to keep per-user uv state and lists every candidate it tried.
The five variables the wrapper exports for uv — UV_CACHE_DIR, UV_TOOL_DIR, UV_TOOL_BIN_DIR,
UV_PYTHON_INSTALL_DIR, UV_PYTHON_BIN_DIR — and the PATH prepends are unchanged. Those are uv's
namespace by right. The split now makes the project's central boundary visible in the names:
storage is ours, resolution is uv's.
Upgrading from 0.2.0
- Rename the six variables wherever your site sets them. The modulefile is the usual place; see
share/modulefiles/uv/main.lua, which now setsUVM_ROOT. - Nothing in the state tree changes. Existing
$UVM_ROOT/<arch>/trees, caches, tool venvs and
managed interpreters are picked up as-is — this is a rename of the inputs, not of the layout. - The Lmod modulefile derives its prefix from
myModuleVersion()and is version-agnostic. A site
deploying per-version module files still needs to add one for 0.3.0 to pin to this version.
Also in this release
Everything below predates this tag and is included because nothing before it was ever tagged.
[fix]Six correctness defects, and removal of the dead surface around them (5bb652c)[fix]Broken-pipe noise silenced instatus; symlink note corrected (f902ebf) —status
output no longer reportswrite error: Broken pipewhen piped intohead[refactor]uv-manageris the canonical script, with auvmalias (88c683b) — the four
entry pointsuv,uvx,uvmanduv-managerare one script, dispatched onargv[0][docs]Documentation follows the removals; two false claims corrected (f564cb2)[harness]A spec-driven software factory under.agents/(33a91fb) — development tooling
only; nothing a site deploys depends on it
Verification
No test suite exists yet. This release was gated by executed commands, not review alone: shellcheck
and bash -n under bash 3.2.57 (the portability floor), offline end-to-end provisioning on two
architectures with the current symlink asserted relative, uvm --version matching the tag exactly,
and uvm help rendering on a node with no state root configured — the property that makes an
unconfigured node recoverable.
Deploying
Sites install by cloning into /apps/external/uv/<version>. Use git clone or rsync -a; a cp -r
without -a will flatten bin/{uv,uvx,uvm} from symlinks into copies, which still works but stops
being one script.
git clone --depth 1 --branch 0.3.0 https://github.com/PurdueRCAC/uv-manager.git
A site tracking main picks this up on its next pull. A site pinned to a tag must be told to move.