Skip to content

Feat: Add caching/cached variants of pip and conda installers - #1409

Open
cjac wants to merge 11 commits into
GoogleCloudDataproc:mainfrom
LLC-Technologies-Collier:python-installer-caching-examples-202610
Open

cjac wants to merge 11 commits into
GoogleCloudDataproc:mainfrom
LLC-Technologies-Collier:python-installer-caching-examples-202610

Conversation

@cjac

@cjac cjac commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

This PR introduces cached variants of the standard Python package installation scripts: pip-install-cached.sh and conda-install-cached.sh. These scripts are designed to significantly reduce cluster initialization time and improve reliability in constrained environments (e.g., behind proxies or with limited egress bandwidth).

Resolves #1408

Problem Solved

Traditional package installation on every node startup can become a bottleneck due to:

  • Proxy Throttling: Strict proxies rate-limiting external downloads (PyPI/Conda).
  • Large Payloads: Repeatedly downloading large ML libraries (e.g., TensorFlow, PyTorch).
  • Startup Timeouts: Egress bottlenecks causing cluster creation to exceed timeout limits.

Solution Approach: "Check-Cache-or-Build"

The new scripts leverage Google Cloud Storage (GCS) to cache pre-packaged Python environments.

  1. Cache Key Identification: A unique cache key is generated based on requested packages, OS, Dataproc version, and Architecture.
  2. Cache Hit: If the environment archive exists in GCS, it is downloaded and unpacked directly to the target environment path, bypassing external downloads.
  3. Cache Miss:
    • The script locks the cache key using a .building sentinel file in GCS to prevent concurrent builds from other nodes.
    • Installs packages standardly.
    • Packages the environment and uploads it to GCS for future reuse.
  4. Locking/Waiting: If another node is building, subsequent nodes will wait for the cache file to appear rather than duplicating the build effort.

Key Features & Customizations

  • Transparent Fallback: Falls back to standard installation if GCS is unreachable or errors occur during caching.
  • Default Temp Bucket Usage: Automatically defaults to using the cluster's dataproc-temp-bucket if no specific CACHE_BUCKET is provided.
  • Customization via Metadata:
    • CACHE_BUCKET: Override default storage bucket.
    • CACHE_KEY_OVERRIDE: Force a manual cache key name.
    • CACHE_TIMEOUT: Set cache validity period (Defaults to effectively infinite/10 years).
    • TARGET_ENV_PATH: Explicitly define destination environment directory.

Testing & Usage

Documentation has been added to python/README.md covering options and examples.

Example Usage:

gcloud dataproc clusters create my-cluster \
    --metadata 'PIP_PACKAGES=pandas scipy' \
    --initialization-actions gs://<my-bucket>/python/pip-install-cached.sh

@cjac cjac self-assigned this Oct 5, 2026
@cjac

cjac commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/gcbrun

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces two new initialization scripts, conda-install-cached.sh and pip-install-cached.sh, to support caching of Conda and PyPI package environments in GCS, along with corresponding documentation in README.md. The review feedback identifies several critical and high-severity issues, including the risk of caching system-wide directories (like /usr) when target environment paths are inferred, potential script crashes under set -e during date parsing or cache unpacking failures, and pathname expansion (globbing) bugs when passing unquoted package lists. The reviewer suggests adding safety checks to prevent system directory caching, wrapping unpacking commands in conditional blocks for graceful fallbacks, using Bash arrays to handle package lists safely, and removing unused helper functions.

Comment thread python/pip-install-cached.sh
Comment thread python/conda-install-cached.sh
Comment thread python/conda-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/conda-install-cached.sh Outdated
Comment thread python/conda-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
…peed up Python package installation by caching environments in GCS.

-   **GCS Caching:** Packages are installed once, and the resulting environment is tarred and uploaded to GCS for reuse.
-   **Deterministic Cache Keys:** Cache keys are generated based on requested packages, OS, Dataproc version, and architecture.
-   **Concurrency Lock:** Uses a `.building` sentinel file in GCS to prevent multiple nodes from simultaneously building the same environment.
-   **Robust Fallback:** If GCS is unreachable or errors occur during caching, the scripts fall back to standard non-cached installation.
-   **Customization Options:**
    -   `CACHE_BUCKET`: Specify custom GCS bucket (defaults to `dataproc-temp-bucket`).
    -   `CACHE_KEY_OVERRIDE`: Manually set cache key.
    -   `CACHE_TIMEOUT`: Set cache expiration (defaults to 10 years/infinite).
    -   `TARGET_ENV_PATH`: Specify target environment directory.

