1 Commits
Author SHA1 Message Date
clawbot ce4df5077b 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. NewProxy 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:39:15 +00:00
11 changed files with 36 additions and 35 deletions

No files matched your search

+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.New` from - `imgcache.Service` is `imageproxy.Proxy`, built by `imageproxy.NewProxy`
an `imageproxy.Config`. from 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.
- `imageproxy.New` makes the counters of upstream fetches and transforms, - `NewProxy` makes the counters of upstream fetches and transforms, and
and registers them on the registry it is given. The cache keeps only the registers them on the registry it is given. The cache keeps only the hit
hit and miss counters. 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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New() error = %v", err) tb.Fatalf("imageproxy.NewProxy() 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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New() error = %v", err) t.Fatalf("imageproxy.NewProxy() 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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New() error = %v", err) t.Fatalf("imageproxy.NewProxy() 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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New(&imageproxy.Config{ svc, err := imageproxy.NewProxy(&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.New() error = %v", err) t.Fatalf("imageproxy.NewProxy() error = %v", err)
} }
keys, err := seal.DeriveKeys(testSigningKey) keys, err := seal.DeriveKeys(testSigningKey)
+16 -15
View File
@@ -9,10 +9,11 @@ import (
"sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/imgcache"
) )
// TestNew_RegistersItsCountersOnItsRegistry checks that New registers the // TestNewProxy_RegistersItsCountersOnItsRegistry checks that NewProxy
// proxy's counters on the registry it is given, under the names README.md // registers the proxy's counters on the registry it is given, under the
// gives them. The cache registers its own counters on another registry. // names README.md gives them. The cache registers its own counters on
func TestNew_RegistersItsCountersOnItsRegistry(t *testing.T) { // another registry.
func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
t.Parallel() t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t), cache, err := imgcache.NewCache(setupTestDB(t),
@@ -24,9 +25,9 @@ func TestNew_RegistersItsCountersOnItsRegistry(t *testing.T) {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
_, err = New(&Config{Cache: cache}, registry) _, err = NewProxy(&Config{Cache: cache}, registry)
if err != nil { if err != nil {
t.Fatalf("New() error = %v", err) t.Fatalf("NewProxy() error = %v", err)
} }
names := []string{ names := []string{
@@ -46,10 +47,10 @@ func TestNew_RegistersItsCountersOnItsRegistry(t *testing.T) {
} }
} }
// TestNew_FailsWhenItsCountersCannotBeRegistered makes a second proxy on // TestNewProxy_FailsWhenItsCountersCannotBeRegistered makes a second proxy
// the registry of a first. Its counters have the same names, so the // on the registry of a first. Its counters have the same names, so the
// registry refuses them, and New must return that error. // registry refuses them, and NewProxy must return that error.
func TestNew_FailsWhenItsCountersCannotBeRegistered(t *testing.T) { func TestNewProxy_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
t.Parallel() t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t), cache, err := imgcache.NewCache(setupTestDB(t),
@@ -61,17 +62,17 @@ func TestNew_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
_, err = New(&Config{Cache: cache}, registry) _, err = NewProxy(&Config{Cache: cache}, registry)
if err != nil { if err != nil {
t.Fatalf("New() of the first proxy error = %v", err) t.Fatalf("NewProxy() of the first proxy error = %v", err)
} }
_, err = New(&Config{Cache: cache}, registry) _, err = NewProxy(&Config{Cache: cache}, registry)
t.Logf("New() of the second proxy error = %v", err) t.Logf("NewProxy() 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("New() of the second proxy error = %v, want %T", t.Errorf("NewProxy() 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
// New. They are kept in memory, so adding to one waits for nothing. // NewProxy. 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")
// New creates a new image proxy and registers its counters on // NewProxy 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 New( func NewProxy(
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 := New(&Config{ proxy, err := NewProxy(&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.New(&imageproxy.Config{ return imageproxy.NewProxy(&imageproxy.Config{
Cache: params.Cache, Cache: params.Cache,
Fetcher: params.Fetcher, Fetcher: params.Fetcher,
Processor: params.Processor, Processor: params.Processor,