Reserve capacity for regular file artifacts

This commit is contained in:
2026-10-09 00:23:27 +00:00
parent 996295d5c1
commit f1e1a45a05
4 changed files with 146 additions and 6 deletions

View File

@@ -21,6 +21,7 @@ var (
)
const fileReadChunkSize = 64 * 1024
const fileReadInitialCapacityLimit = 1 << 20
// Reader resolves artifact references into actual artifacts.
type Reader interface {
@@ -133,7 +134,11 @@ func readFileArtifact(ctx context.Context, path string, open func(string) (artif
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFile, path)
}
data := make([]byte, 0)
capacity := 0
if size := openedInfo.Size(); size > 0 {
capacity = int(min(size, int64(fileReadInitialCapacityLimit)))
}
data := make([]byte, 0, capacity)
chunk := make([]byte, fileReadChunkSize)
for {
if err := ctx.Err(); err != nil {
@@ -153,6 +158,13 @@ func readFileArtifact(ctx context.Context, path string, open func(string) (artif
return nil, fmt.Errorf("failed to read file %s: %w", path, readErr)
}
}
if len(data) > 0 && cap(data)-len(data) > len(data) {
// A stale size hint should not make a small completed artifact retain a
// disproportionately large backing array.
compact := make([]byte, len(data))
copy(compact, data)
data = compact
}
contentType := mime.TypeByExtension(filepath.Ext(path))
if contentType == "" {

View File

@@ -3,7 +3,9 @@ package artifact
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
@@ -232,6 +234,60 @@ func TestFileReaderFailuresAndMetadata(t *testing.T) {
})
}
func TestFileReaderUsesOpenedSizeOnlyAsACapacityHint(t *testing.T) {
tests := []struct {
name string
body []byte
reportedSize int64
}{
{name: "overreported size", body: []byte("actual file content"), reportedSize: 128},
{name: "underreported size", body: bytes.Repeat([]byte("x"), fileReadChunkSize+17), reportedSize: 1},
{name: "very large hint with small content", body: []byte("small actual content"), reportedSize: int64(^uint64(0) >> 1)},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "artifact.txt")
if err := os.WriteFile(filePath, tc.body, 0o600); err != nil {
t.Fatal(err)
}
reader := &fileReader{open: func(path string) (artifactFile, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
info, err := file.Stat()
if err != nil {
file.Close()
return nil, err
}
return &reportedInfoFile{
artifactFile: file,
info: reportedFileSizeInfo{FileInfo: info, size: tc.reportedSize},
}, nil
}}
artifact, err := reader.Read(context.Background(), domain.ArtifactRef{
Type: domain.ArtifactRefFile,
URI: filePath,
})
if err != nil {
t.Fatalf("read artifact: %v", err)
}
if !bytes.Equal(artifact.Body, tc.body) {
t.Fatalf("artifact body length=%d, want %d", len(artifact.Body), len(tc.body))
}
if artifact.Size != int64(len(tc.body)) {
t.Fatalf("artifact size=%d, want %d", artifact.Size, len(tc.body))
}
wantHash := fmt.Sprintf("%x", sha256.Sum256(tc.body))
if artifact.Hash != wantHash {
t.Fatalf("artifact hash=%q, want %q", artifact.Hash, wantHash)
}
})
}
}
func TestFileReaderCancelsAfterReadProgress(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "artifact.bin")
content := bytes.Repeat([]byte("x"), fileReadChunkSize*2)
@@ -268,6 +324,13 @@ func (f *reportedInfoFile) Stat() (os.FileInfo, error) {
return f.info, nil
}
type reportedFileSizeInfo struct {
os.FileInfo
size int64
}
func (info reportedFileSizeInfo) Size() int64 { return info.size }
type cancelAfterProgressFile struct {
artifactFile
cancel context.CancelFunc