check / check (push) Waiting to run
UsageBytes now reads the new cache_usage row, which triggers on source_content and variant_content keep up to date in the statement that adds, removes or resizes a row. The reconciliation pass reads both tables 1000 rows per query, sums them, and corrects the total when it differs, unless a row changed while it summed. Source rows now get last_accessed_at when added, so choosing source images to evict reads that column's index instead of sorting the whole table. Stats still sums the tables, now in pages: an existing test drops both tables and expects that sum to fail. Model: opus-5-5
266 lines
7.2 KiB
Go
266 lines
7.2 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestEvictionReadsTheUsageTotal adds, stores again, resizes and evicts
|
|
// cache content, then drops both content tables. UsageBytes must still
|
|
// report what the tables held, and an eviction pass under the limit must
|
|
// still succeed: neither may sum the tables.
|
|
func TestEvictionReadsTheUsageTotal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
ctx := t.Context()
|
|
|
|
kept := storeEvictionTestSource(t, cache, "total.example.com", "/kept.jpg",
|
|
bytes.Repeat([]byte{0x71}, 1000))
|
|
evicted := storeEvictionTestSource(t, cache, "total.example.com", "/evicted.jpg",
|
|
bytes.Repeat([]byte{0x72}, 700))
|
|
|
|
storeEvictionTestVariant(t, cache, testVariantKeyOne,
|
|
bytes.Repeat([]byte{0x73}, 500))
|
|
storeEvictionTestVariant(t, cache, testVariantKeyOne,
|
|
bytes.Repeat([]byte{0x74}, 300))
|
|
storeEvictionTestVariant(t, cache, testVariantKeyTwo,
|
|
bytes.Repeat([]byte{0x75}, 200))
|
|
|
|
// pixa never changes a source's size, but a statement that does must
|
|
// change the total too.
|
|
_, err := cache.db.ExecContext(ctx,
|
|
`UPDATE source_content SET size_bytes = 900 WHERE content_hash = ?`,
|
|
string(kept))
|
|
if err != nil {
|
|
t.Fatalf("failed to change the source's size: %v", err)
|
|
}
|
|
|
|
err = cache.evictSourceBlob(ctx, evicted)
|
|
if err != nil {
|
|
t.Fatalf("evictSourceBlob failed: %v", err)
|
|
}
|
|
|
|
err = cache.evictVariant(ctx, testVariantKeyTwo)
|
|
if err != nil {
|
|
t.Fatalf("evictVariant failed: %v", err)
|
|
}
|
|
|
|
_, err = cache.db.ExecContext(ctx,
|
|
`DROP TABLE source_content; DROP TABLE variant_content`)
|
|
if err != nil {
|
|
t.Fatalf("failed to drop the content tables: %v", err)
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(ctx)
|
|
t.Logf("UsageBytes() = %d, error = %v", usage, err)
|
|
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes() error = %v, want nil: it read a content table", err)
|
|
}
|
|
|
|
if usage != 1200 {
|
|
t.Errorf("UsageBytes() = %d, want 1200 (900 + 300)", usage)
|
|
}
|
|
|
|
err = cache.EvictToLimit(ctx)
|
|
if err != nil {
|
|
t.Errorf("EvictToLimit() error = %v, want nil: it read a content table", err)
|
|
}
|
|
}
|
|
|
|
// TestReconciliationCorrectsTheUsageTotal sets the total cache usage to a
|
|
// wrong value and checks that a reconciliation pass, which sums both
|
|
// content tables, puts it right.
|
|
func TestReconciliationCorrectsTheUsageTotal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
ctx := t.Context()
|
|
|
|
storeEvictionTestSource(t, cache, "total.example.com", "/a.jpg",
|
|
bytes.Repeat([]byte{0x76}, 1000))
|
|
storeEvictionTestVariant(t, cache, testVariantKeyOne,
|
|
bytes.Repeat([]byte{0x77}, 500))
|
|
|
|
_, err := cache.db.ExecContext(ctx,
|
|
`UPDATE cache_usage SET total_size_bytes = 1 WHERE id = 1`)
|
|
if err != nil {
|
|
t.Fatalf("failed to set a wrong total: %v", err)
|
|
}
|
|
|
|
err = cache.reconcileAccounting(ctx)
|
|
if err != nil {
|
|
t.Fatalf("reconcileAccounting failed: %v", err)
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes failed: %v", err)
|
|
}
|
|
|
|
if usage != 1500 {
|
|
t.Errorf("UsageBytes() after reconciliation = %d, want 1500 (1000 + 500)",
|
|
usage)
|
|
}
|
|
}
|
|
|
|
// TestUsageTotalNotCorrectedFromAnOlderSum stores a variant after the
|
|
// change count was read, then offers a sum taken before the store as the
|
|
// correction: the total must keep the stored variant, as that sum may
|
|
// have missed it.
|
|
func TestUsageTotalNotCorrectedFromAnOlderSum(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
ctx := t.Context()
|
|
|
|
var changeCount int64
|
|
|
|
err := cache.db.QueryRowContext(ctx,
|
|
`SELECT change_count FROM cache_usage WHERE id = 1`,
|
|
).Scan(&changeCount)
|
|
if err != nil {
|
|
t.Fatalf("failed to read the change count: %v", err)
|
|
}
|
|
|
|
storeEvictionTestVariant(t, cache, testVariantKeyOne,
|
|
bytes.Repeat([]byte{0x78}, 500))
|
|
|
|
corrected, err := cache.correctUsageTotal(ctx, 0, changeCount)
|
|
if err != nil {
|
|
t.Fatalf("correctUsageTotal failed: %v", err)
|
|
}
|
|
|
|
if corrected {
|
|
t.Error("correctUsageTotal() = true, want false: content changed since the sum")
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes failed: %v", err)
|
|
}
|
|
|
|
if usage != 500 {
|
|
t.Errorf("UsageBytes() = %d, want 500", usage)
|
|
}
|
|
}
|
|
|
|
// TestReconciliationReadsAPageAtATime stores five source images and five
|
|
// variants, sets the page size to two rows and runs a reconciliation
|
|
// pass. No read the pass makes of a content table, to check its rows or
|
|
// to sum them, may cover more than two rows, so a request's query waits
|
|
// for one page at most. Between them the reads must still cover every
|
|
// row of both tables twice, once to check it and once to sum it, and the
|
|
// sum must put a wrong total right.
|
|
func TestReconciliationReadsAPageAtATime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
ctx := t.Context()
|
|
|
|
const pageSize = 2
|
|
|
|
cache.reconciliationPageSize = pageSize
|
|
|
|
// Sources of 100 bytes and variants of 10, five of each.
|
|
for i := range 5 {
|
|
content := []byte(strconv.Itoa(i))
|
|
|
|
storeEvictionTestSource(t, cache, "pages.example.com",
|
|
"/"+strconv.Itoa(i)+".jpg", bytes.Repeat(content, 100))
|
|
storeEvictionTestVariant(t, cache, VariantKey("aabbccdd000"+strconv.Itoa(i)),
|
|
bytes.Repeat(content, 10))
|
|
}
|
|
|
|
_, err := cache.db.ExecContext(ctx,
|
|
`UPDATE cache_usage SET total_size_bytes = 1 WHERE id = 1`)
|
|
if err != nil {
|
|
t.Fatalf("failed to set a wrong total: %v", err)
|
|
}
|
|
|
|
var rowsPerRead []int
|
|
|
|
cache.reconciliationReadTestHook = func(rows int) {
|
|
rowsPerRead = append(rowsPerRead, rows)
|
|
}
|
|
|
|
err = cache.reconcileAccounting(ctx)
|
|
if err != nil {
|
|
t.Fatalf("reconcileAccounting failed: %v", err)
|
|
}
|
|
|
|
t.Logf("rows covered by each read, in order: %v", rowsPerRead)
|
|
|
|
coveredRows := 0
|
|
|
|
for _, rows := range rowsPerRead {
|
|
if rows > pageSize {
|
|
t.Errorf("a read covered %d rows, want at most %d", rows, pageSize)
|
|
}
|
|
|
|
coveredRows += rows
|
|
}
|
|
|
|
// Ten rows, each read once to check it and once to sum it.
|
|
if coveredRows != 20 {
|
|
t.Errorf("the reads covered %d rows in all, want 20", coveredRows)
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes failed: %v", err)
|
|
}
|
|
|
|
if usage != 550 {
|
|
t.Errorf("UsageBytes() after reconciliation = %d, want 550 (5*100 + 5*10)",
|
|
usage)
|
|
}
|
|
}
|
|
|
|
// TestSourceEvictionCandidatesComeFromTheIndex checks that the query
|
|
// choosing source images to evict reads source_content in last access
|
|
// order from its index, instead of reading and sorting the whole table.
|
|
func TestSourceEvictionCandidatesComeFromTheIndex(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
rows, err := cache.db.QueryContext(t.Context(),
|
|
`EXPLAIN QUERY PLAN `+sourceCandidatesQuery, evictionBatchSize)
|
|
if err != nil {
|
|
t.Fatalf("EXPLAIN QUERY PLAN failed: %v", err)
|
|
}
|
|
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var plan []string
|
|
|
|
for rows.Next() {
|
|
var id, parent, unused int
|
|
|
|
var detail string
|
|
|
|
err := rows.Scan(&id, &parent, &unused, &detail)
|
|
if err != nil {
|
|
t.Fatalf("failed to scan the query plan: %v", err)
|
|
}
|
|
|
|
plan = append(plan, detail)
|
|
}
|
|
|
|
err = rows.Err()
|
|
if err != nil {
|
|
t.Fatalf("query plan iteration failed: %v", err)
|
|
}
|
|
|
|
t.Logf("query plan: %q", plan)
|
|
|
|
if !strings.Contains(strings.Join(plan, "\n"), "idx_source_content_last_accessed") {
|
|
t.Errorf("the source candidate query does not use "+
|
|
"idx_source_content_last_accessed; plan: %q", plan)
|
|
}
|
|
}
|