Skip to content

Commit 414cf9f

Browse files
Xaenaltclaudecursoragent
committed
docs(proposals): add ADR for unified cache manager
Document the architectural decision to introduce a centralized CacheManager with layered backends, store routing, short-circuit optimization, and security hardening. Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Sean Pryor <spryor@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 44daa62 commit 414cf9f

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 base cache of common dependencies across
11+
hardware-specific bootstrap variants (e.g. CUDA, Gaudi). This leads to:
12+
13+
- Redundant builds when wheels already exist in a remote cache or sibling
14+
variant's output.
15+
- Duplicated storage of common (non-accelerated) dependencies in every
16+
variant's output directory.
17+
- No visibility into cache hit rates, artifact integrity, or staleness.
18+
- No short-circuit path — even a full cache hit still downloads source and
19+
sets up a build environment before discovering the wheel exists.
20+
21+
Proposed Solution
22+
-----------------
23+
24+
Introduce a unified ``CacheManager`` class that centralizes all cache
25+
operations behind a layered lookup strategy:
26+
27+
Architecture
28+
~~~~~~~~~~~~
29+
30+
.. code-block:: text
31+
32+
CacheManager
33+
├── CacheCollection("default")
34+
│ ├── LocalDirectoryBackend(wheels-repo/downloads/)
35+
│ ├── LocalDirectoryBackend(wheels-repo/prebuilt/)
36+
│ └── RemotePEP503Backend(https://cache-server/simple/) [optional]
37+
└── CacheCollection("gaudi-ubi9") [variant, if active]
38+
└── LocalDirectoryBackend(wheels-repo-gaudi-ubi9/downloads/)
39+
40+
Key components:
41+
42+
- ``WheelCacheKey`` — Content-addresses artifacts by canonicalized package
43+
name, version, and numeric build tag.
44+
- ``CacheBackend`` protocol — Abstract interface implemented by
45+
``LocalDirectoryBackend`` (filesystem) and ``RemotePEP503Backend``
46+
(PEP 503 simple repository).
47+
- ``CacheCollection`` — Named group of backends searched in priority order
48+
(e.g. "default" searches local then remote).
49+
- ``StoreRouter`` — Determines which collection owns a given package based on
50+
the variant's top-level requirements file and optional overrides.
51+
- ``CacheManager`` — Orchestrates hierarchical lookup across collections with
52+
fallback from variant to default.
53+
54+
Lookup and store routing
55+
~~~~~~~~~~~~~~~~~~~~~~~~
56+
57+
On lookup, the manager searches the active variant collection first, then
58+
falls back to the default collection. Within each collection, backends are
59+
queried in registration order (local before remote).
60+
61+
On store, the ``StoreRouter`` routes packages explicitly listed in the
62+
variant's ``requirements.txt`` to the variant collection directory. All
63+
unlisted transitive dependencies are stored in the default collection. This
64+
prevents duplication of common packages across variant outputs.
65+
66+
Short-circuit optimization
67+
~~~~~~~~~~~~~~~~~~~~~~~~~~
68+
69+
When a cache hit is found during the ``PREPARE_SOURCE`` phase, the
70+
bootstrapper skips source download, build environment creation, and build
71+
dependency resolution entirely — proceeding directly to install dependency
72+
extraction from the cached wheel's metadata. This eliminates the most
73+
expensive steps for packages that do not need rebuilding.
74+
75+
Remote cache with integrity
76+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
77+
78+
``RemotePEP503Backend`` lazily fetches per-project package indices on first
79+
access and maintains a session-scoped in-memory index. Downloads are verified
80+
with streaming SHA256 checksums, use atomic temporary files, and reject
81+
plaintext HTTP URLs that lack integrity hashes (unless ``--cache-allow-insecure``
82+
is passed for development workflows). Filenames are sanitized to prevent path
83+
traversal attacks.
84+
85+
Observability
86+
~~~~~~~~~~~~~
87+
88+
A new ``fromager cache`` CLI command group provides:
89+
90+
- ``cache list`` — Show all cached artifacts with versions and build tags.
91+
- ``cache stats`` — Display hit/miss counts and rates from the last run.
92+
- ``cache verify`` — Validate integrity of local cache contents.
93+
- ``cache invalidate`` — Remove specific artifacts by name/version/tag.
94+
- ``cache gc`` — Garbage-collect old build tags, keeping only the N most
95+
recent per package+version.
96+
97+
Scope
98+
-----
99+
100+
The new cache subsystem is opt-in via ``--use-cache-manager`` on the
101+
``bootstrap`` command. When disabled, existing behavior is preserved
102+
unchanged.
103+
104+
Files added or modified:
105+
106+
- ``src/fromager/cache.py`` — New module with all cache classes.
107+
- ``src/fromager/commands/cache_cmd.py`` — CLI commands and factory function.
108+
- ``src/fromager/commands/bootstrap.py`` — Wiring ``--use-cache-manager`` and
109+
``--cache-allow-insecure`` options.
110+
- ``src/fromager/bootstrapper.py`` — Short-circuit path and store routing for
111+
built wheels.
112+
- ``src/fromager/context.py`` — ``cache`` property on ``WorkContext``.
113+
- ``src/fromager/requirements_file.py`` — ``CACHED`` source type.
114+
- ``tests/test_cache.py`` — Unit tests for the cache subsystem.
115+
- ``tests/test_bootstrapper_iterative.py`` — Integration tests for cache
116+
dispatch and store routing.
117+
118+
Benefits
119+
--------
120+
121+
- Eliminates redundant builds when wheels exist in a remote or sibling cache.
122+
- Reduces bootstrap time by short-circuiting cached packages (skips source
123+
download, build env setup, and build dep resolution).
124+
- Prevents duplication of common dependencies across variant outputs via
125+
hierarchical store routing.
126+
- Provides cache observability through dedicated CLI commands.
127+
- Enforces artifact integrity with SHA256 verification and atomic writes.
128+
129+
Security considerations
130+
-----------------------
131+
132+
- Remote downloads are verified against SHA256 hashes declared in PEP 503
133+
index pages. Mismatched files are deleted and raise an error.
134+
- Plaintext HTTP URLs without SHA256 hashes are rejected by default.
135+
The ``--cache-allow-insecure`` flag explicitly opts in for internal or
136+
development registries.
137+
- Filenames from remote indices are sanitized to prevent directory traversal.
138+
- Local cache writes use atomic ``tempfile`` + ``rename`` to prevent readers
139+
from observing partial files.
140+
- ``scan()`` skips symlinked wheels to prevent ``invalidate``/``gc`` from
141+
deleting files outside the cache root.
142+
- Fetch failures (network errors, hash mismatches) are caught and treated as
143+
cache misses, falling through to the next backend or a fresh build.
144+
145+
Verification
146+
------------
147+
148+
- All existing unit and e2e tests pass unchanged (legacy path preserved).
149+
- 169 new tests cover cache components, short-circuit logic, store routing,
150+
and error handling.
151+
- Validated on walkerpass4 with both local and remote caches:
152+
pre-populated local cache achieves full hit rate; remote PEP 503 server
153+
correctly populates local cache and only uncached packages trigger builds.
154+
- Linting (``ruff``), type checking (``mypy``), and formatting all pass.
155+
156+
Future work
157+
-----------
158+
159+
- Integration with ``build_tag_hook`` (issue #1059) for platform-suffixed
160+
cache keys.
161+
- Automatic detection of accelerated packages via ELF inspection or wheel
162+
tag analysis.
163+
- Promotion of ``--use-cache-manager`` to default behavior once proven in
164+
production.

0 commit comments

Comments
 (0)