One image request type instead of five (closes #307)
check / check (push) Waiting to run

The new package internal/imagerequest holds the request type, its two
parts Source and Transform, the Format and Fit types, the dimension and
fit checks, the /v1/image/ path parsing and the variant key; it imports
no other package of pixa. imgcache.ParsedURL, imgcache.ImageRequest,
signature.Request, imageprocessor.Request, the second Size, format and
fit types and the conversions between them are gone. The image
processor takes a Transform only. encurl keeps a private struct with the
token's CBOR field names, so tokens made before still decode. VariantKey
moves with the function that makes it. Tests follow the moved and
renamed code; the tests of the removed conversions go with them.

Model: opus-5-5
This commit is contained in:
2026-10-21 12:27:29 +00:00
parent 385c0ca76f
commit fece3b69c9
68 changed files with 2217 additions and 1978 deletions
+34 -28
View File
@@ -17,6 +17,7 @@ import (
lru "github.com/hashicorp/golang-lru/v2"
"sneak.berlin/go/pixa/internal/httpfetcher"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// Cache errors.
@@ -111,7 +112,7 @@ type Cache struct {
// metaCache holds the content types of the variants most recently
// stored or served, so a hit does not read the variant's .meta file.
// It never stands in for the variant file, which is always opened.
metaCache *lru.Cache[VariantKey, string]
metaCache *lru.Cache[imagerequest.VariantKey, string]
// contentLocks serializes StoreSource and evictSourceBlob per
// content hash, closing the race window between an eviction's row
@@ -153,7 +154,7 @@ func newCache(
log = slog.Default()
}
metaCache, err := lru.New[VariantKey, string](metaCacheSize)
metaCache, err := lru.New[imagerequest.VariantKey, string](metaCacheSize)
if err != nil {
return nil, fmt.Errorf("failed to create variant content type cache: %w", err)
}
@@ -223,7 +224,7 @@ func newCache(
// LookupResult contains the result of a cache lookup.
type LookupResult struct {
Hit bool
CacheKey VariantKey
CacheKey imagerequest.VariantKey
ContentType string
SizeBytes int64
CacheStatus CacheStatus
@@ -231,8 +232,10 @@ type LookupResult struct {
// Lookup checks if a processed variant exists on disk. 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)
func (c *Cache) Lookup(
ctx context.Context, req *imagerequest.Request,
) (*LookupResult, error) {
cacheKey := imagerequest.CacheKey(req)
// Check variant storage directly - no DB needed for cache hits
if !c.disabled && c.variants.Exists(cacheKey) {
@@ -257,7 +260,9 @@ func (c *Cache) Lookup(ctx context.Context, req *ImageRequest) (*LookupResult, e
// variant's .meta file and is then kept in metaCache. A variant with
// no .meta file is served as application/octet-stream, which is not
// kept.
func (c *Cache) GetVariant(cacheKey VariantKey) (io.ReadCloser, int64, string, error) {
func (c *Cache) GetVariant(
cacheKey imagerequest.VariantKey,
) (io.ReadCloser, int64, string, error) {
if c.disabled {
return nil, 0, "", ErrNotFound
}
@@ -282,7 +287,7 @@ func (c *Cache) GetVariant(cacheKey VariantKey) (io.ReadCloser, int64, string, e
// disabled cache it is a no-op returning an empty hash.
func (c *Cache) StoreSource(
ctx context.Context,
req *ImageRequest,
req *imagerequest.Request,
content io.Reader,
result *httpfetcher.FetchResult,
) (ContentHash, error) {
@@ -322,7 +327,7 @@ func (c *Cache) StoreSource(
}
// Store in database
pathHash := HashPath(req.SourcePath + "?" + req.SourceQuery)
pathHash := HashPath(req.Source.Path + "?" + req.Source.Query)
headersJSON, err := json.Marshal(result.Headers)
if err != nil {
@@ -349,7 +354,7 @@ func (c *Cache) StoreSource(
content_type = excluded.content_type,
response_headers = excluded.response_headers,
fetched_at = CURRENT_TIMESTAMP
`, req.SourceHost, req.SourcePath, req.SourceQuery, pathHash,
`, req.Source.Host, req.Source.Path, req.Source.Query, pathHash,
contentHash, httpStatusOK, result.ContentType, string(headersJSON))
if err != nil {
return "", fmt.Errorf("failed to insert source metadata: %w", err)
@@ -367,7 +372,8 @@ func (c *Cache) StoreSource(
// accounting insert is best-effort (the startup reconciliation pass
// adopts any variant file that misses its accounting row).
func (c *Cache) StoreVariant(
ctx context.Context, cacheKey VariantKey, content io.Reader, contentType string,
ctx context.Context, cacheKey imagerequest.VariantKey,
content io.Reader, contentType string,
) error {
if c.disabled {
return nil
@@ -403,7 +409,7 @@ func (c *Cache) StoreVariant(
// if not. Hits touch the blob's LRU timestamp; a disabled cache always
// reports no cached source.
func (c *Cache) LookupSource(
ctx context.Context, req *ImageRequest,
ctx context.Context, req *imagerequest.Request,
) (ContentHash, string, error) {
if c.disabled {
return "", "", nil
@@ -414,7 +420,7 @@ func (c *Cache) LookupSource(
err := c.db.QueryRowContext(ctx, `
SELECT content_hash, content_type FROM source_metadata
WHERE source_host = ? AND source_path = ? AND source_query = ?
`, req.SourceHost, req.SourcePath, req.SourceQuery).Scan(&hashStr, &contentType)
`, req.Source.Host, req.Source.Path, req.Source.Query).Scan(&hashStr, &contentType)
if errors.Is(err, sql.ErrNoRows) {
return "", "", nil
@@ -438,7 +444,7 @@ func (c *Cache) LookupSource(
// StoreNegative stores a negative cache entry for a failed fetch.
func (c *Cache) StoreNegative(
ctx context.Context, req *ImageRequest, statusCode int, errMsg string,
ctx context.Context, req *imagerequest.Request, statusCode int, errMsg string,
) error {
expiresAt := time.Now().UTC().Add(c.config.NegativeTTL)
@@ -452,7 +458,7 @@ func (c *Cache) StoreNegative(
error_message = excluded.error_message,
fetched_at = CURRENT_TIMESTAMP,
expires_at = excluded.expires_at
`, req.SourceHost, req.SourcePath, req.SourceQuery, statusCode, errMsg, expiresAt)
`, req.Source.Host, req.Source.Path, req.Source.Query, statusCode, errMsg, expiresAt)
if err != nil {
return fmt.Errorf("failed to insert negative cache: %w", err)
}
@@ -462,14 +468,14 @@ func (c *Cache) StoreNegative(
// GetSourceMetadataID returns the source metadata ID for a request.
func (c *Cache) GetSourceMetadataID(
ctx context.Context, req *ImageRequest,
ctx context.Context, req *imagerequest.Request,
) (int64, error) {
var id int64
err := c.db.QueryRowContext(ctx, `
SELECT id FROM source_metadata
WHERE source_host = ? AND source_path = ? AND source_query = ?
`, req.SourceHost, req.SourcePath, req.SourceQuery).Scan(&id)
`, req.Source.Host, req.Source.Path, req.Source.Query).Scan(&id)
if err != nil {
return 0, fmt.Errorf("failed to get source metadata ID: %w", err)
}
@@ -665,7 +671,7 @@ func (c *Cache) readHitAndMissCounts(ctx context.Context, stats *CacheStats) err
// application/octet-stream and keeps nothing, so metaCache only ever
// holds a type read from a .meta file or passed to StoreVariant.
func (c *Cache) loadVariantWithMeta(
cacheKey VariantKey,
cacheKey imagerequest.VariantKey,
) (io.ReadCloser, int64, string, error) {
reader, size, contentType, err := c.variants.LoadWithMeta(cacheKey)
if err != nil {
@@ -685,15 +691,15 @@ func (c *Cache) loadVariantWithMeta(
// A failure is logged and is otherwise non-fatal; the metadata is in the
// database.
func (c *Cache) writeMetadataSidecar(
req *ImageRequest,
req *imagerequest.Request,
pathHash PathHash,
contentHash ContentHash,
result *httpfetcher.FetchResult,
) {
meta := &SourceMetadata{
Host: req.SourceHost,
Path: req.SourcePath,
Query: req.SourceQuery,
Host: req.Source.Host,
Path: req.Source.Path,
Query: req.Source.Query,
ContentHash: string(contentHash),
StatusCode: result.StatusCode,
ContentType: result.ContentType,
@@ -704,16 +710,16 @@ func (c *Cache) writeMetadataSidecar(
RemoteAddr: result.RemoteAddr,
}
err := c.srcMetadata.Store(req.SourceHost, pathHash, meta)
err := c.srcMetadata.Store(req.Source.Host, pathHash, meta)
if err != nil {
c.log.Warn("failed to write metadata sidecar",
"host", req.SourceHost, "path_hash", pathHash, "error", err)
"host", req.Source.Host, "path_hash", pathHash, "error", err)
}
}
// 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) {
func (c *Cache) touchVariant(ctx context.Context, cacheKey imagerequest.VariantKey) {
_, err := c.db.ExecContext(ctx, `
UPDATE variant_content SET last_accessed_at = CURRENT_TIMESTAMP
WHERE cache_key = ?
@@ -739,14 +745,14 @@ func (c *Cache) touchSourceContent(ctx context.Context, contentHash ContentHash)
// checkNegativeCache checks if a request is in the negative cache.
func (c *Cache) checkNegativeCache(
ctx context.Context, req *ImageRequest,
ctx context.Context, req *imagerequest.Request,
) (bool, error) {
var expiresAt time.Time
err := c.db.QueryRowContext(ctx, `
SELECT expires_at FROM negative_cache
WHERE source_host = ? AND source_path = ? AND source_query = ?
`, req.SourceHost, req.SourcePath, req.SourceQuery).Scan(&expiresAt)
`, req.Source.Host, req.Source.Path, req.Source.Query).Scan(&expiresAt)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
@@ -762,10 +768,10 @@ func (c *Cache) checkNegativeCache(
_, err = c.db.ExecContext(ctx, `
DELETE FROM negative_cache
WHERE source_host = ? AND source_path = ? AND source_query = ?
`, req.SourceHost, req.SourcePath, req.SourceQuery)
`, req.Source.Host, req.Source.Path, req.Source.Query)
if err != nil {
c.log.Warn("failed to delete expired negative cache entry",
"host", req.SourceHost, "path", req.SourcePath, "error", err)
"host", req.Source.Host, "path", req.Source.Path, "error", err)
}
return false, nil