check / check (push) Failing after 1s
The default cache_max_bytes was 75% of the space free at startup. The cache's own files are not free space, so a fuller cache got a smaller limit after a restart and eviction then deleted most of it. The default is now 75% of the sum of the free space and what the cache already holds by its own size accounting, at least 500 MiB. The cache works it out when it opens, after the database is open, so the computation and its tests moved from internal/config to internal/imgcache. The config only records whether cache_max_bytes was set; newCacheConfig in the handlers turns the disk cache off only for an explicit 0, and is tested for an omitted, a zero and a positive value. Model: opus-5-5
182 lines
5.0 KiB
Go
182 lines
5.0 KiB
Go
// Package handlers provides HTTP request handlers.
|
|
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/pixa/internal/config"
|
|
"sneak.berlin/go/pixa/internal/database"
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/healthcheck"
|
|
"sneak.berlin/go/pixa/internal/httpfetcher"
|
|
"sneak.berlin/go/pixa/internal/imgcache"
|
|
"sneak.berlin/go/pixa/internal/logger"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
// Params defines dependencies for Handlers.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Logger *logger.Logger
|
|
Healthcheck *healthcheck.Healthcheck
|
|
Database *database.Database
|
|
Config *config.Config
|
|
}
|
|
|
|
// Handlers provides HTTP request handlers.
|
|
type Handlers struct {
|
|
log *slog.Logger
|
|
hc *healthcheck.Healthcheck
|
|
db *database.Database
|
|
config *config.Config
|
|
imgSvc *imgcache.Service
|
|
imgCache *imgcache.Cache
|
|
sessMgr *session.Manager
|
|
encGen *encurl.Generator
|
|
csrfProtect func(http.Handler) http.Handler
|
|
}
|
|
|
|
// New creates a new Handlers instance.
|
|
func New(lc fx.Lifecycle, params Params) (*Handlers, error) {
|
|
csrfProtect, err := newCSRFProtect(params.Config.SigningKey, params.Config.Debug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &Handlers{
|
|
log: params.Logger.Get(),
|
|
hc: params.Healthcheck,
|
|
db: params.Database,
|
|
config: params.Config,
|
|
csrfProtect: csrfProtect,
|
|
}
|
|
|
|
lc.Append(fx.Hook{
|
|
//nolint:contextcheck // the eviction loop outlives OnStart; OnStop cancels it
|
|
OnStart: func(_ context.Context) error {
|
|
return s.initImageService()
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
if s.imgCache == nil {
|
|
return nil
|
|
}
|
|
|
|
return s.imgCache.StopEviction(ctx)
|
|
},
|
|
})
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// WaitForProcessing waits until no image is being processed, or until ctx
|
|
// ends, and returns how many images were still being processed then.
|
|
func (s *Handlers) WaitForProcessing(ctx context.Context) int {
|
|
return s.imgSvc.WaitForProcessing(ctx)
|
|
}
|
|
|
|
// newCacheConfig builds the image cache's configuration from cfg.
|
|
// cache_max_bytes: 0 disables the disk cache entirely; any other value
|
|
// is the eviction limit in bytes; when it is omitted, the cache works
|
|
// out the default limit itself.
|
|
func newCacheConfig(cfg *config.Config, log *slog.Logger) imgcache.CacheConfig {
|
|
return imgcache.CacheConfig{
|
|
StateDir: cfg.StateDir,
|
|
CacheTTL: imgcache.DefaultCacheTTL,
|
|
NegativeTTL: imgcache.DefaultNegativeTTL,
|
|
MaxBytes: cfg.CacheMaxBytes,
|
|
UseDefaultMaxBytes: !cfg.CacheMaxBytesExplicit,
|
|
DisableDiskCache: cfg.CacheMaxBytesExplicit && cfg.CacheMaxBytes == 0,
|
|
Logger: log,
|
|
}
|
|
}
|
|
|
|
// initImageService initializes the image cache and service.
|
|
func (s *Handlers) initImageService() error {
|
|
cache, err := imgcache.NewCache(s.db.DB(), newCacheConfig(s.config, s.log))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.imgCache = cache
|
|
|
|
// Background eviction: startup reconciliation, then periodic and
|
|
// write-pressure passes. No-op when the disk cache is disabled.
|
|
cache.StartEviction(imgcache.DefaultEvictionInterval)
|
|
|
|
// Create the fetcher config
|
|
fetcherCfg := httpfetcher.DefaultConfig()
|
|
fetcherCfg.AllowHTTP = s.config.AllowHTTP
|
|
fetcherCfg.Timeout = s.config.UpstreamFetchTimeout
|
|
fetcherCfg.MaxResponseSize = s.config.UpstreamMaxResponseSize
|
|
|
|
if s.config.UpstreamConnectionsPerHost > 0 {
|
|
fetcherCfg.MaxConnectionsPerHost = s.config.UpstreamConnectionsPerHost
|
|
}
|
|
|
|
fetcherCfg.MaxConnections = s.config.UpstreamConnections
|
|
fetcherCfg.BlockedNetworks = s.config.BlockedNetworks
|
|
|
|
// Create the service
|
|
svc, err := imgcache.NewService(&imgcache.ServiceConfig{
|
|
Cache: cache,
|
|
FetcherConfig: fetcherCfg,
|
|
SigningKey: s.config.SigningKey,
|
|
Allowlist: s.config.AllowlistHosts,
|
|
MaxConcurrentProcessing: s.config.MaxConcurrentProcessing,
|
|
Logger: s.log,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.imgSvc = svc
|
|
s.log.Info("image service initialized")
|
|
|
|
// Initialize session manager (signing key is validated at config load
|
|
// time). Session cookies are always Secure/HttpOnly/SameSite=Strict.
|
|
sessMgr, err := session.NewManager(s.config.SigningKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.sessMgr = sessMgr
|
|
|
|
// Initialize encrypted URL generator
|
|
encGen, err := encurl.NewGenerator(s.config.SigningKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.encGen = encGen
|
|
|
|
s.log.Info("session manager and URL generator initialized")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Handlers) respondJSON(w http.ResponseWriter, data any, status int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
|
|
if data != nil {
|
|
err := json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
s.log.Error("json encode error", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Handlers) respondError(w http.ResponseWriter, message string, status int) {
|
|
s.respondJSON(w, map[string]any{
|
|
"error": message,
|
|
"status": status,
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}, status)
|
|
}
|