Skip to content

Conversation

@js-murph
Copy link
Collaborator

@js-murph js-murph commented Jan 22, 2026

What?

This replaces the custom gomod handling and embeds https://github.com/goproxy/goproxy instead.

Why?

More robust and battle tested than our current implementation. It also should give us a path to supporting private modules as well via a custom fetcher in the future.

Tests

$ time curl http://localhost:8080/gomod/github.com/alecthomas/kong/@latest
{"Version":"v1.13.0","Time":"2025-11-12T21:31:44Z"}
real	0m0.976s
user	0m0.007s
sys	0m0.016s

$ time curl http://localhost:8080/gomod/github.com/alecthomas/kong/@latest
{"Version":"v1.13.0","Time":"2025-11-12T21:31:44Z"}
real	0m0.052s
user	0m0.006s
sys	0m0.012s

@js-murph js-murph requested a review from alecthomas as a code owner January 22, 2026 04:14
max-ttl = "8h"
}

gomod {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sick!

Comment on lines 24 to 25
MutableTTL time.Duration `hcl:"mutable-ttl,optional" help:"TTL for mutable Go module proxy endpoints (list, latest). Defaults to 5m." default:"5m"`
ImmutableTTL time.Duration `hcl:"immutable-ttl,optional" help:"TTL for immutable Go module proxy endpoints (versioned info, mod, zip). Defaults to 168h (7 days)." default:"168h"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are these relevant anymore?

// as a signal to fetch from upstream.
func (g *goproxyCacher) Get(ctx context.Context, name string) (io.ReadCloser, error) {
// Hash the name to create a cache key that matches cachew's format
key := cache.Key(sha256.Sum256([]byte(name)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
key := cache.Key(sha256.Sum256([]byte(name)))
key := cache.NewKey(name)

// Hash the name to create a cache key that matches cachew's format
key := cache.Key(sha256.Sum256([]byte(name)))

// Try to open the cached content
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we get rid of all these completely superfluous comments? I find they make it quite a bit harder to comprehend what is actually going on.

I'm a bit surprised they're showing up TBH, the AGENTS.md file has explicit instructions about it 🤔

// it represents mutable or immutable content.
func (g *goproxyCacher) Put(ctx context.Context, name string, content io.ReadSeeker) error {
// Hash the name to create a cache key
key := cache.Key(sha256.Sum256([]byte(name)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
key := cache.Key(sha256.Sum256([]byte(name)))
key := cache.NewKey(name)

Comment on lines +44 to +45
// The TTL is determined by inspecting the cache name to identify whether
// it represents mutable or immutable content.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Super weird, is this actually true? What is "name" in the context of the interface? Does it actually include things like /v@/list?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I checked and you are 100% right...that's super weird, as the default implementation doesn't seem to do any sort of timing out of /list entries etc...

Ah, I bet I know what's going on - I bet it uses an ETag to conditionally update it.

I think that means we can actually get rid of the two different TTL types, and just rely on the ETag to manage the cache.

Copy link
Collaborator

@alecthomas alecthomas Jan 22, 2026

Choose a reason for hiding this comment

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

I'm not at all convinced I'm correct about that actually, however it does seem to overwrite the list on every go get, as well as hit upstream list on every request...so there doesn't seem to be much point in having a TTL on list at all. If anything, it might be better to completely ignore /list...

DBG Request received client=127.0.0.1:56140 request="GET /gomod/github.com/alecthomas/chroma/@v/list"
DBG Request received client=127.0.0.1:56142 request="GET /gomod/github.com/alecthomas/@v/list"
DBG Request received client=127.0.0.1:56141 request="GET /gomod/github.com/@v/list"
DBG Go module proxy outbound request method=GET url=https://proxy.golang.org/github.com/alecthomas/@v/list
DBG Go module proxy outbound request method=GET url=https://proxy.golang.org/github.com/@v/list
DBG Go module proxy outbound request method=GET url=https://proxy.golang.org/github.com/alecthomas/chroma/@v/list
DBG goproxy.Get client=127.0.0.1:56141 request="GET /gomod/github.com/@v/list" name=github.com/@v/list
DBG goproxy.Get client=127.0.0.1:56142 request="GET /gomod/github.com/alecthomas/@v/list" name=github.com/alecthomas/@v/list
DBG goproxy.Put client=127.0.0.1:56140 request="GET /gomod/github.com/alecthomas/chroma/@v/list" name=github.com/alecthomas/chroma/@v/list
DBG Request received client=127.0.0.1:56142 request="GET /gomod/github.com/dlclark/regexp2/@v/list"
DBG Go module proxy outbound request method=GET url=https://proxy.golang.org/github.com/dlclark/regexp2/@v/list
DBG goproxy.Put client=127.0.0.1:56142 request="GET /gomod/github.com/dlclark/regexp2/@v/list" name=github.com/dlclark/regexp2/@v/list

Super weird.

Also interestingly, on cold start it seems to hit the upstream no matter what. Subsequent runs are quite fast, so it appears it might have some in-memory caching too.

I'm not sure what to think about it all.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I checked the source and it does indeed just call the upstream. I also checked and the upstream has cache control headers with a 60 second TTL.

ttl := g.calculateTTL(name)

// Determine Content-Type from the file extension
contentType := g.getContentType(name)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this make sense?

@alecthomas
Copy link
Collaborator

I pushed up a minor fix to configure it to use our logger, so it didn't spam to stdout, however it does spam errors for every package it pulls, for some reason:

Running:

~/dev/cachew $ GOPROXY=http://localhost:8080/gomod/ go get github.com/alecthomas/chroma@latest
go: added github.com/alecthomas/chroma v0.10.0

Gives:

ERR failed to list module versions error="not found: invalid github.com import path \"github.com\"" target=github.com/@v/list
ERR failed to list module versions error="not found: invalid github.com import path \"github.com/alecthomas\"" target=github.com/alecthomas/@v/list

I think we're going to need some kind of log interceptor to filter these out 😕

@alecthomas
Copy link
Collaborator

Okay fixed the logging too!

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.

3 participants