-   Updated `python/README.md` with usage instructions and examples for both cached installers.
@cjac
cjac force-pushed the python-installer-caching-examples-202610 branch from c5dc9c0 to c596cda Compare October 5, 2026 21:27
- Refuse to cache system-wide directories (/usr, /usr/local, /) to prevent system corruption and excessive cache sizes.
- Handle date parsing failures gracefully during cache age verification.
- Wrap tar unpacking in conditional blocks to allow graceful fallback to rebuilding if cache is corrupted.
- Use Bash arrays to pass package lists, preventing pathname expansion (globbing) bugs with wildcards.
- Remove unused version comparison helper functions.
@cjac

cjac commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces two new initialization scripts, conda-install-cached.sh and pip-install-cached.sh, along with documentation, to support caching Conda and PyPI environments in GCS for faster Dataproc cluster creation. Feedback on these changes highlights a race condition in concurrent lock acquisition for both scripts, which can be resolved by using gsutil with the x-goog-if-generation-match:0 header. Additionally, pip-install-cached.sh should be updated to install python3-pip instead of the deprecated python-pip, and its fallback behavior when TARGET_ENV_PATH is omitted needs to be addressed, as it currently fails on system-wide environments.

Comment thread python/conda-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh
cjac added 2 commits October 5, 2026 22:06
- Introduce GCS helper functions to abstract the underlying tool (gcloud storage or gsutil).
- Prefer `gcloud storage` for better performance if available.
- Fall back to `gsutil` on older environments transparently.
- Maintain atomic uploads and generation matching across both tools.
- Also includes previous fixes for safety warnings, error handling, and atomic locks.
- Prevent caching system-wide directories like `/usr` by switching to a temporary isolated virtual environment (`/tmp/pip-isolated-env`) when system Python is detected.
- Add `ensure_venv` helper to install `python3-venv` or `python3-virtualenv` if missing.
- Add `unpack_and_merge` helper to handle unpacking and merging isolated environments back to the original system path.
- Merge isolated environment contents back to the system path after installation or unpacking to maintain backward compatibility.
@cjac

cjac commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces two new initialization action scripts, conda-install-cached.sh and pip-install-cached.sh, along with updated documentation in README.md to support cached installation of Conda and PyPI packages on Dataproc clusters using Google Cloud Storage. The code review identified several critical and high-severity issues: first, copying the entire isolated virtual environment back to /usr in the pip script can corrupt the system Python interpreter via circular symlinks, which should be resolved by only copying the site-packages directory; second, the lock-waiting mechanism in both scripts can lead to infinite loops or indefinite hangs if a build sentinel file becomes stale; and finally, the pip script is missing the remove_old_backports function required to prevent package update failures on older Debian-based Dataproc images.

Comment thread python/pip-install-cached.sh
Comment thread python/pip-install-cached.sh
Comment thread python/pip-install-cached.sh
Comment thread python/conda-install-cached.sh
Comment thread python/pip-install-cached.sh
Comment thread python/pip-install-cached.sh
Comment thread python/pip-install-cached.sh
cjac added 2 commits October 5, 2026 22:32
…ility in cached installers

- **Fix System Python Corruption (pip):** Replaced dangerous `cp -a` of entire isolated venv to `/usr` with `merge_isolated_env`, which only copies `site-packages`. This avoids creating circular symlinks and corrupting system binaries.
- **Fix Stale Sentinel Hangs (pip/conda):** Added checks to detect and remove stale `.building` sentinel files (older than 20 minutes) in GCS. Capped wait timeouts to 20 minutes and ensured stale sentinels are removed on timeout.
- **Fix Older Image Compatibility (pip):** Re-introduced `remove_old_backports` to fix `apt update` failures on Debian <= 2.1 images.
- **Improve Persistence (pip):** Moved isolated venv path from `/tmp` to `/opt/pip-isolated-env` to prevent loss of python files on node reboot.
- **Clarify Warnings:** Improved phrasing of warnings related to failed unpacking of newly available caches.
- Use a Bash array (`chan_arr`) to safely iterate over space-separated `CONDA_CHANNELS`, avoiding potential globbing bugs if a channel name or specifier contains wildcards.
@cjac

cjac commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces two new initialization scripts, conda-install-cached.sh and pip-install-cached.sh, along with updated documentation in README.md to support installing Conda and PyPI packages with GCS-based environment caching. Feedback on pip-install-cached.sh highlights three key improvements: adding timeouts to curl to prevent hangs in air-gapped environments, copying executable binaries from the isolated virtual environment's bin/ directory to /usr/local/bin/ during merging, and verifying that DATAPROC_IMAGE_VERSION is non-empty before running bc to avoid script crashes.

Comment thread python/pip-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh
Comment thread python/pip-install-cached.sh Outdated
- **Add Curl Timeouts:** Added `--connect-timeout 5 --max-time 10` to `curl` commands in `remove_old_backports` to prevent hanging in air-gapped environments.
- **Merge Binaries:** Updated `merge_isolated_env` to copy executable binaries from isolated venv's `bin/` to `/usr/local/bin/`, making command-line tools available.
- **Robust Version Check:** Verified `DATAPROC_IMAGE_VERSION` is non-empty before running `bc` comparison to avoid script crashes.
@cjac

cjac commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/gcbrun

@cjac

cjac commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

there are no gcbrun tests for the python/ directory so we'll skip dependence on the running of those tests.

@cjac
cjac marked this pull request as ready for review October 6, 2026 00:18
@cjac

cjac commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces two new initialization-action scripts, pip-install-cached.sh and conda-install-cached.sh, along with updated documentation in README.md to support installing and caching Python and Conda environments in GCS. The review feedback identifies several critical and high-severity issues in the scripts: the virtualenv at ${isolated_env_path} is never initialized, failures in merge_isolated_env are silently ignored, copied scripts retain shebangs pointing to the isolated virtualenv instead of the system python, and both scripts lack a fallback for OS detection when lsb_release is missing on minimal images.

Comment on lines +246 to +248
ensure_venv
original_env_path="${env_path}"
env_path="${isolated_env_path}"

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.

critical

The virtualenv at ${isolated_env_path} is never actually created. Although ensure_venv is called to install the system packages for venv, the virtualenv directory itself is never initialized. This will cause the script to fail on any cache miss when attempting to run pip from the non-existent virtualenv path. We should explicitly create the virtualenv after ensuring venv is available.

Suggested change
ensure_venv
original_env_path="${env_path}"
env_path="${isolated_env_path}"
ensure_venv
python3 -m venv "${isolated_env_path}" || virtualenv "${isolated_env_path}"
original_env_path="${env_path}"
env_path="${isolated_env_path}"

Comment on lines +206 to +211
if gcs_cat "${tarball}" | tar -C "${target_path}" -xz; then
if [[ "${is_isolated}" == "true" ]]; then
merge_isolated_env "${target_path}"
fi
return 0
fi

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.

high

If merge_isolated_env fails during unpack_and_merge, the failure is silently ignored and the function returns 0 (success). This happens because merge_isolated_env is called inside an if statement, which disables set -e behavior for the entire function call, and the function then unconditionally returns 0. We should explicitly propagate the failure by returning 1 if merge_isolated_env fails.

Suggested change
if gcs_cat "${tarball}" | tar -C "${target_path}" -xz; then
if [[ "${is_isolated}" == "true" ]]; then
merge_isolated_env "${target_path}"
fi
return 0
fi
if gcs_cat "${tarball}" | tar -C "${target_path}" -xz; then
if [[ "${is_isolated}" == "true" ]]; then
merge_isolated_env "${target_path}" || return 1
fi
return 0
fi

Comment on lines +190 to +192
if [[ -d "${venv_path}/bin" ]]; then
find "${venv_path}/bin" -type f ! -name "python*" ! -name "pip*" ! -name "easy_install*" ! -name "activate*" -exec cp -a {} /usr/local/bin/ \;
fi

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.

medium

When copying executable scripts from the isolated virtualenv to /usr/local/bin/, their shebangs will still point to the virtualenv's python interpreter (e.g., #!/opt/pip-isolated-env/bin/python3). While this works as long as the virtualenv exists, it is cleaner and more robust to rewrite these shebangs to use the system python (/usr/bin/env python3), making the scripts independent of the virtualenv path.

Suggested change
if [[ -d "${venv_path}/bin" ]]; then
find "${venv_path}/bin" -type f ! -name "python*" ! -name "pip*" ! -name "easy_install*" ! -name "activate*" -exec cp -a {} /usr/local/bin/ \;
fi
if [[ -d "${venv_path}/bin" ]]; then
find "${venv_path}/bin" -type f ! -name "python*" ! -name "pip*" ! -name "easy_install*" ! -name "activate*" -exec cp -a {} /usr/local/bin/ \\;
# Rewrite shebangs of copied scripts to use system python
for f in $(find "${venv_path}/bin" -type f ! -name "python*" ! -name "pip*" ! -name "easy_install*" ! -name "activate*" -exec basename {} \\;); do
if head -n 1 "/usr/local/bin/${f}" | grep -q "python"; then
sed -i "1s|^#!.*python.*|#!/usr/bin/env python3|" "/usr/local/bin/${f}"
fi
done
fi

Comment thread python/pip-install-cached.sh Outdated
fi
readonly CACHE_TIMEOUT
readonly TARGET_ENV_PATH=$(/usr/share/google/get_metadata_value attributes/TARGET_ENV_PATH || true)
readonly OS_NAME=$(lsb_release -is | tr '[:upper:]' '[:lower:]')

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.

medium

On some minimal images (such as Rocky Linux), lsb_release is not installed by default. If lsb_release is missing, the command substitution will fail and cause the script to exit immediately due to set -e. We should add a fallback to /etc/os-release to make the OS detection more robust.

Suggested change
readonly OS_NAME=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
readonly OS_NAME=$(if command -v lsb_release >/dev/null 2>&1; then lsb_release -is | tr '[:upper:]' '[:lower:]'; else . /etc/os-release; echo "${ID}" | tr '[:upper:]' '[:lower:]'; fi)

Comment thread python/conda-install-cached.sh Outdated
fi
readonly CACHE_TIMEOUT
readonly TARGET_ENV_PATH=$(/usr/share/google/get_metadata_value attributes/TARGET_ENV_PATH || true)
readonly OS_NAME=$(lsb_release -is | tr '[:upper:]' '[:lower:]')

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.

medium

On some minimal images (such as Rocky Linux), lsb_release is not installed by default. If lsb_release is missing, the command substitution will fail and cause the script to exit immediately due to set -e. We should add a fallback to /etc/os-release to make the OS detection more robust.

Suggested change
readonly OS_NAME=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
readonly OS_NAME=$(if command -v lsb_release >/dev/null 2>&1; then lsb_release -is | tr '[:upper:]' '[:lower:]'; else . /etc/os-release; echo "${ID}" | tr '[:upper:]' '[:lower:]'; fi)

cjac added 2 commits October 6, 2026 01:54
- **Fix Isolated Venv Initialization (pip):** Explicitly create the isolated virtual environment using `python3 -m venv` or `virtualenv` after ensuring its capabilities are installed.
- **Improve Error Handling (pip):** Propagate failures from `merge_isolated_env` in `unpack_and_merge` instead of silently ignoring them.
- **Robust Shebangs (pip):** Rewrite shebangs of executable scripts copied from isolated venv to `/usr/local/bin/` to use system python (`#!/usr/bin/env python3`), making them independent of the venv path.
- **Robust OS Detection (pip/conda):** Added fallback to `/etc/os-release` when `lsb_release` is missing on minimal images.
… OS detection

## Summary
Introduces integration tests for `pip-install-cached.sh` and `conda-install-cached.sh` to verify GCS-based caching behavior. Also improves the robustness of OS detection in both scripts by directly parsing `/etc/os-release`.

## Details
- **Tests**: Added `test_pip_cached.py` and `test_conda_cached.py` in the `python/` directory. These tests verify that packages are installed correctly and that cache tarballs are generated in GCS.
- **Build System**: Created `python/BUILD` to define test targets and a `test_python` suite. Integrated this suite into the root `DataprocInitActionsTestSuite`.
- **Robustness**: Replaced `lsb_release` dependency in installer scripts with direct file parsing of `/etc/os-release` via `grep` and `cut` for better compatibility across minimal images.

