Sync cached files and refuse ones left empty or partial (closes #252)

Source and variant files are synced to disk before the rename into
place, and their directory after. Lookup and LookupSource compare the
file's size with the size its row records: a file that is empty or of
another size is removed with its rows and a warning naming it, and the
request converts the image or fetches the source again. A variant whose
row cannot be read is judged by its file alone. Storing a source over a
file of another size replaces it. A source that fails to decode is not
removed for that alone.

#252

Model: opus-5-5
This commit is contained in:
2026-10-15 04:36:05 +00:00
parent a1fc42b80e
commit 36a4ce22c3
5 changed files with 168 additions and 21 deletions
+73 -11
View File
@@ -222,13 +222,13 @@ type LookupResult struct {
CacheStatus CacheStatus
}
// Lookup checks if a processed variant exists on disk. Hits touch the
// variant's LRU timestamp; a disabled cache always misses.
// Lookup checks if a processed variant is on disk whole (see
// variantFileIsWhole). Hits touch the variant's LRU timestamp; a disabled
// cache always misses.
func (c *Cache) Lookup(ctx context.Context, req *ImageRequest) (*LookupResult, error) {
cacheKey := CacheKey(req)
// Check variant storage directly - no DB needed for cache hits
if !c.disabled && c.variants.Exists(cacheKey) {
if !c.disabled && c.variantFileIsWhole(ctx, cacheKey) {
c.touchVariant(ctx, cacheKey)
return &LookupResult{
@@ -393,8 +393,11 @@ func (c *Cache) StoreVariant(
// LookupSource checks if we have cached source content for a request.
// Returns the content hash and content type if found, or empty values
// if not. Hits touch the blob's LRU timestamp; a disabled cache always
// reports no cached source.
// if not. A content file that is empty, or not the size its
// source_content row records, is damaged: it is removed with its rows,
// and no cached source is reported, so the source is fetched again. Hits
// touch the blob's LRU timestamp; a disabled cache always reports no
// cached source.
func (c *Cache) LookupSource(
ctx context.Context, req *ImageRequest,
) (ContentHash, string, error) {
@@ -402,12 +405,18 @@ func (c *Cache) LookupSource(
return "", "", nil
}
var hashStr, contentType string
var (
hashStr, contentType string
recordedSize int64
)
err := c.db.QueryRowContext(ctx, `
SELECT content_hash, content_type FROM source_metadata
SELECT source_metadata.content_hash, source_metadata.content_type,
source_content.size_bytes
FROM source_metadata JOIN source_content USING (content_hash)
WHERE source_host = ? AND source_path = ? AND source_query = ?
`, req.SourceHost, req.SourcePath, req.SourceQuery).Scan(&hashStr, &contentType)
`, req.SourceHost, req.SourcePath, req.SourceQuery).
Scan(&hashStr, &contentType, &recordedSize)
if errors.Is(err, sql.ErrNoRows) {
return "", "", nil
@@ -419,8 +428,24 @@ func (c *Cache) LookupSource(
contentHash := ContentHash(hashStr)
// Verify the content file exists
if !c.srcContent.Exists(contentHash) {
fileSize, err := c.srcContent.Size(contentHash)
if errors.Is(err, ErrNotFound) {
return "", "", nil
}
if err != nil {
return "", "", err
}
if fileSize == 0 || fileSize != recordedSize {
c.log.Warn("removing damaged cached source", "content_hash", contentHash,
"file_bytes", fileSize, "recorded_bytes", recordedSize)
err = c.evictSourceBlob(ctx, contentHash)
if err != nil {
return "", "", fmt.Errorf("failed to remove damaged source: %w", err)
}
return "", "", nil
}
@@ -694,6 +719,43 @@ func (c *Cache) writeMetadataSidecar(
}
}
// variantFileIsWhole reports whether the variant file of cacheKey is on
// disk with the size its variant_content row records. A file that is
// empty or of another size is damaged: it is removed with its row, so the
// variant is converted again. A variant whose row cannot be read, as
// between StoreVariant's writing of its file and of its row, is judged
// by its file alone.
func (c *Cache) variantFileIsWhole(ctx context.Context, cacheKey VariantKey) bool {
fileSize, err := c.variants.Size(cacheKey)
if err != nil {
return false
}
var recordedSize int64
err = c.db.QueryRowContext(ctx,
`SELECT size_bytes FROM variant_content WHERE cache_key = ?`,
string(cacheKey)).Scan(&recordedSize)
if err != nil {
recordedSize = fileSize
}
if fileSize > 0 && fileSize == recordedSize {
return true
}
c.log.Warn("removing damaged cached variant", "cache_key", cacheKey,
"file_bytes", fileSize, "recorded_bytes", recordedSize)
err = c.evictVariant(ctx, cacheKey)
if err != nil {
c.log.Warn("failed to remove damaged cached variant",
"cache_key", cacheKey, "error", err)
}
return false
}
// touchVariant updates the LRU timestamp of a variant, best-effort:
// a failed touch only makes the entry look colder to eviction.
func (c *Cache) touchVariant(ctx context.Context, cacheKey VariantKey) {