1 Commits
Author SHA1 Message Date
clawbot ef70bb6fa0 Split internal/imgcache into the image proxy and the cache (closes #317)
check / check (push) Waiting to run
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
package `internal/imageproxy` fetches, transforms, caches and serves images.
It imports `imgcache`, and `imgcache` does not import it.
- `imgcache.Service` is `imageproxy.Proxy`, built by `imageproxy.NewProxy`
from an `imageproxy.Config`.
- `imgcache.Service` is `imageproxy.Proxy`, built by `imageproxy.New` from
an `imageproxy.Config`.
- `ImageResponse`, `CacheStatus`, `ErrNegativeCached` and
`ErrProcessingTimedOut` moved with it, under their names.
- `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
background writer. A cache hit still makes no database call.
- `imgcache.go` and `module.go` are gone.
- `NewProxy` makes the counters of upstream fetches and transforms, and
registers them on the registry it is given. The cache keeps only the hit
and miss counters.
- `imageproxy.New` makes the counters of upstream fetches and transforms,
and registers them on the registry it is given. The cache keeps only the
hit and miss counters.
- A cached source whose row cannot be read is logged as "failed to load
cached source, fetching". It was logged as "source lookup failed".
- 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)
}
svc, err := imageproxy.NewProxy(&imageproxy.Config{
svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache,
Fetcher: newMockFetcher(fstest.MapFS{
countingHost + photoPath: &fstest.MapFile{Data: photo.Bytes()},
@@ -145,7 +145,7 @@ func newCountingDatabaseServer(
Logger: slog.New(slog.DiscardHandler),
}, prometheus.NewRegistry())
if err != nil {
tb.Fatalf("imageproxy.NewProxy() error = %v", err)
tb.Fatalf("imageproxy.New() error = %v", err)
}
keys, err := seal.DeriveKeys(testSigningKey)
@@ -48,7 +48,7 @@ func newRoutesFetchingThrough(
t.Fatalf("imgcache.NewCache() error = %v", err)
}
svc, err := imageproxy.NewProxy(&imageproxy.Config{
svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache,
Fetcher: fetcher,
Processor: imageprocessor.New(imageprocessor.Params{
@@ -59,7 +59,7 @@ func newRoutesFetchingThrough(
Logger: log,
}, prometheus.NewRegistry())
if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err)
t.Fatalf("imageproxy.New() error = %v", err)
}
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)
}
svc, err := imageproxy.NewProxy(&imageproxy.Config{
svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache,
Fetcher: newMockFetcher(mockFS),
Processor: imageprocessor.New(imageprocessor.Params{
@@ -47,7 +47,7 @@ func newSignedHostServer(
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,
Fetcher: newMockFetcher(fstest.MapFS{
signedHost + photoPath: &fstest.MapFile{Data: jpegData},
@@ -60,7 +60,7 @@ func newSignedHostServer(
Logger: log,
}, prometheus.NewRegistry())
if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err)
t.Fatalf("imageproxy.New() error = %v", err)
}
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)
}
svc, err := imageproxy.NewProxy(&imageproxy.Config{
svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache,
Fetcher: fetcher,
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)
}
svc, err := imageproxy.NewProxy(&imageproxy.Config{
svc, err := imageproxy.New(&imageproxy.Config{
Cache: cache,
Fetcher: fetcher,
Processor: imageprocessor.New(imageprocessor.Params{
@@ -72,7 +72,7 @@ func newRefererRoutes(t *testing.T) (http.Handler, *Handlers, *countingFetcher)
MaxResponseSize: 50 << 20,
}, prometheus.NewRegistry())
if err != nil {
t.Fatalf("imageproxy.NewProxy() error = %v", err)
t.Fatalf("imageproxy.New() error = %v", err)
}
keys, err := seal.DeriveKeys(testSigningKey)
+15 -16
View File
@@ -9,11 +9,10 @@ import (
"sneak.berlin/go/pixa/internal/imgcache"
)
// TestNewProxy_RegistersItsCountersOnItsRegistry checks that NewProxy
// registers the proxy's counters on the registry it is given, under the
// names README.md gives them. The cache registers its own counters on
// another registry.
func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
// TestNew_RegistersItsCountersOnItsRegistry checks that New registers the
// proxy's counters on the registry it is given, under the names README.md
// gives them. The cache registers its own counters on another registry.
func TestNew_RegistersItsCountersOnItsRegistry(t *testing.T) {
t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t),
@@ -25,9 +24,9 @@ func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
registry := prometheus.NewRegistry()
_, err = NewProxy(&Config{Cache: cache}, registry)
_, err = New(&Config{Cache: cache}, registry)
if err != nil {
t.Fatalf("NewProxy() error = %v", err)
t.Fatalf("New() error = %v", err)
}
names := []string{
@@ -47,10 +46,10 @@ func TestNewProxy_RegistersItsCountersOnItsRegistry(t *testing.T) {
}
}
// TestNewProxy_FailsWhenItsCountersCannotBeRegistered makes a second proxy
// on the registry of a first. Its counters have the same names, so the
// registry refuses them, and NewProxy must return that error.
func TestNewProxy_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
// TestNew_FailsWhenItsCountersCannotBeRegistered makes a second proxy on
// the registry of a first. Its counters have the same names, so the
// registry refuses them, and New must return that error.
func TestNew_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
t.Parallel()
cache, err := imgcache.NewCache(setupTestDB(t),
@@ -62,17 +61,17 @@ func TestNewProxy_FailsWhenItsCountersCannotBeRegistered(t *testing.T) {
registry := prometheus.NewRegistry()
_, err = NewProxy(&Config{Cache: cache}, registry)
_, err = New(&Config{Cache: cache}, registry)
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)
t.Logf("NewProxy() of the second proxy error = %v", err)
_, err = New(&Config{Cache: cache}, registry)
t.Logf("New() of the second proxy error = %v", err)
var alreadyRegistered prometheus.AlreadyRegisteredError
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)
}
}
+3 -3
View File
@@ -37,7 +37,7 @@ type Proxy struct {
variantsInProgress singleflight.Group
// 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
upstreamFetchBytes prometheus.Counter
transforms prometheus.Counter
@@ -61,11 +61,11 @@ type Config struct {
// errCacheRequired is returned when the proxy is built without a cache.
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
// /metrics serves. It fails when metricsRegistry already holds counters of
// the same names.
func NewProxy(
func New(
cfg *Config, metricsRegistry prometheus.Registerer,
) (*Proxy, error) {
if cfg.Cache == nil {
@@ -199,7 +199,7 @@ func setupTestProxy(t *testing.T) (*testProxy, *TestFixtures) {
t.Fatalf("failed to create cache: %v", err)
}
proxy, err := NewProxy(&Config{
proxy, err := New(&Config{
Cache: cache,
Fetcher: httpfetcher.NewMock(mockFS),
Processor: imageprocessor.New(imageprocessor.Params{
+1 -1
View File
@@ -227,7 +227,7 @@ type imageServiceParams struct {
func newImageService(
_ fx.Lifecycle, params imageServiceParams,
) (*imageproxy.Proxy, error) {
return imageproxy.NewProxy(&imageproxy.Config{
return imageproxy.New(&imageproxy.Config{
Cache: params.Cache,
Fetcher: params.Fetcher,
Processor: params.Processor,