Cache rewritten npm and Composer metadata - #402
Merged
Merged
Conversation
The npm and Composer handlers rewrite every metadata document they serve so that download URLs point at the proxy: decode the whole document into generic maps, change the URLs, encode it again, and for Composer expand the minified format first. That ran on every request, cached metadata included. With metadata caching on and upstream out of the picture, a cached request still cost 7 ms for @babel/core, 17 ms for symfony/console and 155 ms and 134 MB of allocations for typescript, and an 8 vCPU VM serving cached npm packuments to 20 clients ran the proxy at 450% CPU for about 1,100 requests a second. Rewritten documents are now kept in memory, keyed by the ecosystem, proxy URL, package and a SHA-256 of the raw document, so new bytes from upstream are rewritten again and nothing is served stale. Requests that arrive while a document is being rewritten wait for that rewrite rather than running their own; a waiter leaves when its client does, and the rewrite still completes and is cached. The denylist is fixed at startup, so it needs no place in the key. Cooldown filtering depends on the current time, so with cooldown on the cache is bypassed. metadata_rewrite_cache_size bounds the cache (default "256MB", least recently used out first, "0" to rewrite on every request). NewProxy callers keep the old behaviour unless they set it. Whole cached requests, measured locally: @babel/core 7.1 ms -> 0.38 ms, 54,489 -> 119 allocations typescript 155 ms -> 10.5 ms, 1.37M -> 133 allocations symfony/console 17.4 ms -> 0.48 ms, 165,909 -> 123 allocations What remains is reading the raw document from storage and hashing it.
andrew
approved these changes
Oct 3, 2026
Contributor
|
Thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The npm and Composer handlers rewrite every metadata document they serve so that download URLs point at the proxy. That means decoding the whole document into generic maps, changing the URLs and encoding it again, and for Composer, expanding the minified format first. It ran on every request, including requests for metadata that was already cached.
I measured the cost on an 8 vCPU VM with
cache_metadataon, so upstream was out of the picture:typescriptrequest allocates about 134 MB.What this changes
metadata_rewrite_cache_sizebounds the cache, least recently used out first. It defaults to"256MB", and"0"rewrites on every request. It's also available asPROXY_METADATA_REWRITE_CACHE_SIZE.NewProxycallers such as tests and the mirror command keep the old behaviour unless they callSetMetadataRewriteCacheSize, as withBatchHits.rewriteMetadatareturns its input unchanged when a document has noversionsobject. Caching that is safe because the input is already the shared read-only slice fromfetchOrCacheMetadata.Benchmarks
Whole cached requests (lookup, read, rewrite, response), measured locally on real documents:
@babel/core(426 KB)typescript(8.7 MB)symfony/consoleWhat remains is reading the raw document from storage and hashing it. Hashing is most of what's left for
typescript.Tests cover:
"0"disabling the cacheRoutes()Tested with
go test ./...,-raceoninternal/handler, and golangci-lint. I moved the new config check intovalidateComponentsto keepConfig.Validateunder thegocognitlimit.Persisting rewritten documents across restarts would be possible later, but in-process caching gets nearly all the benefit with much less to review.