Add type-safe hash types for cache storage

Define ContentHash, VariantKey, and PathHash types to replace
raw strings, providing compile-time type safety for storage
operations. Update storage layer to use typed parameters,
refactor cache to use variant storage keyed by VariantKey,
and implement source content reuse on cache misses.
This commit is contained in:
2026-01-08 16:55:20 -08:00
parent 555f150179
commit be293906bc
9 changed files with 476 additions and 381 deletions

View File

@@ -99,11 +99,9 @@ func setupTestCache(t *testing.T) (*Cache, string) {
db := setupTestDB(t)
cache, err := NewCache(db, CacheConfig{
StateDir: tmpDir,
CacheTTL: time.Hour,
NegativeTTL: 5 * time.Minute,
HotCacheSize: 100,
HotCacheEnabled: true,
StateDir: tmpDir,
CacheTTL: time.Hour,
NegativeTTL: 5 * time.Minute,
})
if err != nil {
t.Fatalf("failed to create cache: %v", err)
@@ -168,17 +166,12 @@ func TestCache_StoreAndLookup(t *testing.T) {
t.Error("StoreSource() returned empty hash")
}
// Get source metadata ID
metaID, err := cache.GetSourceMetadataID(ctx, req)
if err != nil {
t.Fatalf("GetSourceMetadataID() error = %v", err)
}
// Store output content
// Store variant
cacheKey := CacheKey(req)
outputContent := []byte("fake webp data")
_, err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp")
err = cache.StoreVariant(cacheKey, bytes.NewReader(outputContent), "image/webp")
if err != nil {
t.Fatalf("StoreOutput() error = %v", err)
t.Fatalf("StoreVariant() error = %v", err)
}
// Now lookup should hit
@@ -195,8 +188,8 @@ func TestCache_StoreAndLookup(t *testing.T) {
t.Errorf("Lookup() status = %v, want %v", result.CacheStatus, CacheHit)
}
if result.OutputHash == "" {
t.Error("Lookup() returned empty output hash")
if result.CacheKey == "" {
t.Error("Lookup() returned empty cache key")
}
}
@@ -217,10 +210,14 @@ func TestCache_NegativeCache(t *testing.T) {
t.Fatalf("StoreNegative() error = %v", err)
}
// Lookup should return negative cache error
_, err = cache.Lookup(ctx, req)
if err != ErrNegativeCache {
t.Errorf("Lookup() error = %v, want ErrNegativeCache", 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")
}
}
@@ -230,11 +227,9 @@ func TestCache_NegativeCacheExpiry(t *testing.T) {
// Very short negative TTL for testing
cache, err := NewCache(db, CacheConfig{
StateDir: tmpDir,
CacheTTL: time.Hour,
NegativeTTL: 1 * time.Millisecond,
HotCacheSize: 100,
HotCacheEnabled: true,
StateDir: tmpDir,
CacheTTL: time.Hour,
NegativeTTL: 1 * time.Millisecond,
})
if err != nil {
t.Fatalf("failed to create cache: %v", err)
@@ -258,7 +253,7 @@ func TestCache_NegativeCacheExpiry(t *testing.T) {
// Wait for expiry
time.Sleep(10 * time.Millisecond)
// Lookup should return miss (not negative cache) after expiry
// 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)
@@ -269,40 +264,28 @@ func TestCache_NegativeCacheExpiry(t *testing.T) {
}
}
func TestCache_HotCache(t *testing.T) {
func TestCache_VariantLookup(t *testing.T) {
cache, _ := setupTestCache(t)
ctx := context.Background()
req := &ImageRequest{
SourceHost: "cdn.example.com",
SourcePath: "/photos/hot.jpg",
SourcePath: "/photos/variant.jpg",
Size: Size{Width: 800, Height: 600},
Format: FormatWebP,
Quality: 85,
FitMode: FitCover,
}
// Store content
sourceContent := []byte("source data")
fetchResult := &FetchResult{
ContentType: "image/jpeg",
Headers: map[string][]string{},
}
_, err := cache.StoreSource(ctx, req, bytes.NewReader(sourceContent), fetchResult)
if err != nil {
t.Fatalf("StoreSource() error = %v", err)
}
metaID, _ := cache.GetSourceMetadataID(ctx, req)
// Store variant
cacheKey := CacheKey(req)
outputContent := []byte("output data")
_, err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp")
err := cache.StoreVariant(cacheKey, bytes.NewReader(outputContent), "image/webp")
if err != nil {
t.Fatalf("StoreOutput() error = %v", err)
t.Fatalf("StoreVariant() error = %v", err)
}
// First lookup populates hot cache
// First lookup
result1, err := cache.Lookup(ctx, req)
if err != nil {
t.Fatalf("Lookup() first error = %v", err)
@@ -312,7 +295,7 @@ func TestCache_HotCache(t *testing.T) {
t.Error("Lookup() first hit = false")
}
// Second lookup should use hot cache
// Second lookup should also hit (from disk)
result2, err := cache.Lookup(ctx, req)
if err != nil {
t.Fatalf("Lookup() second error = %v", err)
@@ -322,66 +305,59 @@ func TestCache_HotCache(t *testing.T) {
t.Error("Lookup() second hit = false")
}
if result1.OutputHash != result2.OutputHash {
t.Error("Lookup() returned different hashes")
if result1.CacheKey != result2.CacheKey {
t.Error("Lookup() returned different cache keys")
}
}
func TestCache_HotCache_ReturnsContentType(t *testing.T) {
func TestCache_GetVariant_ReturnsContentType(t *testing.T) {
cache, _ := setupTestCache(t)
ctx := context.Background()
req := &ImageRequest{
SourceHost: "cdn.example.com",
SourcePath: "/photos/hotct.jpg",
SourcePath: "/photos/variantct.jpg",
Size: Size{Width: 800, Height: 600},
Format: FormatWebP,
Quality: 85,
FitMode: FitCover,
}
// Store content
sourceContent := []byte("source data")
fetchResult := &FetchResult{
ContentType: "image/jpeg",
Headers: map[string][]string{},
}
_, err := cache.StoreSource(ctx, req, bytes.NewReader(sourceContent), fetchResult)
if err != nil {
t.Fatalf("StoreSource() error = %v", err)
}
metaID, _ := cache.GetSourceMetadataID(ctx, req)
// Store variant
cacheKey := CacheKey(req)
outputContent := []byte("output webp data")
_, err = cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp")
err := cache.StoreVariant(cacheKey, bytes.NewReader(outputContent), "image/webp")
if err != nil {
t.Fatalf("StoreOutput() error = %v", err)
t.Fatalf("StoreVariant() error = %v", err)
}
// First lookup populates hot cache
result1, err := cache.Lookup(ctx, req)
// Lookup
result, err := cache.Lookup(ctx, req)
if err != nil {
t.Fatalf("Lookup() first error = %v", err)
t.Fatalf("Lookup() error = %v", err)
}
if result1.ContentType != "image/webp" {
t.Errorf("Lookup() first ContentType = %q, want %q", result1.ContentType, "image/webp")
if !result.Hit {
t.Fatal("Lookup() hit = false, want true")
}
// Second lookup uses hot cache - must still have ContentType
result2, err := cache.Lookup(ctx, req)
// GetVariant should return content type
reader, size, contentType, err := cache.GetVariant(result.CacheKey)
if err != nil {
t.Fatalf("Lookup() second error = %v", err)
t.Fatalf("GetVariant() error = %v", err)
}
defer reader.Close()
if contentType != "image/webp" {
t.Errorf("GetVariant() ContentType = %q, want %q", contentType, "image/webp")
}
if result2.ContentType != "image/webp" {
t.Errorf("Lookup() hot cache ContentType = %q, want %q", result2.ContentType, "image/webp")
if size != int64(len(outputContent)) {
t.Errorf("GetVariant() size = %d, want %d", size, len(outputContent))
}
}
func TestCache_GetOutput(t *testing.T) {
func TestCache_GetVariant(t *testing.T) {
cache, _ := setupTestCache(t)
ctx := context.Background()
@@ -394,38 +370,24 @@ func TestCache_GetOutput(t *testing.T) {
FitMode: FitCover,
}
// Store content
sourceContent := []byte("source")
fetchResult := &FetchResult{ContentType: "image/jpeg", Headers: map[string][]string{}}
_, err := cache.StoreSource(ctx, req, bytes.NewReader(sourceContent), fetchResult)
if err != nil {
t.Fatalf("StoreSource() error = %v", err)
}
metaID, err := cache.GetSourceMetadataID(ctx, req)
if err != nil {
t.Fatalf("GetSourceMetadataID() error = %v", err)
}
// Store variant
cacheKey := CacheKey(req)
outputContent := []byte("the actual output content")
outputHash, err := cache.StoreOutput(ctx, req, metaID, bytes.NewReader(outputContent), "image/webp")
err := cache.StoreVariant(cacheKey, bytes.NewReader(outputContent), "image/webp")
if err != nil {
t.Fatalf("StoreOutput() error = %v", err)
}
if outputHash == "" {
t.Fatal("StoreOutput() returned empty hash")
t.Fatalf("StoreVariant() error = %v", err)
}
// Lookup to get hash
// Lookup to get cache key
result, err := cache.Lookup(ctx, req)
if err != nil {
t.Fatalf("Lookup() error = %v", err)
}
// Get output content
reader, err := cache.GetOutput(result.OutputHash)
// Get variant content
reader, _, _, err := cache.GetVariant(result.CacheKey)
if err != nil {
t.Fatalf("GetOutput() error = %v", err)
t.Fatalf("GetVariant() error = %v", err)
}
defer reader.Close()
@@ -433,7 +395,7 @@ func TestCache_GetOutput(t *testing.T) {
n, _ := reader.Read(buf)
if !bytes.Equal(buf[:n], outputContent) {
t.Errorf("GetOutput() content = %q, want %q", buf[:n], outputContent)
t.Errorf("GetVariant() content = %q, want %q", buf[:n], outputContent)
}
}
@@ -465,9 +427,8 @@ func TestCache_CleanExpired(t *testing.T) {
db := setupTestDB(t)
cache, _ := NewCache(db, CacheConfig{
StateDir: tmpDir,
NegativeTTL: 1 * time.Millisecond,
HotCacheEnabled: false,
StateDir: tmpDir,
NegativeTTL: 1 * time.Millisecond,
})
ctx := context.Background()
@@ -506,8 +467,7 @@ func TestCache_StorageDirectoriesCreated(t *testing.T) {
db := setupTestDB(t)
_, err := NewCache(db, CacheConfig{
StateDir: tmpDir,
HotCacheEnabled: false,
StateDir: tmpDir,
})
if err != nil {
t.Fatalf("NewCache() error = %v", err)
@@ -515,9 +475,9 @@ func TestCache_StorageDirectoriesCreated(t *testing.T) {
// Verify directories were created
dirs := []string{
"cache/src-content",
"cache/dst-content",
"cache/src-metadata",
"cache/sources",
"cache/variants",
"cache/metadata",
}
for _, dir := range dirs {