Compare commits
2
Commits
0969cf43bb
...
0e345482bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e345482bc | ||
|
|
5058fd532b |
+6
-3
@@ -22,8 +22,9 @@ FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# script/bootstrap --cgo installs the build dependencies (a C compiler
|
||||
# and the libvips and libheif headers) and downloads the Go modules.
|
||||
# script/bootstrap --cgo installs the build dependencies (a C compiler,
|
||||
# the libvips and libheif headers, and libvips' JPEG XL support, which
|
||||
# the tests need) and downloads the Go modules.
|
||||
COPY script/ ./script/
|
||||
COPY go.mod go.sum ./
|
||||
RUN script/bootstrap --cgo
|
||||
@@ -80,9 +81,11 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
||||
# alpine:3.21, 2026-02-25
|
||||
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||
|
||||
# Install runtime dependencies only
|
||||
# Install runtime dependencies only. vips-jxl is libvips' JPEG XL
|
||||
# support, without which pixad does not start.
|
||||
RUN apk add --no-cache \
|
||||
vips \
|
||||
vips-jxl \
|
||||
libheif \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
|
||||
@@ -89,7 +89,10 @@ another part of pixa failed to stop. A request not finished by then is cut off.
|
||||
`docker stop` waits 10 seconds before it kills the container.
|
||||
|
||||
Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as
|
||||
it uses libvips through CGO; building it also needs their development files,
|
||||
it uses libvips through CGO. pixad does not start unless libvips has its JPEG XL
|
||||
support, which on Alpine is the `vips-jxl` package and which the nix and brew
|
||||
packages of libvips include, as do the apt ones from Debian 12 and Ubuntu 24.04
|
||||
on. Building pixa also needs the development files of libvips and libheif,
|
||||
`pkg-config` and a C compiler. `script/bootstrap --cgo` installs all of these,
|
||||
as the `Dockerfile` does where it compiles pixa. Plain `script/bootstrap`, which
|
||||
`script/setup` and `script/cibuild` run, installs git, make and Go, and Node,
|
||||
@@ -529,10 +532,13 @@ Key settings in more detail:
|
||||
- `signing_key` — HMAC secret for URL signatures
|
||||
- `db_url` — the SQLite database to open; omitted, it is
|
||||
`file:<state_dir>/state.sqlite3?_pragma=journal_mode(WAL)`, which keeps the
|
||||
database in WAL mode. pixa adds `_pragma=busy_timeout(5000)` to any `db_url`,
|
||||
so a write that finds another in progress waits up to five seconds for it
|
||||
instead of failing. WAL mode comes only from the URL: keep
|
||||
`_pragma=journal_mode(WAL)` in one you set
|
||||
database in WAL mode. pixa opens one connection to it, so its own reads and
|
||||
writes run one at a time. Requests wait while eviction runs one of its
|
||||
queries, some of which read a whole table. pixa adds
|
||||
`_pragma=busy_timeout(5000)` to any `db_url`, so a write that finds another
|
||||
program writing to the file waits up to five seconds for it instead of
|
||||
failing. WAL mode comes only from the URL: keep `_pragma=journal_mode(WAL)` in
|
||||
one you set
|
||||
- `cache_max_bytes` — disk cache size limit in bytes; `0` disables the disk
|
||||
cache entirely; omitted defaults to 75% of the sum of the free space on the
|
||||
filesystem containing `<state_dir>/cache/` and the bytes of source and
|
||||
@@ -582,8 +588,8 @@ provide:
|
||||
|
||||
- `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and
|
||||
download the Go modules (idempotent); with `--cgo`, the C compiler and the
|
||||
libvips and libheif libraries that compiling pixa needs instead of Node, Yarn
|
||||
and prettier
|
||||
libvips (with its JPEG XL support) and libheif libraries that compiling and
|
||||
testing pixa need instead of Node, Yarn and prettier
|
||||
- `script/setup` — make a fresh clone ready for development (bootstrap, then
|
||||
install-precommit)
|
||||
- `script/projectname` — output the project name ("pixa")
|
||||
|
||||
@@ -30,6 +30,21 @@ P2: security: per-IP rate limiting on the image routes
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-08 libvips' JPEG XL support is installed and required (part of #222):
|
||||
`script/bootstrap --cgo` installs `vips-jxl` when its package manager is apk,
|
||||
as Alpine's `vips` package lacks the support, and the runtime stage of the
|
||||
`Dockerfile` installs it too. `imgcache.NewService` fails, naming the fix,
|
||||
when `imageprocessor.CheckJPEGXLSupport` finds that libvips cannot load and
|
||||
save JPEG XL, so pixad does not start without it. A test saves an image as
|
||||
JPEG XL with govips and loads it back. JPEG XL is not yet a format pixa
|
||||
serves.
|
||||
- 2026-10-08 SQLite writes no longer fail with "database is locked" under load
|
||||
(closes #223): `internal/database` opens the database with one connection, so
|
||||
pixa's own reads and writes run on it one at a time instead of competing for
|
||||
SQLite's lock, where a write that kept losing could wait past the five-second
|
||||
busy timeout and be lost. The busy timeout stays, for another program writing
|
||||
to the same file. No code in pixa keeps rows or a transaction open while it
|
||||
runs another query, which with one connection would wait forever.
|
||||
- 2026-10-05 the format `auto` (closes #88): a format in the `/v1/image/` path,
|
||||
an encrypted URL's token and the generator page's format choice, chosen for
|
||||
each request from `Accept` once the signature or token is checked: AVIF when
|
||||
|
||||
@@ -13,11 +13,10 @@ import (
|
||||
)
|
||||
|
||||
// TestConcurrentWritesAllSucceed opens a database the way pixad does and
|
||||
// writes to it from several goroutines at once, so the writes run on
|
||||
// separate connections, as one request's writes and the background eviction
|
||||
// pass do. Every write must succeed, none failing with "database is locked",
|
||||
// whether or not db_url already has parameters, and the parameters it has
|
||||
// must still apply.
|
||||
// writes to it from several goroutines at once, as one request's writes and
|
||||
// the background eviction pass do. Every write must succeed, none failing
|
||||
// with "database is locked", whether or not db_url already has parameters,
|
||||
// and the parameters it has must still apply.
|
||||
func TestConcurrentWritesAllSucceed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -237,17 +237,18 @@ func ApplyMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB returns the underlying sql.DB.
|
||||
// DB returns the underlying sql.DB. It has one connection, so close any
|
||||
// rows and end any transaction before running another query on it; a
|
||||
// query run while they are open waits forever.
|
||||
func (s *Database) DB() *sql.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Database) connect(ctx context.Context) error {
|
||||
// Requests and the eviction pass write on separate connections. With
|
||||
// a busy timeout, a write that finds another one in progress waits up
|
||||
// to five seconds for it instead of failing at once with "database is
|
||||
// locked". The driver runs each _pragma parameter on every connection
|
||||
// it opens.
|
||||
// With a busy timeout, a write that finds another program writing to
|
||||
// the same database file waits up to five seconds for it instead of
|
||||
// failing at once with "database is locked". The driver runs each
|
||||
// _pragma parameter on every connection it opens.
|
||||
separator := "?"
|
||||
if strings.Contains(s.config.DBURL, "?") {
|
||||
separator = "&"
|
||||
@@ -264,6 +265,11 @@ func (s *Database) connect(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// One connection: pixa's own reads and writes run on it one at a
|
||||
// time instead of competing for SQLite's lock, where a write that
|
||||
// keeps losing can wait past the busy timeout and be lost.
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
err = db.PingContext(ctx)
|
||||
if err != nil {
|
||||
s.log.Error("failed to ping database", "error", err)
|
||||
|
||||
@@ -37,6 +37,24 @@ func initVips() {
|
||||
})
|
||||
}
|
||||
|
||||
// errNoJPEGXL is returned by CheckJPEGXLSupport.
|
||||
var errNoJPEGXL = errors.New("libvips lacks JPEG XL support: install " +
|
||||
"vips-jxl on Alpine, or use a libvips built with libjxl")
|
||||
|
||||
// CheckJPEGXLSupport returns an error, naming the fix, when libvips
|
||||
// cannot load and save JPEG XL.
|
||||
func CheckJPEGXLSupport() error {
|
||||
initVips()
|
||||
|
||||
// govips counts a format as supported when libvips has its loader;
|
||||
// libvips builds the JPEG XL loader and saver together.
|
||||
if !vips.IsTypeSupported(vips.ImageTypeJXL) {
|
||||
return errNoJPEGXL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format represents supported output image formats.
|
||||
type Format string
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package imageprocessor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davidbyttow/govips/v2/vips"
|
||||
)
|
||||
|
||||
// TestCheckJPEGXLSupport fails when libvips lacks JPEG XL support, as on
|
||||
// Alpine without the vips-jxl package.
|
||||
func TestCheckJPEGXLSupport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := CheckJPEGXLSupport()
|
||||
if err != nil {
|
||||
t.Fatalf("CheckJPEGXLSupport() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLibvipsSavesAndLoadsJPEGXL saves an image as JPEG XL with govips and
|
||||
// loads it back. It fails when libvips lacks JPEG XL support, as on Alpine
|
||||
// without the vips-jxl package.
|
||||
func TestLibvipsSavesAndLoadsJPEGXL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
img, err := vips.NewImageFromBuffer(createTestJPEG(t, 64, 48))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test JPEG: %v", err)
|
||||
}
|
||||
|
||||
defer img.Close()
|
||||
|
||||
jxl, _, err := img.ExportJxl(vips.NewJxlExportParams())
|
||||
if err != nil {
|
||||
t.Fatalf("ExportJxl() error = %v", err)
|
||||
}
|
||||
|
||||
loaded, err := vips.NewImageFromBuffer(jxl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load the JPEG XL image: %v", err)
|
||||
}
|
||||
|
||||
defer loaded.Close()
|
||||
|
||||
if loaded.Format() != vips.ImageTypeJXL {
|
||||
t.Errorf("loaded format = %s, want jxl", vips.ImageTypes[loaded.Format()])
|
||||
}
|
||||
|
||||
if loaded.Width() != 64 || loaded.Height() != 48 {
|
||||
t.Errorf("loaded size = %dx%d, want 64x48", loaded.Width(), loaded.Height())
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,13 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
allowHTTP = cfg.FetcherConfig.AllowHTTP
|
||||
}
|
||||
|
||||
// JPEG XL is to become the default output format, so pixad does not
|
||||
// start without it.
|
||||
err := imageprocessor.CheckJPEGXLSupport()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxResponseSize := fetcherCfg.MaxResponseSize
|
||||
processor := imageprocessor.New(imageprocessor.Params{
|
||||
MaxInputBytes: maxResponseSize,
|
||||
|
||||
+16
-5
@@ -14,11 +14,12 @@
|
||||
# script/fmt-check: all the host needs, as
|
||||
# the checks compile pixa in Docker
|
||||
# script/bootstrap --cgo git, make, Go, and a C compiler and the
|
||||
# CGO image libraries (pkg-config, vips,
|
||||
# libheif) for the govips bindings instead
|
||||
# of Node: to compile pixa, in the
|
||||
# Dockerfile's test phase and build stage,
|
||||
# which format nothing
|
||||
# CGO image libraries (pkg-config, vips
|
||||
# with its JPEG XL support, libheif) for
|
||||
# the govips bindings instead of Node: to
|
||||
# compile pixa, in the Dockerfile's test
|
||||
# phase and build stage, which format
|
||||
# nothing
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -150,6 +151,16 @@ ensure_cgo_deps() {
|
||||
if ! pkg-config --exists vips; then
|
||||
pkg_install vips libvips-dev vips vips-dev
|
||||
fi
|
||||
# libvips' JPEG XL loader and saver are in the nix and brew vips
|
||||
# packages, and in apt's from Debian 12 and Ubuntu 24.04 on, but in
|
||||
# the package vips-jxl on Alpine. detect_pkgmgr is called only where
|
||||
# apk exists, as on apt it updates the package lists.
|
||||
if ! missing apk; then
|
||||
detect_pkgmgr
|
||||
fi
|
||||
if [ "$PKGMGR" = "apk" ] && ! apk info -e vips-jxl >/dev/null; then
|
||||
apk add --no-cache vips-jxl
|
||||
fi
|
||||
if ! pkg-config --exists libheif; then
|
||||
pkg_install libheif libheif-dev libheif libheif-dev
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user