Serve a cache hit with no database read or write (closes #309)
check / check (push) Waiting to run
check / check (push) Waiting to run
A hit opens the variant file and nothing else: its content type comes from the file's first bytes, and a file in no output format is a miss. Its access time and hit count, and the access time of a cached source a miss uses, go without waiting to the goroutine that wrote late counts, which now writes them all once a second; each kind of access time waits in a channel of 10,000 that drops the rest. checkNegativeCache leaves expired rows to CleanExpired. The .meta files, metaCache, Lookup and golang-lru go, with their tests. Dropping the .meta skip made the variant and source reconciliation walks identical, so they share one. make bench runs a parallel cache hit benchmark on the host, outside make check. Model: opus-5-5
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
@@ -9,8 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
"sneak.berlin/go/pixa/internal/httpfetcher"
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
@@ -100,347 +97,6 @@ func setupTestCache(t *testing.T) *Cache {
|
||||
return cache
|
||||
}
|
||||
|
||||
func TestCache_LookupMiss(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: testPathCat,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Hit {
|
||||
t.Error("Lookup() hit = true, want false for cache miss")
|
||||
}
|
||||
|
||||
if result.CacheStatus != CacheMiss {
|
||||
t.Errorf("Lookup() status = %v, want %v", result.CacheStatus, CacheMiss)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_StoreAndLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: testPathCat,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Store source content
|
||||
sourceContent := []byte("fake jpeg data")
|
||||
fetchResult := &httpfetcher.FetchResult{
|
||||
ContentType: testContentTypeJPEG,
|
||||
Headers: map[string][]string{testHeaderContentType: {testContentTypeJPEG}},
|
||||
}
|
||||
|
||||
sourceKey, err := cache.StoreSource(
|
||||
ctx, req, bytes.NewReader(sourceContent), fetchResult)
|
||||
if err != nil {
|
||||
t.Fatalf("StoreSource() error = %v", err)
|
||||
}
|
||||
|
||||
if sourceKey == "" {
|
||||
t.Error("StoreSource() returned empty key")
|
||||
}
|
||||
|
||||
// Store variant
|
||||
cacheKey := imagerequest.CacheKey(req)
|
||||
outputContent := []byte("fake webp data")
|
||||
|
||||
err = cache.StoreVariant(
|
||||
t.Context(), cacheKey, bytes.NewReader(outputContent), "image/webp")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreVariant() error = %v", err)
|
||||
}
|
||||
|
||||
// Now lookup should hit
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
|
||||
if !result.Hit {
|
||||
t.Error("Lookup() hit = false, want true after store")
|
||||
}
|
||||
|
||||
if result.CacheStatus != CacheHit {
|
||||
t.Errorf("Lookup() status = %v, want %v", result.CacheStatus, CacheHit)
|
||||
}
|
||||
|
||||
if result.CacheKey == "" {
|
||||
t.Error("Lookup() returned empty cache key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_NegativeCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: "/photos/notfound.jpg",
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
},
|
||||
}
|
||||
|
||||
// Store negative cache entry
|
||||
err := cache.StoreNegative(ctx, req, 404, "not found")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreNegative() error = %v", err)
|
||||
}
|
||||
|
||||
// Lookup should return miss (negative cache is checked at service level)
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Hit {
|
||||
t.Error("Lookup() hit = true, want false for negative cache entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_NegativeCacheExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Very short negative TTL for testing
|
||||
cache, err := NewCache(db, CacheConfig{
|
||||
StateDir: tmpDir,
|
||||
NegativeTTL: 1 * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cache: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: "/photos/expired.jpg",
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
},
|
||||
}
|
||||
|
||||
// Store negative cache entry
|
||||
err = cache.StoreNegative(ctx, req, 404, "not found")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreNegative() error = %v", err)
|
||||
}
|
||||
|
||||
// Wait for expiry
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Lookup should return miss after expiry
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v, want nil after expiry", err)
|
||||
}
|
||||
|
||||
if result.Hit {
|
||||
t.Error("Lookup() hit = true, want false after negative cache expiry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_VariantLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: "/photos/variant.jpg",
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Store variant
|
||||
cacheKey := imagerequest.CacheKey(req)
|
||||
outputContent := []byte("output data")
|
||||
|
||||
err := cache.StoreVariant(
|
||||
t.Context(), cacheKey, bytes.NewReader(outputContent), "image/webp")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreVariant() error = %v", err)
|
||||
}
|
||||
|
||||
// First lookup
|
||||
result1, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() first error = %v", err)
|
||||
}
|
||||
|
||||
if !result1.Hit {
|
||||
t.Error("Lookup() first hit = false")
|
||||
}
|
||||
|
||||
// Second lookup should also hit (from disk)
|
||||
result2, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() second error = %v", err)
|
||||
}
|
||||
|
||||
if !result2.Hit {
|
||||
t.Error("Lookup() second hit = false")
|
||||
}
|
||||
|
||||
if result1.CacheKey != result2.CacheKey {
|
||||
t.Error("Lookup() returned different cache keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetVariant_ReturnsContentType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: "/photos/variantct.jpg",
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Store variant
|
||||
cacheKey := imagerequest.CacheKey(req)
|
||||
outputContent := []byte("output webp data")
|
||||
|
||||
err := cache.StoreVariant(
|
||||
t.Context(), cacheKey, bytes.NewReader(outputContent), "image/webp")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreVariant() error = %v", err)
|
||||
}
|
||||
|
||||
// Lookup
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
|
||||
if !result.Hit {
|
||||
t.Fatal("Lookup() hit = false, want true")
|
||||
}
|
||||
|
||||
// GetVariant should return content type
|
||||
reader, size, contentType, err := cache.GetVariant(result.CacheKey)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVariant() error = %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
if contentType != "image/webp" {
|
||||
t.Errorf("GetVariant() ContentType = %q, want %q", contentType, "image/webp")
|
||||
}
|
||||
|
||||
if size != int64(len(outputContent)) {
|
||||
t.Errorf("GetVariant() size = %d, want %d", size, len(outputContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetVariant(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := setupTestCache(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: "/photos/output.jpg",
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 800, Height: 600},
|
||||
Format: imagerequest.FormatWebP,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Store variant
|
||||
cacheKey := imagerequest.CacheKey(req)
|
||||
outputContent := []byte("the actual output content")
|
||||
|
||||
err := cache.StoreVariant(
|
||||
t.Context(), cacheKey, bytes.NewReader(outputContent), "image/webp")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreVariant() error = %v", err)
|
||||
}
|
||||
|
||||
// Lookup to get cache key
|
||||
result, err := cache.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
|
||||
// Get variant content
|
||||
reader, _, _, err := cache.GetVariant(result.CacheKey)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVariant() error = %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
buf := make([]byte, 100)
|
||||
n, _ := reader.Read(buf)
|
||||
|
||||
if !bytes.Equal(buf[:n], outputContent) {
|
||||
t.Errorf("GetVariant() content = %q, want %q", buf[:n], outputContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Stats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -448,9 +104,9 @@ func TestCache_Stats(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Increment some stats
|
||||
cache.IncrementStats(ctx, true, 0)
|
||||
cache.IncrementStats(ctx, true, 0)
|
||||
cache.IncrementStats(ctx, false, 1024)
|
||||
cache.IncrementStats(true, 0)
|
||||
cache.IncrementStats(true, 0)
|
||||
cache.IncrementStats(false, 1024)
|
||||
|
||||
stats, err := cache.Stats(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user