@@ -2,51 +2,221 @@ name: downloader-race-repro
22
33on :
44 workflow_dispatch :
5+ inputs :
6+ release_tag :
7+ description : " utoo release tag to test"
8+ required : false
9+ default : " utoo-v1.1.4"
10+ attempts :
11+ description : " stress attempts"
12+ required : false
13+ default : " 40"
14+ parallelism :
15+ description : " parallel utoo install processes"
16+ required : false
17+ default : " 24"
518 pull_request :
619 paths :
720 - " .github/workflows/downloader-race-repro.yml"
8- - " crates/pm/src/util/downloader.rs"
921
1022permissions :
1123 contents : read
1224
1325jobs :
14- linux-repro :
15- name : linux downloader race repro
26+ linux-release- repro :
27+ name : linux release cache race repro
1628 runs-on : ubuntu-latest
17- timeout-minutes : 45
29+ timeout-minutes : 60
30+ env :
31+ UTOO_RELEASE_TAG : " utoo-v1.1.4"
32+ ATTEMPTS : " 40"
33+ PARALLELISM : " 24"
34+ REGISTRY : " https://registry.npmjs.org"
35+ CACHE_DIR : ${{ runner.temp }}/utoo-npm-cache
36+ LOG_DIR : ${{ runner.temp }}/utoo-repro-logs
1837 steps :
19- - uses : actions/checkout@v4
38+ - name : Apply manual inputs
39+ if : ${{ github.event_name == 'workflow_dispatch' }}
40+ shell : bash
41+ run : |
42+ set -euo pipefail
2043
21- - name : Install Rust
22- uses : dtolnay/rust-toolchain@stable
23- with :
24- toolchain : nightly-2026-01-04
44+ echo "UTOO_RELEASE_TAG=${{ github.event.inputs.release_tag }}" >> "$GITHUB_ENV"
45+ echo "ATTEMPTS=${{ github.event.inputs.attempts }}" >> "$GITHUB_ENV"
46+ echo "PARALLELISM=${{ github.event.inputs.parallelism }}" >> "$GITHUB_ENV"
2547
26- - name : Cache cargo
27- uses : Swatinem/rust-cache@v2
28- with :
29- shared-key : downloader-race-repro-linux
48+ - name : Download utoo release
49+ shell : bash
50+ env :
51+ GH_TOKEN : ${{ github.token }}
52+ run : |
53+ set -euo pipefail
54+
55+ mkdir -p "$RUNNER_TEMP/utoo-release" "$LOG_DIR"
56+ gh release download "$UTOO_RELEASE_TAG" \
57+ --repo "$GITHUB_REPOSITORY" \
58+ --pattern "utoo-linux-x64.tar.gz" \
59+ --dir "$RUNNER_TEMP/utoo-release"
60+
61+ tar -xzf "$RUNNER_TEMP/utoo-release/utoo-linux-x64.tar.gz" -C "$RUNNER_TEMP/utoo-release"
62+ chmod +x "$RUNNER_TEMP/utoo-release/utoo"
63+ echo "$RUNNER_TEMP/utoo-release" >> "$GITHUB_PATH"
64+ "$RUNNER_TEMP/utoo-release/utoo" --version
3065
31- - name : Try to reproduce zero-length resolved cache
66+ - name : Create repro project template
67+ shell : bash
68+ run : |
69+ set -euo pipefail
70+
71+ mkdir -p "$RUNNER_TEMP/utoo-repro-template"
72+ cat > "$RUNNER_TEMP/utoo-repro-template/package.json" <<'JSON'
73+ {
74+ "name": "utoo-cache-race-repro",
75+ "version": "0.0.0",
76+ "private": true,
77+ "dependencies": {
78+ "@babel/plugin-transform-react-jsx-source": "7.29.7",
79+ "cookie-es": "3.1.1",
80+ "d3-time-format": "4.1.0",
81+ "date-fns": "4.4.0",
82+ "recharts-scale": "0.4.5",
83+ "tapable": "2.3.3"
84+ }
85+ }
86+ JSON
87+
88+ - name : Stress shared cache with release utoo
3289 shell : bash
3390 run : |
3491 set -uo pipefail
3592
36- test_name='downloader::tests::repro_download_can_leave_zero_file_if_racing_process_dies_after_resolved'
37- for attempt in $(seq 1 30); do
38- echo "::group::attempt ${attempt}"
39- cargo test -p utoo-pm "${test_name}" -- --ignored --nocapture
40- status=$?
93+ UTOO_BIN="$(command -v utoo)"
94+ ZERO_LIST="$RUNNER_TEMP/zero-package-json.txt"
95+ : > "$ZERO_LIST"
96+
97+ scan_zero_package_json() {
98+ find "$CACHE_DIR" -type f -path '*/package/package.json' -size 0c -print 2>/dev/null | sort
99+ }
100+
101+ dump_cache_summary() {
102+ local attempt="$1"
103+ local mode="$2"
104+ {
105+ echo "attempt=$attempt mode=$mode"
106+ echo "cache_dir=$CACHE_DIR"
107+ find "$CACHE_DIR" -maxdepth 4 -type f \( -name package.json -o -name _resolved \) \
108+ -printf '%s %p\n' 2>/dev/null | sort || true
109+ } > "$LOG_DIR/cache-summary-${mode}-${attempt}.txt"
110+ }
111+
112+ start_installers() {
113+ local attempt="$1"
114+ local mode="$2"
115+ local workdir="$RUNNER_TEMP/repro-${mode}-${attempt}"
116+ mkdir -p "$workdir"
117+
118+ PIDS=()
119+ for idx in $(seq 1 "$PARALLELISM"); do
120+ local project="$workdir/project-$idx"
121+ mkdir -p "$project"
122+ cp "$RUNNER_TEMP/utoo-repro-template/package.json" "$project/package.json"
123+ (
124+ cd "$project"
125+ UTOO_CACHE_DIR="$CACHE_DIR" UTOO_REGISTRY="$REGISTRY" \
126+ timeout --kill-after=5s 180s "$UTOO_BIN" \
127+ --cache-dir "$CACHE_DIR" \
128+ --registry "$REGISTRY" \
129+ install --ignore-scripts \
130+ > "$LOG_DIR/install-${mode}-${attempt}-${idx}.log" 2>&1
131+ ) &
132+ PIDS+=("$!")
133+ done
134+ }
135+
136+ wait_installers() {
137+ local status=0
138+ for pid in "${PIDS[@]}"; do
139+ if ! wait "$pid"; then
140+ status=1
141+ fi
142+ done
143+ return "$status"
144+ }
145+
146+ kill_after_resolved() {
147+ local deadline=$((SECONDS + 120))
148+ while [ "$SECONDS" -lt "$deadline" ]; do
149+ if find "$CACHE_DIR" -type f -name _resolved -print -quit 2>/dev/null | grep -q .; then
150+ sleep 0.02
151+ local killed=0
152+ for pid in "${PIDS[@]}"; do
153+ if kill -0 "$pid" 2>/dev/null; then
154+ kill -9 "$pid" 2>/dev/null || true
155+ killed=$((killed + 1))
156+ if [ "$killed" -ge $((PARALLELISM / 2)) ]; then
157+ break
158+ fi
159+ fi
160+ done
161+ echo "killed $killed installers after _resolved appeared"
162+ return 0
163+ fi
164+ sleep 0.01
165+ done
166+ echo "no _resolved observed before kill deadline"
167+ }
168+
169+ run_attempt() {
170+ local attempt="$1"
171+ local mode="$2"
172+
173+ rm -rf "$CACHE_DIR"
174+ mkdir -p "$CACHE_DIR"
175+
176+ echo "::group::attempt $attempt ($mode)"
177+ start_installers "$attempt" "$mode"
178+ if [ "$mode" = "kill-after-resolved" ]; then
179+ kill_after_resolved
180+ fi
181+
182+ wait_installers || true
183+ scan_zero_package_json | tee "$ZERO_LIST"
184+ dump_cache_summary "$attempt" "$mode"
41185 echo "::endgroup::"
42186
43- if [ "${status}" -eq 0 ]; then
44- echo "::notice::Reproduced zero-length package.json in resolved downloader cache on attempt ${attempt}."
187+ if [ -s "$ZERO_LIST" ]; then
188+ echo "::error::Reproduced zero-byte cache package.json with release $UTOO_RELEASE_TAG on attempt $attempt ($mode)."
189+ cat "$ZERO_LIST"
190+ return 0
191+ fi
192+
193+ return 1
194+ }
195+
196+ echo "utoo=$UTOO_BIN"
197+ echo "release=$UTOO_RELEASE_TAG attempts=$ATTEMPTS parallelism=$PARALLELISM registry=$REGISTRY"
198+
199+ for attempt in $(seq 1 "$ATTEMPTS"); do
200+ if run_attempt "$attempt" "passive"; then
201+ exit 0
202+ fi
203+
204+ if run_attempt "$attempt" "kill-after-resolved"; then
45205 exit 0
46206 fi
47207
48- echo "attempt ${ attempt} did not hit the zero-length window "
208+ echo "attempt $attempt did not leave a zero-byte package.json "
49209 done
50210
51- echo "::error::Did not reproduce zero-length package.json after 30 attempts."
211+ echo "::error::Did not reproduce a zero-byte cache package.json after $ATTEMPTS release-based attempts."
52212 exit 1
213+
214+ - name : Upload repro logs
215+ if : always()
216+ uses : actions/upload-artifact@v4
217+ with :
218+ name : utoo-release-cache-race-logs
219+ path : |
220+ ${{ runner.temp }}/utoo-repro-logs
221+ ${{ runner.temp }}/zero-package-json.txt
222+ if-no-files-found : ignore
0 commit comments