Skip to content

[ci] Cache Android toolchain downloads + bump DownloadFile retries - #11618

Merged
jonathanpeppers merged 2 commits into
mainfrom
jonathanpeppers/android-sdk-download-caching
Jun 10, 2026
Merged

[ci] Cache Android toolchain downloads + bump DownloadFile retries#11618
jonathanpeppers merged 2 commits into
mainfrom
jonathanpeppers/android-sdk-download-caching

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Jun 10, 2026

Copy link
Copy Markdown
Member

Context

PR builds frequently fail on dl.google.com DNS / TCP errors during the initial download of the Android toolchain (commandlinetools, build-tools, emulator, system images, m2repository, docs, source, etc.) on dnceng-public agents. Example from a recent build:

error MSB3923: Failed to download file
"https://dl.google.com/android/repository/source-36_r01.zip".
nodename nor servname provided, or not known (dl.google.com:443)

This costs the entire ~4 hour pipeline run for one transient DNS hiccup.

Changes

1. Cache $(AndroidToolchainCacheDirectory) between runs

New build-tools/automation/yaml-templates/cache-android-archives.yaml template, modeled exactly on the existing cache-gradle.yaml. Wired into all three of build-{linux,macos,windows}-steps.yaml right next to the existing Gradle cache step.

Cache safety. Every download under $HOME/android-archives is verified after download by the corresponding MSBuild target (androidsdk / openjdk / binutils / aapt2 / bundletool). Most targets check the downloaded file's SHA-256 against a hash hardcoded in Configuration.props; openjdk instead verifies against Microsoft's published .sha256sum.txt. In all cases a corrupt or stale archive is deleted, the build is failed, and the next run re-downloads it fresh — so cache hits cannot mask a version bump.

Cache key: Configuration.props + each download *.targets file. Any version or hash change invalidates the cache. The restoreKeys fallback (Agent.OS only) still recovers most bytes when only a subset of components changes.

Cache size. ~2 GB physical / ~10.7 GB raw per OS, comfortably within Azure Pipelines' per-key limit.

2. Bump DownloadFile retries

Retries="3"Retries="5" and added RetryDelayMilliseconds="5000" on every DownloadFile invocation. This helps the first (cold-cache) and scheduled runs — which are responsible for warming the shared cache — better tolerate transient flakes on dl.google.com / aka.ms / github.com.

3. Delete corrupt bundletool.jar on SHA-256 mismatch

bundletool.targets previously errored on hash mismatch but left the corrupt jar in place. With the new pipeline cache, that bad jar could be persisted and repeatedly break future builds. Now matches the pattern used in androidsdk/binutils/aapt2/openjdk.

Verification — what we actually save

Measured against build #1456864 (first run with the cache, so all three OSes are a cache miss — they still downloaded everything; the cache save for the next build is what completed successfully):

Job Cache step (miss) What actually got downloaded Time the cache will save on hit
Linux > Build ✅ 5s — miss ~2 GB SDK fresh (00:37:0000:38:33) ~1.5 min per build
macOS > Build ✅ 54s — miss Nothing — agent already had every archive on disk ("Did not download file … SkipUnchangedFiles=true") ~0 (agents persist $HOME)
Windows > Build & Smoke Test ✅ 1s — miss Only JDK (~200 MB, a few seconds); SDK comes from agent image ~0

So the wall-clock savings are modest: ~1.5–2 min per Linux build on a cache hit, near-zero on macOS / Windows.

