check / check (push) Canceled after 0s
A cached source and its variants were served until eviction, so a changed image at the same URL was never picked up. Once a source was last checked 24 hours ago, the next request for one of its images asks upstream with If-None-Match and If-Modified-Since, from the ETag and Last-Modified it stored. A 304 renews the source and its variant without a download or a conversion. New bytes replace the source, and each variant made before them is converted again. A failed check serves the cached image and puts the URL in the negative cache. Sources and variants not checked successfully for 30 days are evicted. A variant file's modification time is its source's check time, so a hit still makes no database call. Existing tests change only in call signatures. The test that asserted two-day-old entries are served without asking upstream is replaced by the new ones. Model: opus-5-5
89 lines
2.7 KiB
Go
89 lines
2.7 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// TestEvictPastMaxAge stores a source and a variant last checked with
|
|
// upstream 31 days ago and a source and a variant checked 29 days ago.
|
|
// EvictPastMaxAge must remove the rows and files of the first two and keep
|
|
// the other two.
|
|
func TestEvictPastMaxAge(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
old := time.Now().Add(-31 * 24 * time.Hour)
|
|
recent := time.Now().Add(-29 * 24 * time.Hour)
|
|
|
|
oldSource := imagerequest.Source{Host: testHostCDN, Path: "/old.jpg"}
|
|
recentSource := imagerequest.Source{Host: testHostCDN, Path: "/recent.jpg"}
|
|
|
|
for src, checkedAt := range map[imagerequest.Source]time.Time{
|
|
oldSource: old, recentSource: recent,
|
|
} {
|
|
_, err := cache.PutSource(t.Context(), src, bytes.NewReader([]byte("source")),
|
|
SourceInfo{FetchedAt: checkedAt, CheckedAt: checkedAt})
|
|
if err != nil {
|
|
t.Fatalf("PutSource(%s) error = %v", src.Path, err)
|
|
}
|
|
}
|
|
|
|
oldVariant := imagerequest.VariantKey("aabbccdd0001")
|
|
recentVariant := imagerequest.VariantKey("aabbccdd0002")
|
|
|
|
for key, checkedAt := range map[imagerequest.VariantKey]time.Time{
|
|
oldVariant: old, recentVariant: recent,
|
|
} {
|
|
err := cache.PutVariant(t.Context(), key, checkedAt,
|
|
bytes.NewReader([]byte("variant")))
|
|
if err != nil {
|
|
t.Fatalf("PutVariant(%s) error = %v", key, err)
|
|
}
|
|
}
|
|
|
|
err := cache.EvictPastMaxAge(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("EvictPastMaxAge() error = %v", err)
|
|
}
|
|
|
|
sourceRows, err := queryStringColumn[imagerequest.SourceKey](t.Context(),
|
|
cache.db, `SELECT source_key FROM source_content`, "sources", "source")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
variantRows, err := queryStringColumn[imagerequest.VariantKey](t.Context(),
|
|
cache.db, `SELECT cache_key FROM variant_content`, "variants", "variant")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Logf("source rows left = %v, variant rows left = %v", sourceRows, variantRows)
|
|
|
|
if !slices.Equal(sourceRows, []imagerequest.SourceKey{recentSource.Key()}) {
|
|
t.Errorf("source rows left = %v, want only %s", sourceRows, recentSource.Key())
|
|
}
|
|
|
|
if !slices.Equal(variantRows, []imagerequest.VariantKey{recentVariant}) {
|
|
t.Errorf("variant rows left = %v, want only %s", variantRows, recentVariant)
|
|
}
|
|
|
|
for path, wantKept := range map[string]bool{
|
|
cache.srcContent.keyToPath(string(oldSource.Key())): false,
|
|
cache.srcContent.keyToPath(string(recentSource.Key())): true,
|
|
cache.variants.keyToPath(string(oldVariant)): false,
|
|
cache.variants.keyToPath(string(recentVariant)): true,
|
|
} {
|
|
_, statErr := os.Stat(path)
|
|
if kept := statErr == nil; kept != wantKept {
|
|
t.Errorf("file %s kept = %t, want %t (%v)", path, kept, wantKept, statErr)
|
|
}
|
|
}
|
|
}
|