From db49042f6586f48a587b914ae5b9251a2b274f5f Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 13 Oct 2026 01:31:00 +0000 Subject: [PATCH] Remove the unused cache lifetime setting (closes #69) CacheConfig.CacheTTL was set to DefaultCacheTTL (24 hours) and never read, so cached images never expired. Remove the field, the constant and the line that set it. Cached source and transformed images stay until eviction removes them to keep the cache within cache_max_bytes. A new test checks that a source and a transformed image cached two days ago are still served from the cache without a new upstream fetch. Existing tests lose only the CacheTTL line of their cache config; no test's checks change. Model: opus-5-5 --- TODO.md | 5 ++ internal/handlers/handlers.go | 1 - internal/handlers/handlers_internal_test.go | 1 - .../image_cache_control_internal_test.go | 1 - .../handlers/image_errors_internal_test.go | 1 - .../referer_blocklist_internal_test.go | 1 - internal/imgcache/cache.go | 1 - internal/imgcache/cache_internal_test.go | 2 - internal/imgcache/eviction_internal_test.go | 1 - internal/imgcache/module.go | 1 - .../imgcache/negative_cache_internal_test.go | 2 - internal/imgcache/service_internal_test.go | 58 +++++++++++++++++++ internal/imgcache/stats_internal_test.go | 5 -- internal/imgcache/testutil_internal_test.go | 1 - 14 files changed, 63 insertions(+), 18 deletions(-) diff --git a/TODO.md b/TODO.md index 66067f4..0f36ccf 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,11 @@ P2: security: per-origin rate limiting # Completed Steps +- 2026-10-13 the unused cache lifetime is removed (closes #69): + `CacheConfig.CacheTTL`, which pixad set to `DefaultCacheTTL` (24 hours), was + never read, so cached images never expired. The field and the constant are + removed. Cached source and transformed images stay until eviction removes them + to keep the cache within `cache_max_bytes`. - 2026-10-12 per-client limit on the image routes (closes #236): the new setting `image_requests_per_minute` (`PIXA_IMAGE_REQUESTS_PER_MINUTE`), default 600, is how many `GET` and `HEAD` requests one client may send to `/v1/image/` and diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 6405d08..297ba2d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -106,7 +106,6 @@ func (s *Handlers) WaitForProcessing(ctx context.Context) int { func newCacheConfig(cfg *config.Config, log *slog.Logger) imgcache.CacheConfig { return imgcache.CacheConfig{ StateDir: cfg.StateDir, - CacheTTL: imgcache.DefaultCacheTTL, NegativeTTL: imgcache.DefaultNegativeTTL, MaxBytes: cfg.CacheMaxBytes, UseDefaultMaxBytes: !cfg.CacheMaxBytesExplicit, diff --git a/internal/handlers/handlers_internal_test.go b/internal/handlers/handlers_internal_test.go index 35c1522..f396a05 100644 --- a/internal/handlers/handlers_internal_test.go +++ b/internal/handlers/handlers_internal_test.go @@ -47,7 +47,6 @@ func setupTestHandler(t *testing.T) *testFixtures { cache, err := imgcache.NewCache(db, imgcache.CacheConfig{ StateDir: tmpDir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { diff --git a/internal/handlers/image_cache_control_internal_test.go b/internal/handlers/image_cache_control_internal_test.go index 7845df5..f4e65a5 100644 --- a/internal/handlers/image_cache_control_internal_test.go +++ b/internal/handlers/image_cache_control_internal_test.go @@ -31,7 +31,6 @@ func newSignedHostServer( cache, err := imgcache.NewCache(setupTestDB(t), imgcache.CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { diff --git a/internal/handlers/image_errors_internal_test.go b/internal/handlers/image_errors_internal_test.go index 0bb3fea..fbbcf6f 100644 --- a/internal/handlers/image_errors_internal_test.go +++ b/internal/handlers/image_errors_internal_test.go @@ -29,7 +29,6 @@ func newImageRoute(t *testing.T, fetcher httpfetcher.Fetcher) http.Handler { cache, err := imgcache.NewCache(setupTestDB(t), imgcache.CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { diff --git a/internal/handlers/referer_blocklist_internal_test.go b/internal/handlers/referer_blocklist_internal_test.go index 9235eb1..86f0c32 100644 --- a/internal/handlers/referer_blocklist_internal_test.go +++ b/internal/handlers/referer_blocklist_internal_test.go @@ -49,7 +49,6 @@ func newRefererRoutes(t *testing.T) (http.Handler, *Handlers, *countingFetcher) cache, err := imgcache.NewCache(setupTestDB(t), imgcache.CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { diff --git a/internal/imgcache/cache.go b/internal/imgcache/cache.go index ff27456..1f73019 100644 --- a/internal/imgcache/cache.go +++ b/internal/imgcache/cache.go @@ -35,7 +35,6 @@ const metaCacheSize = 10000 // CacheConfig holds cache configuration. type CacheConfig struct { StateDir string - CacheTTL time.Duration NegativeTTL time.Duration // MaxBytes is the disk cache size limit in bytes that eviction diff --git a/internal/imgcache/cache_internal_test.go b/internal/imgcache/cache_internal_test.go index b4026f5..6659b93 100644 --- a/internal/imgcache/cache_internal_test.go +++ b/internal/imgcache/cache_internal_test.go @@ -102,7 +102,6 @@ func setupTestCache(t *testing.T) *Cache { cache, err := NewCache(db, CacheConfig{ StateDir: tmpDir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { @@ -241,7 +240,6 @@ 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, }) if err != nil { diff --git a/internal/imgcache/eviction_internal_test.go b/internal/imgcache/eviction_internal_test.go index af91ac5..7d54df1 100644 --- a/internal/imgcache/eviction_internal_test.go +++ b/internal/imgcache/eviction_internal_test.go @@ -70,7 +70,6 @@ func newEvictionTestCache(t *testing.T, maxBytes int64) (*Cache, string) { // backwards compatibility with existing fixtures. cache, err := NewCache(db, CacheConfig{ StateDir: tmpDir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, MaxBytes: maxBytes, DisableDiskCache: maxBytes == 0, diff --git a/internal/imgcache/module.go b/internal/imgcache/module.go index 266e398..81d329a 100644 --- a/internal/imgcache/module.go +++ b/internal/imgcache/module.go @@ -4,6 +4,5 @@ import "time" // CacheConfig defaults. const ( - DefaultCacheTTL = 24 * time.Hour DefaultNegativeTTL = 5 * time.Minute ) diff --git a/internal/imgcache/negative_cache_internal_test.go b/internal/imgcache/negative_cache_internal_test.go index 856f6ae..f2c2115 100644 --- a/internal/imgcache/negative_cache_internal_test.go +++ b/internal/imgcache/negative_cache_internal_test.go @@ -15,7 +15,6 @@ func TestNegativeCache_StoreAndCheck(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: dir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { @@ -63,7 +62,6 @@ func TestNegativeCache_Expired(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: dir, - CacheTTL: time.Hour, NegativeTTL: 1 * time.Millisecond, // very short TTL }) if err != nil { diff --git a/internal/imgcache/service_internal_test.go b/internal/imgcache/service_internal_test.go index 5607853..cec1aa6 100644 --- a/internal/imgcache/service_internal_test.go +++ b/internal/imgcache/service_internal_test.go @@ -430,6 +430,64 @@ func TestService_Get_Caching(t *testing.T) { } } +// TestService_Get_ServesSourceAndVariantCachedTwoDaysAgo caches a source and +// a variant of it, sets their stored fetch and creation times back two days, +// and checks that the variant is still served from the cache and that a new +// variant is made from the cached source without fetching it again. +func TestService_Get_ServesSourceAndVariantCachedTwoDaysAgo(t *testing.T) { + t.Parallel() + + svc, fixtures := SetupTestService(t) + + resp, err := svc.Get(t.Context(), photoVariant(fixtures, 85, FitCover)) + if err != nil { + t.Fatalf("Get() first request error = %v", err) + } + + _ = resp.Content.Close() + + for _, statement := range []string{ + `UPDATE source_metadata SET fetched_at = datetime('now', '-2 days')`, + `UPDATE source_content SET fetched_at = datetime('now', '-2 days')`, + `UPDATE variant_content SET created_at = datetime('now', '-2 days')`, + } { + result, err := svc.cache.db.ExecContext(t.Context(), statement) + if err != nil { + t.Fatalf("%s: %v", statement, err) + } + + rows, err := result.RowsAffected() + if err != nil || rows != 1 { + t.Fatalf("%s: rows = %d, error = %v, want 1 row", statement, rows, err) + } + } + + resp, err = svc.Get(t.Context(), photoVariant(fixtures, 85, FitCover)) + if err != nil { + t.Fatalf("Get() of the cached variant error = %v", err) + } + + _ = resp.Content.Close() + + if resp.CacheStatus != CacheHit { + t.Errorf("cached variant CacheStatus = %q, want %q", + resp.CacheStatus, CacheHit) + } + + // Another quality is a new variant, made from the cached source + resp, err = svc.Get(t.Context(), photoVariant(fixtures, 40, FitCover)) + if err != nil { + t.Fatalf("Get() of a new variant error = %v", err) + } + + _ = resp.Content.Close() + + fetches := readCacheStatsCounters(t, svc.cache).upstreamFetchCount + if fetches != 1 { + t.Errorf("upstream fetches = %d, want 1", fetches) + } +} + func TestService_Get_DifferentSizes(t *testing.T) { t.Parallel() diff --git a/internal/imgcache/stats_internal_test.go b/internal/imgcache/stats_internal_test.go index 94ee241..2fe9dbb 100644 --- a/internal/imgcache/stats_internal_test.go +++ b/internal/imgcache/stats_internal_test.go @@ -43,7 +43,6 @@ func TestStats_HitRateIsRatio(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: dir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { @@ -91,7 +90,6 @@ func TestStats_ZeroCounts(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: dir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil { @@ -120,7 +118,6 @@ func TestStats_LogsFailedCountQueries(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)), }) @@ -162,7 +159,6 @@ func TestIncrementStats_LogsFailedUpdates(t *testing.T) { cache, err := NewCache(db, CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)), }) @@ -232,7 +228,6 @@ func TestStats_DisabledCacheReportsNoItems(t *testing.T) { disabled, err := NewCache(enabled.db, CacheConfig{ StateDir: t.TempDir(), - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, DisableDiskCache: true, }) diff --git a/internal/imgcache/testutil_internal_test.go b/internal/imgcache/testutil_internal_test.go index b29f750..326537f 100644 --- a/internal/imgcache/testutil_internal_test.go +++ b/internal/imgcache/testutil_internal_test.go @@ -185,7 +185,6 @@ func SetupTestService( cache, err := NewCache(db, CacheConfig{ StateDir: tmpDir, - CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, }) if err != nil {