Skip to content

fix(backend): Don't cache empty version lists#9444

Merged
jdx merged 1 commit intojdx:mainfrom
c22:main
Apr 29, 2026
Merged

fix(backend): Don't cache empty version lists#9444
jdx merged 1 commit intojdx:mainfrom
c22:main

Conversation

@c22
Copy link
Copy Markdown
Contributor

@c22 c22 commented Apr 28, 2026

When _list_remote_versions returns no versions (e.g. invalid Go module path), the empty list was cached as a positive result via
get_or_try_init_async.

This poisons the cache for up to 1 hour, both on disk and in the in-memory OnceCell.

Clear both the disk cache file and the in-memory cells after fetching if the version list is empty so subsequent calls re-fetch instead of serving a stale empty result.

This is an alternative approach to my earlier attempt at fixing this because the previous approach ended up getting a bit complicated (required backend specific changes).

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR fixes a cache poisoning bug where list_remote_versions_with_info would write an empty Vec into both the on-disk cache and the in-memory OnceCell when _list_remote_versions returned no results (e.g. an invalid Go module path), causing all subsequent calls for up to 1 hour to immediately return an empty list. The fix clears both caches after a successful fetch that yields an empty result, so the next call for the same backend will re-fetch from the upstream.

Confidence Score: 5/5

Safe to merge — the fix correctly addresses cache poisoning with no new logic errors or regressions introduced.

Both previously-raised concerns (bail! breaking unresolved_latest_version fallback, and in-memory OnceCell remaining poisoned) are addressed. The warn! path is preserved, the .clone() before clear() ensures no borrow issue, and Default::default() on both OnceCell types genuinely resets the cells. No security implications, no new edge cases introduced.

No files require special attention.

Important Files Changed

Filename Overview
src/cache.rs Removes #[cfg(test)] gate from clear(), makes it &mut self, and adds *self.cache = Default::default(); *self.cache_async = Default::default(); to reset both in-memory OnceCells alongside the disk file deletion. Change is correct — both once_cell::sync::OnceCell and tokio::sync::OnceCell implement Default as a new empty cell.
src/backend/mod.rs Adds mut to the remote_versions guard so clear() can be called, chains .clone() onto the get_or_try_init_async result to get an owned Vec before calling clear(), and calls remote_versions.clear() when the fetched list is empty. The warn! path for empty lists is preserved, keeping the unresolved_latest_version fallback intact.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Backend as Backend::list_remote_versions_with_info
    participant Mutex as TokioMutex<CacheManager>
    participant OnceCell as OnceCell (memory)
    participant Disk as Disk cache

    Caller->>Backend: list_remote_versions_with_info()
    Backend->>Mutex: lock()
    Backend->>OnceCell: get_or_try_init_async()
    OnceCell-->>Backend: (uninitialized — fetch)
    Backend->>Backend: _list_remote_versions() → []
    Backend->>Disk: write(empty Vec)
    Backend->>OnceCell: initialize(empty Vec)
    OnceCell-->>Backend: &[]
    Backend->>Backend: .clone() → owned []
    alt versions.is_empty()
        Backend->>Disk: remove cache file
        Backend->>OnceCell: *cell = Default::default() (reset)
    end
    Backend->>Mutex: unlock()
    Backend-->>Caller: Ok([]) — caller sees empty this run

    Note over Caller,Disk: Next call in same process re-fetches (OnceCell reset)

    Caller->>Backend: list_remote_versions_with_info() [retry]
    Backend->>Mutex: lock()
    Backend->>OnceCell: get_or_try_init_async()
    OnceCell-->>Backend: (uninitialized again — re-fetch)
    Backend->>Backend: _list_remote_versions() → [v1, v2, ...]
    Backend->>Disk: write(versions)
    Backend->>OnceCell: initialize(versions)
    Backend->>Mutex: unlock()
    Backend-->>Caller: Ok([v1, v2, ...])
Loading

Reviews (11): Last reviewed commit: "fix(backend): Don't cache empty version ..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

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 modifies the backend version retrieval logic to return an error instead of a warning when no versions are found, specifically for non-HTTP backends. The review feedback points out that S3 backends should likely be excluded from this strict check as well, consistent with how they are handled elsewhere in the codebase, to avoid unexpected errors when version lists are missing.

