Skip to content

Send artifacts to the client while a cache miss is stored #596

Open

Description

@simonchrz

On a cache miss the proxy stores the whole artifact before it sends the first byte. Clients with an idle timeout give up while a large artifact downloads:

  • git-lfs after 30 seconds without data (lfs.activitytimeout), which matters for LFS objects with Cache Git LFS objects #595;
  • pacman after 10 seconds, which led to 2ed0327.

Since 2ed0327 the fetch keeps running after the client leaves, and a retry joins it. So the artifact does get cached, but the first clone or install fails or has to go through several retries. It works only if the download fits within the client's retries. A download longer than artifact_timeout never gets cached, and the whole response, download included, also has to fit within write_timeout.

Proposal: on a miss, send the bytes to the client while they are written to storage, with the hash computed along the way. StreamArtifacts already sends without storing.

  • If the hash or declared integrity doesn't match at the end, the stored copy is discarded and the client's chunked response is left unterminated, so the client rejects it. This is how StreamArtifacts already handles a mismatch.
  • Requests that join the same fetch keep waiting for the stored artifact as now. Letting them read the growing file could follow.
  • Scanners in block mode need the whole file before anything is served, so with those configured a miss stays as it is now.
  • If the client disconnects, the download keeps going, as it does today for detached fetches.

Open question: should this be the default for every ecosystem, or opt-in or per ecosystem? Package archives are mostly small, and the benefit is mainly for LFS objects, container layers and large packages.

Activity

  1. simonchrz commented on Oct 25, 2026

    @simonchrz
    ContributorAuthor

    A related case: if Storage.Store fails, for example because the disk is full, storeArtifact returns "storing artifact: …" and the client gets a 502, although the upstream answered. With the bytes already going to the client, a storage error could just drop the stored copy and let the response finish. rudolfs handles a failed cache write the same way.

  2. andrew commented on Oct 25, 2026

    @andrew
    Contributor

    Yes, let's do this. Make it the default for every ecosystem, except when a block-mode scanner is configured, where a miss stays as it is now. Small archives arrive just as fast either way; the only visible change for them is a chunked response without Content-Length, which cache_artifacts: false already sends. A per-ecosystem switch would add config without a case that needs it.

    Include the storage failure case from your comment: once bytes are going to the client, a failed Storage.Store drops the stored copy and lets the response finish. Log it at error level and count it in a metric, so a full disk doesn't go unnoticed while every request still succeeds.

    Please test it through a handler with a slow upstream, checking the client gets its first bytes before the upstream has finished, as well as the mismatch and storage failure paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions