Test that expired negative cache rows are deleted in batches
One statement deletes one batch and no more, CleanExpired removes the rest, and it deletes nothing once its context has ended. Model: opus-5-5
This commit is contained in:
@@ -3,6 +3,7 @@ package imgcache
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -185,6 +186,120 @@ func negativeCacheRowCount(tb testing.TB, cache *Cache, sourcePath string) int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCleanExpiredDeletesInBatches stores one expired negative cache row
|
||||||
|
// more than one batch holds, and one row that has not expired. One
|
||||||
|
// statement must delete one batch and no more, so a request waits behind
|
||||||
|
// one batch at most; CleanExpired must then remove the expired row left
|
||||||
|
// over and keep the row that has not expired.
|
||||||
|
func TestCleanExpiredDeletesInBatches(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cache, err := NewCache(evictionTestDB(t), CacheConfig{StateDir: t.TempDir()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
insertExpiredNegativeCacheRows(t, cache, cleanExpiredBatchSize+1)
|
||||||
|
|
||||||
|
const notExpiredPath = "/later.jpg"
|
||||||
|
|
||||||
|
_, err = cache.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO negative_cache
|
||||||
|
(source_host, source_path, status_code, expires_at)
|
||||||
|
VALUES (?, ?, 404, datetime('now', '+1 hour'))
|
||||||
|
`, testHostExample, notExpiredPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to insert a row that has not expired: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := cache.cleanExpiredBatch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cleanExpiredBatch failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("one statement deleted %d of %d expired rows",
|
||||||
|
deleted, cleanExpiredBatchSize+1)
|
||||||
|
|
||||||
|
if deleted != cleanExpiredBatchSize {
|
||||||
|
t.Errorf("one statement deleted %d rows, want %d",
|
||||||
|
deleted, cleanExpiredBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cache.CleanExpired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CleanExpired failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count := negativeCacheTableRowCount(t, cache); count != 1 {
|
||||||
|
t.Errorf("negative_cache holds %d rows after CleanExpired, want 1", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if negativeCacheRowCount(t, cache, notExpiredPath) != 1 {
|
||||||
|
t.Error("the row that has not expired was removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCleanExpiredStopsOnceContextEnds runs CleanExpired with a context
|
||||||
|
// that has already ended: it must return the context's error and delete
|
||||||
|
// no row.
|
||||||
|
func TestCleanExpiredStopsOnceContextEnds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cache, err := NewCache(evictionTestDB(t), CacheConfig{StateDir: t.TempDir()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertExpiredNegativeCacheRows(t, cache, 1)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err = cache.CleanExpired(ctx)
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Errorf("CleanExpired() error = %v, want context.Canceled", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count := negativeCacheTableRowCount(t, cache); count != 1 {
|
||||||
|
t.Errorf("negative_cache holds %d rows, want the expired row kept", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertExpiredNegativeCacheRows inserts count negative_cache rows that
|
||||||
|
// expired an hour ago, each for a different source path.
|
||||||
|
func insertExpiredNegativeCacheRows(t *testing.T, cache *Cache, count int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for i := range count {
|
||||||
|
_, err := cache.db.ExecContext(t.Context(), `
|
||||||
|
INSERT INTO negative_cache
|
||||||
|
(source_host, source_path, status_code, expires_at)
|
||||||
|
VALUES (?, ?, 404, datetime('now', '-1 hour'))
|
||||||
|
`, testHostExample, "/expired/"+strconv.Itoa(i)+".jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to insert an expired row: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// negativeCacheTableRowCount returns how many rows negative_cache holds.
|
||||||
|
func negativeCacheTableRowCount(tb testing.TB, cache *Cache) int {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
|
||||||
|
err := cache.db.QueryRowContext(tb.Context(),
|
||||||
|
`SELECT COUNT(*) FROM negative_cache`,
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatalf("failed to count negative_cache rows: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func TestService_Get_ReturnsErrorForNegativeCachedURL(t *testing.T) {
|
func TestService_Get_ReturnsErrorForNegativeCachedURL(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user