The bigger value of this PR is reliability, not throughput. dnceng-public Linux agents are ephemeral and download ~2 GB from dl.google.com on every build today. The previous motivating failure (build #1456555) lost a 4-hour run to a single DNS resolution error on one of those downloads. Cache hits avoid the request entirely; the bumped retries help the cold-cache and scheduled runs that have to repopulate the cache.

Builds frequently fail on `dl.google.com` DNS / TCP errors during the
initial download of the Android toolchain (commandlinetools, build-tools,
emulator, system images, m2repository, docs, source, etc.) on dnceng-public
agents. Example failure:

    error MSB3923: Failed to download file
    "https://dl.google.com/android/repository/source-36_r01.zip".
    nodename nor servname provided, or not known (dl.google.com:443)

Two changes here:

1. Add a Pipelines `Cache@2` step for `$(AndroidToolchainCacheDirectory)`
   (a.k.a. `$HOME/android-archives`), modeled exactly on the existing
   `cache-gradle.yaml` template. Wired into all three of
   build-{linux,macos,windows}-steps.yaml.

   Cache safety: every download under this directory is verified against
   a hardcoded SHA-256 by the corresponding MSBuild target
   (androidsdk/openjdk/binutils/aapt2/bundletool). A stale / corrupt
   cached entry is detected and re-downloaded - cache hits cannot mask
   a version bump.

   Cache key is `Configuration.props` + each download `*.targets` file,
   so any version or hash change invalidates the cache. The `restoreKeys`
   fallback (`Agent.OS` only) still recovers most bytes when only a
   subset of components changes.

2. Bump `Retries` from 3 to 5 and add `RetryDelayMilliseconds="5000"`
   on every `DownloadFile` call. This makes the first (cold-cache) and
   scheduled runs (which warm the shared cache) more resilient to
   transient `dl.google.com` / `aka.ms` / `github.com` flakes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 10, 2026 00:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves CI reliability for dotnet/android by reducing sensitivity to transient network/DNS failures when downloading the Android toolchain, and by reusing previously downloaded archives across ephemeral agents.

Changes:

  • Add an Azure Pipelines cache for $(AndroidToolchainCacheDirectory) (default ~/android-archives) and wire it into Linux/macOS/Windows build step templates.
  • Increase DownloadFile retries from 3 → 5 and add a 5s retry delay across toolchain download targets (Android SDK packages, aapt2/build-tools, OpenJDK, binutils, bundletool).
  • Introduce a cache key strategy based on Configuration.props and the download targets so cache invalidates on version/hash changes.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/openjdk/openjdk.targets Increase download retries and add retry delay for OpenJDK archive + hash file downloads.
src/bundletool/bundletool.targets Increase download retries and add retry delay for bundletool jar download.
src/binutils/binutils.targets Increase download retries and add retry delay for binutils archive download.
src/androidsdk/androidsdk.targets Increase download retries and add retry delay for Android SDK package downloads.
src/aapt2/aapt2.targets Increase download retries and add retry delay for build-tools (aapt2) downloads.
build-tools/automation/yaml-templates/cache-android-archives.yaml New template to cache Android toolchain archives directory between pipeline runs.
build-tools/automation/yaml-templates/build-windows-steps.yaml Wire android-archives cache template into Windows build steps.
build-tools/automation/yaml-templates/build-macos-steps.yaml Wire android-archives cache template into macOS build steps.
build-tools/automation/yaml-templates/build-linux-steps.yaml Wire android-archives cache template into Linux build steps.

Comment thread build-tools/automation/yaml-templates/cache-android-archives.yaml Outdated
Comment thread src/bundletool/bundletool.targets
* src/bundletool/bundletool.targets: delete the cached jar when the
  SHA-256 does not match `$(XABundleToolHash)`, matching the pattern
  used in androidsdk/binutils/aapt2/openjdk targets. Without this,
  a corrupt jar persisted in the new pipeline cache could repeatedly
  break future builds.
* cache-android-archives.yaml: reword the cache-safety comment to
  accurately describe verification - most targets compare against a
  hash hardcoded in Configuration.props, but openjdk verifies against
  a downloaded .sha256sum.txt from Microsoft.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 10, 2026
@jonathanpeppers
jonathanpeppers merged commit 8c22f01 into main Jun 10, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the jonathanpeppers/android-sdk-download-caching branch June 10, 2026 14:55
simonrozsival added a commit that referenced this pull request Jul 7, 2026
## Summary

This is another incremental hardening PR for the recurring `install OpenJDK and accept Android SDK licenses` CI failure where large Android SDK archives from `dl.google.com` fail with:

```text
MSB3923: Failed to download file "https://dl.google.com/android/repository/...".
The response ended prematurely. (ResponseEnded)
```

The specific recent failure already had the newer retry/backoff behavior: the log shows retries waiting 30s, 60s, 90s, and 120s before ultimately exhausting `x86_64-29_r08-darwin.zip`. So this PR does **not** replace the existing download retry work; it builds on it and covers the next failure mode.

## Prior work this builds on

- [#11348](#11348) moved OpenJDK installation into an MSBuild NoTargets project.
- [#11440](#11440) moved Android SDK/NDK component downloads into `src/androidsdk/androidsdk.targets`, which is what this setup step builds.
- [#11618](#11618) added the shared `android-archives` pipeline cache for build jobs and bumped the built-in `DownloadFile` retries.
- [#11647](#11647) added the shared `DownloadFileWithRetry` wrapper because `ResponseEnded` is a top-level `HttpIOException` that MSBuild's built-in `DownloadFile` retry logic does not catch.
- [#11693](#11693) made `DownloadOneFileWithRetry` skip cleanly on no-op builds using per-file stamps, which makes whole-step retries less wasteful once some archives already succeeded.
- [#11817](#11817) added escalating outer backoff between retry attempts, so transient CDN hiccups get several minutes to recover instead of burning through attempts immediately.

## Remaining gap

Those PRs made individual downloads much more resilient, but the failing path is the **test environment setup** template, not the main build template:

- `build-{linux,macos,windows}-steps.yaml` already uses `cache-android-archives.yaml`.
- `setup-test-environment-steps.yaml` did **not** use that cache before running `src/androidsdk/androidsdk.csproj`.
- The `install OpenJDK and accept Android SDK licenses` `run-dotnet-preview.yaml` invocation also had `retryCountOnTaskFailure: 0` through the template default.

That means a macOS test setup job could still cold-download several large archives concurrently and fail the entire job if one archive exhausted the per-file retry/backoff loop.

## Change

This PR hardens that remaining test-setup path by:

- adding a `condition` parameter to `cache-android-archives.yaml`, preserving the existing default behavior for build jobs
- invoking the Android archive cache from `setup-test-environment-steps.yaml` for macOS test setup before SDK downloads run
- setting `retryCountOnTaskFailure: 2` on the `install OpenJDK and accept Android SDK licenses` step

The cache is intentionally limited to macOS test setup for now. Linux archive caching already has a separate disk-pressure concern tracked by [#11837](#11837), so this PR avoids expanding Linux cache usage while still targeting the observed macOS `ResponseEnded` failure.

## Why step retry helps

The per-file retry wrapper is still the first line of defense. The task-level retry is a second line of defense for the case where a specific archive exhausts all per-file attempts.

On a second task attempt, successfully downloaded archives should be reused/skipped by the existing cache/stamp logic, so the retry mostly focuses on the archive that failed rather than restarting all work from scratch.

## Validation

- `git diff --check`
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants