|
| 1 | +Proposal: Unified Cache Manager |
| 2 | +================================ |
| 3 | + |
| 4 | +Problem |
| 5 | +------- |
| 6 | + |
| 7 | +Fromager's caching logic is scattered across multiple modules with no central |
| 8 | +coordination. Prebuilt wheels are checked separately from previously built |
| 9 | +wheels, remote wheel servers are not consulted during local builds, and there |
| 10 | +is no mechanism to share a cache across multiple backends. This leads to: |
| 11 | + |
| 12 | +- Redundant builds when wheels already exist in a remote cache. |
| 13 | +- No visibility into cache hit rates, artifact integrity, or staleness. |
| 14 | +- No short-circuit path — even a full cache hit still downloads source and |
| 15 | + sets up a build environment before discovering the wheel exists. |
| 16 | + |
| 17 | +Proposed Solution |
| 18 | +----------------- |
| 19 | + |
| 20 | +Introduce a unified ``CacheManager`` class that centralizes all cache |
| 21 | +operations behind a prioritized multi-backend lookup strategy. |
| 22 | + |
| 23 | +Architecture |
| 24 | +~~~~~~~~~~~~ |
| 25 | + |
| 26 | +.. code-block:: text |
| 27 | +
|
| 28 | + CacheManager |
| 29 | + ├── lookup_backends (searched in order): |
| 30 | + │ ├── LocalDirectoryBackend(wheels-repo/build/) [recursive for parallel builds] |
| 31 | + │ ├── LocalDirectoryBackend(wheels-repo/downloads/) |
| 32 | + │ └── RemotePEP503Backend(https://cache-server/simple/) [optional] |
| 33 | + └── store_backend: |
| 34 | + └── LocalDirectoryBackend(wheels-repo/downloads/) |
| 35 | +
|
| 36 | + Prebuilt wheels stay on the dedicated ``SourceType.PREBUILT`` path and are |
| 37 | + not consulted by the general cache short-circuit. |
| 38 | +
|
| 39 | +Key components: |
| 40 | + |
| 41 | +- ``WheelCacheKey`` — Content-addresses artifacts by canonicalized package |
| 42 | + name, version, and numeric build tag. Wheel compatibility tags |
| 43 | + (interpreter, ABI, platform) are not part of the key; instead, backends |
| 44 | + filter candidates against the current interpreter's supported tags at |
| 45 | + lookup time, keeping the key space small while ensuring only compatible |
| 46 | + wheels are returned. |
| 47 | +- ``CacheBackend`` protocol — Abstract interface implemented by |
| 48 | + ``LocalDirectoryBackend`` (filesystem) and ``RemotePEP503Backend`` |
| 49 | + (PEP 503 simple repository). Thread-safe with internal locking. |
| 50 | +- ``CacheManager`` — Orchestrates prioritized lookup across backends with |
| 51 | + graceful fallback on fetch failures, and routes all stores to a single |
| 52 | + designated local backend. |
| 53 | + |
| 54 | +Lookup and store |
| 55 | +~~~~~~~~~~~~~~~~ |
| 56 | + |
| 57 | +On lookup, the manager iterates ``lookup_backends`` in order and returns the |
| 58 | +first hit. Remote hits are downloaded to the store backend's directory and |
| 59 | +registered in the local index for subsequent fast lookups. |
| 60 | + |
| 61 | +On store, newly built wheels are always placed in the single ``store_backend`` |
| 62 | +(the downloads directory). This keeps the design simple and avoids |
| 63 | +collection-routing complexity in core Fromager. Variant-specific routing |
| 64 | +can be added as builder-level logic in a follow-up. |
| 65 | + |
| 66 | +Short-circuit optimization |
| 67 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 68 | + |
| 69 | +When a cache hit is found during the ``PREPARE_SOURCE`` phase and a |
| 70 | +``CacheManager`` is active, ``PrepareSource.run()`` skips build environment |
| 71 | +creation and build dependency resolution entirely — proceeding directly to |
| 72 | +``ProcessInstallDeps``. Install dependencies are extracted from the cached |
| 73 | +wheel's metadata. This eliminates the most expensive steps for packages that |
| 74 | +do not need rebuilding. |
| 75 | + |
| 76 | +Remote cache with integrity |
| 77 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 78 | + |
| 79 | +``RemotePEP503Backend`` lazily fetches per-project package indices on first |
| 80 | +access and maintains a session-scoped in-memory index. Lookups honor |
| 81 | +interpreter wheel tags, skip yanked files (PEP 592), and filter |
| 82 | +``data-requires-python`` the same way the resolver does. Downloads are |
| 83 | +verified with streaming SHA256 checksums, use atomic temporary files, and |
| 84 | +reject plaintext HTTP URLs that lack integrity hashes (unless |
| 85 | +``--cache-allow-insecure`` is passed for development workflows). Filenames |
| 86 | +are sanitized to prevent path traversal attacks. HTTP 400/404 (and empty |
| 87 | +successful pages) are treated as definitive misses for the run; transport |
| 88 | +and 5xx failures are retried on later lookups. |
| 89 | + |
| 90 | +Observability |
| 91 | +~~~~~~~~~~~~~ |
| 92 | + |
| 93 | +A new ``fromager cache`` CLI command group provides: |
| 94 | + |
| 95 | +- ``cache list`` — Show all cached artifacts with versions and build tags. |
| 96 | +- ``cache stats`` — Display on-disk inventory counts and sizes per backend. |
| 97 | + (Hit/miss rates are process-local during bootstrap and are not persisted.) |
| 98 | +- ``cache verify`` — Validate integrity of local cache contents. |
| 99 | +- ``cache invalidate`` — Remove specific artifacts by name/version/tag. |
| 100 | +- ``cache gc`` — Garbage-collect old build tags, keeping only the N most |
| 101 | + recent per package+version. |
| 102 | + |
| 103 | +Scope |
| 104 | +----- |
| 105 | + |
| 106 | +The new cache subsystem is opt-in via ``--use-cache-manager`` on the |
| 107 | +``bootstrap`` command. When disabled, existing behavior is preserved |
| 108 | +unchanged. |
| 109 | + |
| 110 | +Planned implementation touchpoints: |
| 111 | + |
| 112 | +- ``src/fromager/cache.py`` — New module with all cache classes and factory. |
| 113 | +- ``src/fromager/commands/cache_cmd.py`` — CLI commands. |
| 114 | +- ``src/fromager/commands/bootstrap.py`` — Wiring ``--use-cache-manager`` and |
| 115 | + ``--cache-allow-insecure`` options. |
| 116 | +- ``src/fromager/bootstrapper/_cache.py`` — Short-circuit integration via |
| 117 | + ``_find_cached_wheel_via_manager``. |
| 118 | +- ``src/fromager/context.py`` — ``cache`` property on ``WorkContext``. |
| 119 | +- ``tests/test_cache.py`` — Unit tests for the cache subsystem. |
| 120 | + |
| 121 | +Benefits |
| 122 | +-------- |
| 123 | + |
| 124 | +- Eliminates redundant builds when wheels exist in a remote or local cache. |
| 125 | +- Reduces bootstrap time by short-circuiting cached packages (skips source |
| 126 | + download, build env setup, and build dep resolution). |
| 127 | +- Provides cache observability through dedicated CLI commands. |
| 128 | +- Enforces artifact integrity with SHA256 verification and atomic writes. |
| 129 | +- Thread-safe design compatible with background I/O pre-fetching. |
| 130 | + |
| 131 | +Security considerations |
| 132 | +----------------------- |
| 133 | + |
| 134 | +- Remote downloads are verified against SHA256 hashes declared in PEP 503 |
| 135 | + index pages. At the backend layer, mismatched files are deleted and a |
| 136 | + ``ValueError`` is raised — the corrupted artifact is never persisted. |
| 137 | +- At the manager layer, any backend fetch failure (including hash mismatches) |
| 138 | + is caught and treated as a backend-level miss, falling through to the next |
| 139 | + backend or ultimately a fresh build. This two-layer design ensures |
| 140 | + integrity enforcement within each backend while providing graceful |
| 141 | + degradation across backends. |
| 142 | +- Plaintext HTTP URLs without SHA256 hashes are rejected by default. |
| 143 | + The ``--cache-allow-insecure`` flag explicitly opts in for internal or |
| 144 | + development registries. |
| 145 | +- Filenames from remote indices are sanitized to prevent directory traversal. |
| 146 | +- Local cache writes use atomic ``tempfile`` + ``rename`` to prevent readers |
| 147 | + from observing partial files. |
| 148 | +- ``scan()`` skips symlinked wheels to prevent ``invalidate``/``gc`` from |
| 149 | + deleting files outside the cache root. |
| 150 | + |
| 151 | +Verification |
| 152 | +------------ |
| 153 | + |
| 154 | +- All existing unit and e2e tests pass unchanged (legacy path preserved). |
| 155 | +- New tests cover cache components, short-circuit logic, concurrency safety, |
| 156 | + CLI commands, and error handling. |
| 157 | +- Linting (``ruff``), type checking (``mypy``), and formatting all pass. |
| 158 | + |
| 159 | +Future work |
| 160 | +----------- |
| 161 | + |
| 162 | +- Integration with ``build_tag_hook`` (issue #1059) for platform-suffixed |
| 163 | + cache keys. |
| 164 | +- Variant/collection-based store routing as builder-level logic (separate PR). |
| 165 | +- Automatic detection of accelerated packages via ELF inspection or wheel |
| 166 | + tag analysis. |
| 167 | +- Promotion of ``--use-cache-manager`` to default behavior once proven in |
| 168 | + production. |
0 commit comments