Fix env cache churn from defaulted exclude-newer and race in concurrent env creation - #138
Merged
Merged
Conversation
Two related fixes for environment caching: 1. compute_env_hash no longer includes a defaulted (non-user-set) exclude-newer. The default is the parse-time clock, so hashing it changed the env hash every second, giving every run (and every Function templated in a different second) a fresh ENV_DIR and a full rebuild. Only an exclude-newer actually pinned in the script header affects the hash now. 2. The shell template builds the environment in a unique temp dir (hostname + pid) and renames it into place, instead of creating it at the final path unguarded. Concurrent tasks sharing an env hash previously raced on creation and failed with 'directory already exists' / missing uv.toml / ModuleNotFoundError from half-installed envs; now the loser of the publish race discards its build and uses the winner's. uv venv gets --relocatable so entry-point shebangs survive the rename. An existing ENV_DIR now always means a complete environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- exclude-newer: default to None on UvMetadata instead of guarding the env hash with model_fields_set (which was defeated by extra="allow" underscore-typo keys and by CLI file rewrites baking the wall-clock default into user scripts). The effective default is now injected at command-templating time: _serialize_uv_toml() setdefaults the build-time timestamp into the uv.toml handed to uv, so fresh builds still resolve against a fixed cutoff while the volatile value never enters env identity or file rewrites. hog init calls _default_exclude_newer() directly so new scripts keep an explicit pin. - shell template: extend the EXIT trap to also remove $ENV_TMP and $ENV_TMP.uv.toml so failed builds under set -euo pipefail don't leak partial venvs. - shell template: probe `uv venv --help` for --relocatable and pass it via $UV_VENV_RELOCATABLE so endpoints with uv < 0.2.31 degrade gracefully instead of hard-failing. - shell template: suffix ENV_TMP with .$RANDOM to avoid collisions across PID namespaces (containerized tasks sharing hostname and scratch); drop the now-pointless pre-build rm -rf. - tests: replace the FakeDatetime hash test with parse-twice/None assertions; add coverage for the underscore-typo extra key, rewrite round-trips not injecting exclude-newer, and the injected/pinned exclude-newer in the uv.toml heredoc; add a trap-covers-ENV_TMP test; fix the create-branch slice in the publish-by-rename test to actually span venv-creation..publish (with unique-marker guards) instead of a vacuous window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Found 2026-09-01 while launching several concurrent tasks of one function: 5 of 6 concurrent
.local()tasks failed with a mix ofuv venv: "A directory already exists", "failed to open file ...uv.toml: No such file or directory", andModuleNotFoundErrorfrom a half-installed environment. Two underlying bugs:1. A defaulted
exclude-newerchurns the env hash every second. When a script's PEP 723 header has no[tool.uv] exclude-newer,Pep723Metadatadefaults it todatetime.now(timezone.utc)at parse time, andcompute_env_hashincludes the whole[tool.uv]table. So the env hash changes every second: every run (and every Function whose shell command is templated in a different second) gets a freshENV_DIRand a full uv rebuild, defeating the per-site environment cache entirely. Meanwhile N tasks of one function submitted together do share a hash — and since their env never pre-exists, they all take the create path at once.2. Environment creation isn't safe under concurrency. The shell template guards creation with a bare
[ -d "$ENV_DIR" ]check and builds directly at the final path, so concurrent same-hash tasks race: twouv venvcalls collide, the shared${ENV_DIR}.uv.tomlstaging file getsmv'd out from under the others, and a task can start running against an env another task is still installing into.Fix
compute_env_hashnow dropsexclude-newerfrom the hashed[tool.uv]table when the user didn't set it explicitly (checked via pydanticmodel_fields_set). Anexclude-newerpinned in the script header still affects the hash exactly as before, and the defaulted value is still written to the env'suv.toml, so fresh builds remain pinned to their build time.$ENV_DIR.tmp.$(hostname).$$) — including its stageduv.tomlandgroundhog-meta.json— then publishes it with an atomic rename. A task that loses the publish race discards its build and uses the winner's.uv venvgets--relocatableso entry-point script shebangs stay valid after the rename. Net effect: an existingENV_DIRalways means a complete environment.A task killed mid-build can still leave a stale
*.tmp.*dir behind, but stale temp dirs are inert (never reused, unlike the half-builtENV_DIRs the old code could leave and then happily reuse).Verification
exclude-newerhashes identically across parses at different times, user-pinnedexclude-newerstill changes the hash, and the template builds in the temp dir / publishes by rename / never touches$ENV_DIR/before publish.exclude-newer; the env hash is now stable across seconds, and 6 concurrent executions of the env-creation section against a cold cache all succeeded (previously 5/6 failed), leaving exactly one complete env dir — withuv.tomlandgroundhog-meta.jsoninside, and its python able to import the installed dependency.One adjacent issue observed but left alone: on hosts with no
uvon PATH, concurrent tasks also race in thepip install uvbootstrap block. That's pre-existing and much rarer in practice.🤖 Generated with Claude Code