Comment thread src/backend/mod.rs Outdated
Comment thread src/backend/mod.rs
@c22 c22 force-pushed the main branch 2 times, most recently from 8dc00fb to 10c05c4 Compare April 28, 2026 01:18
@c22 c22 changed the title fix(go): Don't cache empty version lists fix(backend): Don't cache empty version lists Apr 28, 2026
@c22 c22 force-pushed the main branch 3 times, most recently from 8b0aab6 to 1da830c Compare April 28, 2026 02:37
@c22
Copy link
Copy Markdown
Contributor Author

c22 commented Apr 28, 2026

The e2e failures appear to be related to GH rate limits.

@c22 c22 force-pushed the main branch 4 times, most recently from a6c352f to c6bf536 Compare April 28, 2026 05:25
@c22
Copy link
Copy Markdown
Contributor Author

c22 commented Apr 28, 2026

With the newer, simpler approach it should be noted that BackendType::S3 is not currently excluded from the warnings.

This PR ended up being a focused change, so I am not changing the logging for BackendType::S3.

It's probably better to refactor this into trait-based logic in the future if desirable (which is what my first attempt did but ended up getting out of hand).

Comment thread src/backend/mod.rs
@c22 c22 force-pushed the main branch 2 times, most recently from 92fa5b6 to 7b4b25e Compare April 28, 2026 09:08
When _list_remote_versions returns no versions (e.g. invalid Go module path), the empty list was cached as a positive result via
get_or_try_init_async.

This poisons the cache for up to 1 hour, both on disk and in the in-memory OnceCell.

Clear both the disk cache file and the in-memory cells after fetching if the version list is empty so subsequent calls re-fetch instead of serving a stale empty result.
@jdx jdx merged commit 7242aa2 into jdx:main Apr 29, 2026
34 checks passed
jdx added a commit that referenced this pull request May 2, 2026
…ends (#9548)

## Summary

Backends that opt into installing an unresolved `latest` selector (today
only `pipx` for git-backed requests) legitimately return an empty remote
version list when their source has no discoverable versions — e.g. a
GitHub repo with no releases (only tags). The hard-error path was
already addressed in #9401 by falling back to the unresolved selector.

What's still left is noise: every command that touches such a tool emits
3–12 repeated `mise WARN No versions found for …` warnings. The
repetition comes from #9444 (don't cache empty version lists) — every
separate call site that asks for remote versions in the same process
re-fetches and re-warns. Underlying GitHub API calls are still
deduplicated by `RELEASE_CACHE`, so this is purely log noise, not extra
HTTP traffic.

This change suppresses the warning when the backend opts into
`unresolved_latest_version()`. For those backends an empty list is the
expected steady state, not an error worth flagging. Other backends still
warn as before, so the warning continues to catch real problems (typo'd
repos, configs pointing at the wrong place).

Refs #9381.

## Reproduction

\`\`\`toml
# ~/.config/mise/conf.d/tools.toml
[tools]
"pipx:a13xp0p0v/kernel-hardening-checker" = "latest"
\`\`\`

`a13xp0p0v/kernel-hardening-checker` has 0 GitHub releases (14 tags).
Before this PR:

\`\`\`
$ mise current
mise WARN  No versions found for pipx:a13xp0p0v/kernel-hardening-checker
mise WARN  No versions found for pipx:a13xp0p0v/kernel-hardening-checker
mise WARN  No versions found for pipx:a13xp0p0v/kernel-hardening-checker
pipx:a13xp0p0v/kernel-hardening-checker latest
\`\`\`

After:

\`\`\`
$ mise current
pipx:a13xp0p0v/kernel-hardening-checker latest
\`\`\`

`mise ls`, `mise outdated`, and `mise install` are similarly quiet now.

## Test plan

- [x] Manual repro with the discussion's tool — all warnings gone for
`mise ls`, `mise current`, `mise outdated`, `mise install`
- [x] `mise run lint`
- [x] `cargo build`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: only changes warning emission logic when a backend opts into
`unresolved_latest_version()`, with no effect on version fetching or
installation behavior.
> 
> **Overview**
> Suppresses repeated `No versions found` warnings when
`list_remote_versions_with_info` returns an empty list for backends that
support installing an unresolved `latest` selector (via
`unresolved_latest_version()`). Other backends still warn on empty
remote version lists as before.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9ca20e5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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.

2 participants