1 Commits
Author SHA1 Message Date
clawbot ef70bb6fa0 Split internal/imgcache into the image proxy and the cache (closes #317)
check / check (push) Canceled after 0s
The new package internal/imageproxy holds Proxy, which was imgcache.Service,
with its Config, ImageResponse, CacheStatus and errors. imageproxy.New makes
and registers the counters of upstream fetches and transforms.
internal/imgcache holds the cache only, with its hit and miss counters, and
imgcache.CacheConfig is imgcache.Config. The cache reads and writes each kind
it keeps with one pair of methods: Variant and PutVariant, Source and
PutSource, Failure and PutFailure. Source replaces LookupSource followed by
GetSourceContent. imgcache.go and module.go are gone. The proxy's tests moved
with it. The cache's tests check that the reconciliation pass removes an empty
or cut file and its row. The proxy's tests remove them by hand, then check
that Get fetches or converts again.

Model: opus-5-5
2026-10-26 18:51:01 +00:00
11 changed files with 35 additions and 36 deletions
+5 -5
View File
@@ -33,8 +33,8 @@ P2: security: per-origin rate limiting
- 2026-10-26 `internal/imgcache` holds the cache only (closes #317). The new - 2026-10-26 `internal/imgcache` holds the cache only (closes #317). The new
package `internal/imageproxy` fetches, transforms, caches and serves images. package `internal/imageproxy` fetches, transforms, caches and serves images.
It imports `imgcache`, and `imgcache` does not import it. It imports `imgcache`, and `imgcache` does not import it.
- `imgcache.Service` is `imageproxy.Proxy`, built by `imageproxy.NewProxy` - `imgcache.Service` is `imageproxy.Proxy`, built by `imageproxy.New` from
from an `imageproxy.Config`. an `imageproxy.Config`.
- `ImageResponse`, `CacheStatus`, `ErrNegativeCached` and - `ImageResponse`, `CacheStatus`, `ErrNegativeCached` and
`ErrProcessingTimedOut` moved with it, under their names. `ErrProcessingTimedOut` moved with it, under their names.
- `imgcache.CacheConfig` is `imgcache.Config`. - `imgcache.CacheConfig` is `imgcache.Config`.
@@ -49,9 +49,9 @@ P2: security: per-origin rate limiting
- `Variant` and `Source` each open the file and hand its access time to the - `Variant` and `Source` each open the file and hand its access time to the
background writer. A cache hit still makes no database call. background writer. A cache hit still makes no database call.
- `imgcache.go` and `module.go` are gone. - `imgcache.go` and `module.go` are gone.
- `NewProxy` makes the counters of upstream fetches and transforms, and - `imageproxy.New` makes the counters of upstream fetches and transforms,
registers them on the registry it is given. The cache keeps only the hit and registers them on the registry it is given. The cache keeps only the
and miss counters. hit and miss counters.
- A cached source whose row cannot be read is logged as "failed to load - A cached source whose row cannot be read is logged as "failed to load
cached source, fetching". It was logged as "source lookup failed". cached source, fetching". It was logged as "source lookup failed".
- The proxy's tests moved to `internal/imageproxy`. They reach the cache - The proxy's tests moved to `internal/imageproxy`. They reach the cache
+2 -2
View File
@@ -132,7 +132,7 @@ func newCountingDatabaseServer(
tb.Fatalf("imgcache.NewCache() error = %v", err) tb.Fatalf("imgcache.NewCache() error = %v", err)
} }
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: newMockFetcher(fstest.MapFS{ Fetcher: newMockFetcher(fstest.MapFS{
countingHost + photoPath: &fstest.MapFile{Data: photo.Bytes()}, countingHost + photoPath: &fstest.MapFile{Data: photo.Bytes()},
@@ -145,7 +145,7 @@ func newCountingDatabaseServer(
Logger: slog.New(slog.DiscardHandler), Logger: slog.New(slog.DiscardHandler),
}, prometheus.NewRegistry()) }, prometheus.NewRegistry())
if err != nil { if err != nil {
tb.Fatalf("imageproxy.NewProxy() error = %v", err) tb.Fatalf("imageproxy.New() error = %v", err)
} }
keys, err := seal.DeriveKeys(testSigningKey) keys, err := seal.DeriveKeys(testSigningKey)
@@ -48,7 +48,7 @@ func newRoutesFetchingThrough(
t.Fatalf("imgcache.NewCache() error = %v", err) t.Fatalf("imgcache.NewCache() error = %v", err)
} }
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: fetcher, Fetcher: fetcher,
Processor: imageprocessor.New(imageprocessor.Params{ Processor: imageprocessor.New(imageprocessor.Params{
@@ -59,7 +59,7 @@ func newRoutesFetchingThrough(
Logger: log, Logger: log,
}, prometheus.NewRegistry()) }, prometheus.NewRegistry())
if err != nil { if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err) t.Fatalf("imageproxy.New() error = %v", err)
} }
keys, err := seal.DeriveKeys(testSigningKey) keys, err := seal.DeriveKeys(testSigningKey)
+1 -1
View File
@@ -59,7 +59,7 @@ func setupTestHandler(t *testing.T) *testFixtures {
t.Fatalf("failed to create cache: %v", err) t.Fatalf("failed to create cache: %v", err)
} }
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: newMockFetcher(mockFS), Fetcher: newMockFetcher(mockFS),
Processor: imageprocessor.New(imageprocessor.Params{ Processor: imageprocessor.New(imageprocessor.Params{
@@ -47,7 +47,7 @@ func newSignedHostServer(
jpegData := generateTestJPEG(t, 100, 100, color.RGBA{255, 0, 0, 255}) jpegData := generateTestJPEG(t, 100, 100, color.RGBA{255, 0, 0, 255})
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: newMockFetcher(fstest.MapFS{ Fetcher: newMockFetcher(fstest.MapFS{
signedHost + photoPath: &fstest.MapFile{Data: jpegData}, signedHost + photoPath: &fstest.MapFile{Data: jpegData},
@@ -60,7 +60,7 @@ func newSignedHostServer(
Logger: log, Logger: log,
}, prometheus.NewRegistry()) }, prometheus.NewRegistry())
if err != nil { if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err) t.Fatalf("imageproxy.New() error = %v", err)
} }
keys, err := seal.DeriveKeys(testSigningKey) keys, err := seal.DeriveKeys(testSigningKey)
@@ -46,7 +46,7 @@ func newImageRoute(t *testing.T, fetcher httpfetcher.Fetcher) http.Handler {
t.Fatalf("failed to create cache: %v", err) t.Fatalf("failed to create cache: %v", err)
} }
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: fetcher, Fetcher: fetcher,
Processor: imageprocessor.New(imageprocessor.Params{ Processor: imageprocessor.New(imageprocessor.Params{
@@ -62,7 +62,7 @@ func newRefererRoutes(t *testing.T) (http.Handler, *Handlers, *countingFetcher)
t.Fatalf("imgcache.NewCache() error = %v", err) t.Fatalf("imgcache.NewCache() error = %v", err)
} }
svc, err := imageproxy.NewProxy(&imageproxy.Config{ svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache, Cache: cache,
Fetcher: fetcher, Fetcher: fetcher,
Processor: imageprocessor.New(imageprocessor.Params{ Processor: imageprocessor.New(imageprocessor.Params{
@@ -72,7 +72,7 @@ func newRefererRoutes(t *testing.T) (http.Handler, *Handlers, *countingFetcher)
MaxResponseSize: 50 << 20, MaxResponseSize: 50 << 20,
}, prometheus.NewRegistry()) }, prometheus.NewRegistry())
if err != nil { if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err) t.Fatalf("imageproxy.New() error = %v", err)
} }
keys, err := seal.DeriveKeys(testSigningKey) keys, err := seal.DeriveKeys(testSigningKey)
+15 -16
View File
@@ -9,11 +9,10 @@ import (
"sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/imgcache"
) )
// TestNewProxy_RegistersItsCountersOnItsRegistry checks that NewProxy // TestNew_RegistersItsCountersOnItsRegistry checks that New registers the
// registers the proxy's counters on the registry it is given, under the // proxy's counters on the registry it is given, under the names README.md
// names README.md gives them. The cache registers its own counters on // gives them. The cache registers its own counters on another registry.
// another registry. func TestNew_RegistersItsCountersOnItsRegistry(t *testing.T) {
func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
t.Parallel() t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t), cache, err := imgcache.NewCache(setupTestDB(t),
@@ -25,9 +24,9 @@ func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
_, err = NewProxy(&Config{Cache: cache}, registry) _, err = New(&Config{Cache: cache}, registry)
if err != nil { if err != nil {
t.Fatalf("NewProxy() error = %v", err) t.Fatalf("New() error = %v", err)
} }
names := []string{ names := []string{
@@ -47,10 +46,10 @@ func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
} }
} }
// TestNewProxy_FailsWhenItsCountersCannotBeRegistered makes a second proxy // TestNew_FailsWhenItsCountersCannotBeRegistered makes a second proxy on
// on the registry of a first. Its counters have the same names, so the // the registry of a first. Its counters have the same names, so the
// registry refuses them, and NewProxy must return that error. // registry refuses them, and New must return that error.
func TestNewProxy_FailsWhenItsCountersCannotBeRegistered(t *testing.T) { func TestNew_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
t.Parallel() t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t), cache, err := imgcache.NewCache(setupTestDB(t),
@@ -62,17 +61,17 @@ func TestNewProxy_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
_, err = NewProxy(&Config{Cache: cache}, registry) _, err = New(&Config{Cache: cache}, registry)
if err != nil { if err != nil {
t.Fatalf("NewProxy() of the first proxy error = %v", err) t.Fatalf("New() of the first proxy error = %v", err)
} }
_, err = NewProxy(&Config{Cache: cache}, registry) _, err = New(&Config{Cache: cache}, registry)
t.Logf("NewProxy() of the second proxy error = %v", err) t.Logf("New() of the second proxy error = %v", err)
var alreadyRegistered prometheus.AlreadyRegisteredError var alreadyRegistered prometheus.AlreadyRegisteredError
if !errors.As(err, &alreadyRegistered) { if !errors.As(err, &alreadyRegistered) {
t.Errorf("NewProxy() of the second proxy error = %v, want %T", t.Errorf("New() of the second proxy error = %v, want %T",
err, alreadyRegistered) err, alreadyRegistered)
} }
} }
+3 -3
View File
@@ -37,7 +37,7 @@ type Proxy struct {
variantsInProgress singleflight.Group variantsInProgress singleflight.Group
// The counts of upstream fetches and transforms, made and registered by // The counts of upstream fetches and transforms, made and registered by
// NewProxy. They are kept in memory, so adding to one waits for nothing. // New. They are kept in memory, so adding to one waits for nothing.
upstreamFetches prometheus.Counter upstreamFetches prometheus.Counter
upstreamFetchBytes prometheus.Counter upstreamFetchBytes prometheus.Counter
transforms prometheus.Counter transforms prometheus.Counter
@@ -61,11 +61,11 @@ type Config struct {
// errCacheRequired is returned when the proxy is built without a cache. // errCacheRequired is returned when the proxy is built without a cache.
var errCacheRequired = errors.New("cache is required") var errCacheRequired = errors.New("cache is required")
// NewProxy creates a new image proxy and registers its counters on // New creates a new image proxy and registers its counters on
// metricsRegistry. pixad passes Prometheus' default registry, the one // metricsRegistry. pixad passes Prometheus' default registry, the one
// /metrics serves. It fails when metricsRegistry already holds counters of // /metrics serves. It fails when metricsRegistry already holds counters of
// the same names. // the same names.
func NewProxy( func New(
cfg *Config, metricsRegistry prometheus.Registerer, cfg *Config, metricsRegistry prometheus.Registerer,
) (*Proxy, error) { ) (*Proxy, error) {
if cfg.Cache == nil { if cfg.Cache == nil {
@@ -199,7 +199,7 @@ func setupTestProxy(t *testing.T) (*testProxy, *TestFixtures) {
t.Fatalf("failed to create cache: %v", err) t.Fatalf("failed to create cache: %v", err)
} }
proxy, err := NewProxy(&Config{ proxy, err := New(&Config{
Cache: cache, Cache: cache,
Fetcher: httpfetcher.NewMock(mockFS), Fetcher: httpfetcher.NewMock(mockFS),
Processor: imageprocessor.New(imageprocessor.Params{ Processor: imageprocessor.New(imageprocessor.Params{
+1 -1
View File
@@ -227,7 +227,7 @@ type imageServiceParams struct {
func newImageService( func newImageService(
_ fx.Lifecycle, params imageServiceParams, _ fx.Lifecycle, params imageServiceParams,
) (*imageproxy.Proxy, error) { ) (*imageproxy.Proxy, error) {
return imageproxy.NewProxy(&imageproxy.Config{ return imageproxy.New(&imageproxy.Config{
Cache: params.Cache, Cache: params.Cache,
Fetcher: params.Fetcher, Fetcher: params.Fetcher,
Processor: params.Processor, Processor: params.Processor,