## Changes
- `BUILD`: Added `//python:test_python` to `DataprocInitActionsTestSuite`.
- `python/BUILD`: New file defining test targets.
- `python/conda-install-cached.sh`: Updated `OS_NAME` detection.
- `python/pip-install-cached.sh`: Updated `OS_NAME` detection.
- `python/test_conda_cached.py`: New integration test for cached conda installs.
- `python/test_pip_cached.py`: New integration test for cached pip installs.
@cjac

cjac commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

/gcbrun

@cjac cjac changed the title Feat: Add cached variants of pip and conda installers Feat: Add caching/cached variants of pip and conda installers Oct 6, 2026
@cjac

cjac commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces initialization actions and integration tests for installing PyPI and Conda packages with caching support in Google Cloud Storage (GCS) to speed up Dataproc cluster creations. Feedback on the changes highlights three main areas of improvement: ensuring the target environment directory is created before unpacking the Conda cache, handling potential network failures gracefully when querying Debian repositories in private clusters, and verifying that the conda binary is available in the PATH before attempting to configure channels.

Comment thread python/conda-install-cached.sh
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/conda-install-cached.sh Outdated
…vironments

## Summary
Addressed critical feedback to improve the reliability of `conda-install-cached.sh` and `pip-install-cached.sh`, particularly in private clusters or when custom target environments are used.

## Details
- **Conda Installer**:
  - Ensured target environment path is created (`mkdir -p`) before unpacking, preventing failure when `TARGET_ENV_PATH` points to a non-existent directory.
  - Moved channel configuration to after `conda` binary detection to avoid crashes if `conda` is not yet available in `PATH`.
  - Added graceful handling and warnings if `conda` is missing but `TARGET_ENV_PATH` is specified.
- **Pip Installer**:
  - Added fallbacks and error silencing (`2>/dev/null`) to `curl` commands in `remove_old_backports`. This prevents script failure in air-gapped or private clusters where `deb.debian.org` is unreachable.

## Changes
- `python/conda-install-cached.sh`: Fixed channel config order, added directory creation, improved `conda` binary detection.
- `python/pip-install-cached.sh`: Added fallbacks for `curl` calls.
@cjac

cjac commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces caching mechanisms for installing PyPI and Conda packages on Dataproc clusters using GCS, including the scripts pip-install-cached.sh and conda-install-cached.sh, documentation, and integration tests. Feedback suggests several improvements: using --system-site-packages in the isolated virtual environment to avoid redundant downloads, warning users against caching the base /opt/conda directory, sanitizing DATAPROC_IMAGE_VERSION to prevent bc parsing errors, and using run_command instead of assert_command in test tearDown methods to avoid masking setup failures.

Comment thread python/pip-install-cached.sh Outdated
Comment thread python/conda-install-cached.sh Outdated
Comment thread python/pip-install-cached.sh Outdated
Comment thread python/test_pip_cached.py
Comment thread python/test_conda_cached.py
…prove test cleanup

## Summary
Addressed further review feedback to optimize performance, prevent script crashes, and improve test stability.

## Details
- **Pip Installer Optimization**: Added `--system-site-packages` to isolated `venv` creation. This avoids redundant downloads of packages already present in the base image, speeding up installations and reducing cache size.
- **Version Sanitization**: Added logic to sanitize `DATAPROC_IMAGE_VERSION` to `major.minor` format before passing to `bc`, preventing syntax errors on versions with non-numeric suffixes.
- **Safety Warnings**: Added `/opt/conda` to the list of unsafe directories to cache in `conda-install-cached.sh`, warning users against overwriting the base environment.
- **Test Stability**: Refactored `tearDown` in integration tests to use `run_command` instead of `assert_command` and added existence checks for buckets. This prevents cleanup failures from masking actual test errors.

## Changes
- `python/conda-install-cached.sh`: Added `/opt/conda` to unsafe directories list.
- `python/pip-install-cached.sh`: Added `--system-site-packages` to venv creation, sanitized version for `bc`.
- `python/test_conda_cached.py`: Robust `tearDown` implementation.
- `python/test_pip_cached.py`: Robust `tearDown` implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add Cached Versions of Python/Conda Install Initialization Actions

1 participant