perf: stream proxied artifacts instead of buffering the full body in memory #94

Merged
benvin merged 1 commits from benvin/stream-immutable-blobs into master 2026-07-02 21:33:42 +10:00
+32 -30
View File
@@ -2,8 +2,6 @@ package proxy
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -184,6 +182,13 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa
return nil, &ProxyError{Status: resp.StatusCode, Message: fmt.Sprintf("upstream returned %d", resp.StatusCode)} return nil, &ProxyError{Status: resp.StatusCode, Message: fmt.Sprintf("upstream returned %d", resp.StatusCode)}
} }
contentType := prov.ContentType(path)
if ct := resp.Header.Get("Content-Type"); ct != "" {
contentType = ct
}
// Mutable indexes are small and may be rewritten, so buffer them in memory.
if class == ClassMutable {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
@@ -198,38 +203,37 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa
body = rewritten body = rewritten
} }
contentType := prov.ContentType(path)
if ct := resp.Header.Get("Content-Type"); ct != "" {
contentType = ct
}
if class == ClassMutable {
s3Key := storage.IndexKey(remote.Name, path) s3Key := storage.IndexKey(remote.Name, path)
if err := e.store.Upload(ctx, s3Key, bytesReader(body), int64(len(body)), contentType); err != nil { if err := e.store.Upload(ctx, s3Key, bytesReader(body), int64(len(body)), contentType); err != nil {
return nil, fmt.Errorf("upload index: %w", err) return nil, fmt.Errorf("upload index: %w", err)
} }
etag := resp.Header.Get("ETag")
_ = e.cache.SetTTL(ctx, remote.Name, path, ttl) _ = e.cache.SetTTL(ctx, remote.Name, path, ttl)
if etag != "" { if etag := resp.Header.Get("ETag"); etag != "" {
_ = e.cache.SetETag(ctx, remote.Name, path, etag, ttl) _ = e.cache.SetETag(ctx, remote.Name, path, etag, ttl)
} }
} else {
hash := sha256Hash(body)
s3Key := storage.BlobKey(hash)
exists, _ := e.store.Exists(ctx, s3Key) return &FetchResult{
if !exists { Reader: io.NopCloser(bytesReader(body)),
if err := e.store.Upload(ctx, s3Key, bytesReader(body), int64(len(body)), contentType); err != nil { ContentType: contentType,
return nil, fmt.Errorf("upload blob: %w", err) Size: int64(len(body)),
} Source: "remote",
}, nil
} }
contentHash := fmt.Sprintf("sha256:%s", hash) // Immutable blobs are streamed through the content-addressable store
if err := e.db.UpsertBlob(ctx, contentHash, s3Key, int64(len(body)), contentType); err != nil { // (tempfile -> sha256 -> S3) so arbitrarily large artifacts never sit
// fully in memory. Immutable content is never rewritten in the proxy path.
casResult, err := e.cas.Store(ctx, resp.Body, contentType)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("store blob: %w", err)
}
if err := e.db.UpsertBlob(ctx, casResult.ContentHash, casResult.S3Key, casResult.SizeBytes, contentType); err != nil {
slog.Warn("upsert blob failed", "error", err) slog.Warn("upsert blob failed", "error", err)
} }
if err := e.db.UpsertArtifact(ctx, remote.Name, path, contentHash, resp.Header.Get("ETag")); err != nil { if err := e.db.UpsertArtifact(ctx, remote.Name, path, casResult.ContentHash, resp.Header.Get("ETag")); err != nil {
slog.Warn("upsert artifact failed", "error", err) slog.Warn("upsert artifact failed", "error", err)
} }
@@ -237,12 +241,15 @@ func (e *Engine) fetchFromUpstream(ctx context.Context, remote models.Remote, pa
if etag := resp.Header.Get("ETag"); etag != "" { if etag := resp.Header.Get("ETag"); etag != "" {
_ = e.cache.SetETag(ctx, remote.Name, path, etag, ttl) _ = e.cache.SetETag(ctx, remote.Name, path, etag, ttl)
} }
}
reader, info, err := e.store.Download(ctx, casResult.S3Key)
if err != nil {
return nil, fmt.Errorf("serve stored blob: %w", err)
}
return &FetchResult{ return &FetchResult{
Reader: io.NopCloser(bytesReader(body)), Reader: reader,
ContentType: contentType, ContentType: info.ContentType,
Size: int64(len(body)), Size: casResult.SizeBytes,
Source: "remote", Source: "remote",
}, nil }, nil
} }
@@ -329,11 +336,6 @@ func (e *Engine) logAccess(remoteName, path string, cacheHit bool, size int64, u
_ = e.db.InsertAccessLog(ctx, remoteName, path, cacheHit, size, upstreamMS, "") _ = e.db.InsertAccessLog(ctx, remoteName, path, cacheHit, size, upstreamMS, "")
} }
func sha256Hash(data []byte) string {
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
func bytesReader(data []byte) io.Reader { func bytesReader(data []byte) io.Reader {
return io.NewSectionReader(readerAt(data), 0, int64(len(data))) return io.NewSectionReader(readerAt(data), 0, int64(len(data)))
} }