Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dfb2e9651 |
+3
-6
@@ -22,9 +22,8 @@ FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# script/bootstrap --cgo installs the build dependencies (a C compiler,
|
# script/bootstrap --cgo installs the build dependencies (a C compiler
|
||||||
# the libvips and libheif headers, and libvips' JPEG XL support, which
|
# and the libvips and libheif headers) and downloads the Go modules.
|
||||||
# the tests need) and downloads the Go modules.
|
|
||||||
COPY script/ ./script/
|
COPY script/ ./script/
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN script/bootstrap --cgo
|
RUN script/bootstrap --cgo
|
||||||
@@ -81,11 +80,9 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|||||||
# alpine:3.21, 2026-02-25
|
# alpine:3.21, 2026-02-25
|
||||||
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|
||||||
# Install runtime dependencies only. vips-jxl is libvips' JPEG XL
|
# Install runtime dependencies only
|
||||||
# support, without which pixad does not start.
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
vips \
|
vips \
|
||||||
vips-jxl \
|
|
||||||
libheif \
|
libheif \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
|
|||||||
@@ -89,10 +89,7 @@ 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.
|
`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
|
Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as
|
||||||
it uses libvips through CGO. pixad does not start unless libvips has its JPEG XL
|
it uses libvips through CGO; building it also needs their development files,
|
||||||
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,
|
`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
|
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,
|
`script/setup` and `script/cibuild` run, installs git, make and Go, and Node,
|
||||||
@@ -586,8 +583,8 @@ provide:
|
|||||||
|
|
||||||
- `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and
|
- `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and
|
||||||
download the Go modules (idempotent); with `--cgo`, the C compiler and the
|
download the Go modules (idempotent); with `--cgo`, the C compiler and the
|
||||||
libvips (with its JPEG XL support) and libheif libraries that compiling and
|
libvips and libheif libraries that compiling pixa needs instead of Node, Yarn
|
||||||
testing pixa need instead of Node, Yarn and prettier
|
and prettier
|
||||||
- `script/setup` — make a fresh clone ready for development (bootstrap, then
|
- `script/setup` — make a fresh clone ready for development (bootstrap, then
|
||||||
install-precommit)
|
install-precommit)
|
||||||
- `script/projectname` — output the project name ("pixa")
|
- `script/projectname` — output the project name ("pixa")
|
||||||
|
|||||||
@@ -30,14 +30,6 @@ P2: security: per-IP rate limiting on the image routes
|
|||||||
|
|
||||||
# Completed Steps
|
# 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 requests no longer wait behind eviction queries that read a whole
|
- 2026-10-08 requests no longer wait behind eviction queries that read a whole
|
||||||
table (closes #227): the new `cache_usage` table holds the total cache usage,
|
table (closes #227): the new `cache_usage` table holds the total cache usage,
|
||||||
kept up to date by triggers on `source_content` and `variant_content` in the
|
kept up to date by triggers on `source_content` and `variant_content` in the
|
||||||
|
|||||||
@@ -37,24 +37,6 @@ 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.
|
// Format represents supported output image formats.
|
||||||
type Format string
|
type Format string
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -96,17 +96,6 @@ type Cache struct {
|
|||||||
// deterministically pause inside that window to exercise
|
// deterministically pause inside that window to exercise
|
||||||
// concurrent stores against it; production code leaves it nil.
|
// concurrent stores against it; production code leaves it nil.
|
||||||
evictSourceBlobTestHook func(ContentHash)
|
evictSourceBlobTestHook func(ContentHash)
|
||||||
|
|
||||||
// reconciliationPageSize is the most rows one read of a content table
|
|
||||||
// returns in the reconciliation pass and in Stats. newCache sets it to
|
|
||||||
// defaultReconciliationPageSize; tests set it smaller.
|
|
||||||
reconciliationPageSize int
|
|
||||||
|
|
||||||
// reconciliationReadTestHook, when set, is called after each of those
|
|
||||||
// reads with the number of rows the read covered, so tests can check
|
|
||||||
// that no read covers more than one page; production code leaves it
|
|
||||||
// nil.
|
|
||||||
reconciliationReadTestHook func(rows int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCache creates a new cache instance.
|
// NewCache creates a new cache instance.
|
||||||
@@ -138,8 +127,6 @@ func newCache(
|
|||||||
evictionDone: make(chan struct{}),
|
evictionDone: make(chan struct{}),
|
||||||
metaCache: metaCache,
|
metaCache: metaCache,
|
||||||
contentLocks: newContentLock(),
|
contentLocks: newContentLock(),
|
||||||
|
|
||||||
reconciliationPageSize: defaultReconciliationPageSize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.disabled {
|
if c.disabled {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package imgcache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"strconv"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -148,43 +148,64 @@ func TestUsageTotalNotCorrectedFromAnOlderSum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReconciliationReadsAPageAtATime stores five source images and five
|
// TestReconciliationReadsAPageAtATime adds one row more than a page to
|
||||||
// variants, sets the page size to two rows and runs a reconciliation
|
// each content table, none of them with a file. Each reconciliation read
|
||||||
// pass. No read the pass makes of a content table, to check its rows or
|
// must return one page at most, so a request's query waits for one page
|
||||||
// to sum them, may cover more than two rows, so a request's query waits
|
// at most, and the pass must still reach every row: it sums all of them
|
||||||
// for one page at most. Between them the reads must still cover every
|
// and drops all of them.
|
||||||
// row of both tables twice, once to check it and once to sum it, and the
|
|
||||||
// sum must put a wrong total right.
|
|
||||||
func TestReconciliationReadsAPageAtATime(t *testing.T) {
|
func TestReconciliationReadsAPageAtATime(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cache, _ := newEvictionTestCache(t, 1<<30)
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
const pageSize = 2
|
const rowsPerTable = reconciliationPageSize + 1
|
||||||
|
|
||||||
cache.reconciliationPageSize = pageSize
|
for i := range rowsPerTable {
|
||||||
|
_, err := cache.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO variant_content (cache_key, size_bytes, content_type)
|
||||||
|
VALUES (?, 1, ?)
|
||||||
|
`, fmt.Sprintf("%012x", i), testContentTypeWebP)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to insert variant row %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Sources of 100 bytes and variants of 10, five of each.
|
_, err = cache.db.ExecContext(ctx, `
|
||||||
for i := range 5 {
|
INSERT INTO source_content (content_hash, content_type, size_bytes)
|
||||||
content := []byte(strconv.Itoa(i))
|
VALUES (?, ?, 1)
|
||||||
|
`, fmt.Sprintf("%064x", i), testContentTypeJPEG)
|
||||||
storeEvictionTestSource(t, cache, "pages.example.com",
|
if err != nil {
|
||||||
"/"+strconv.Itoa(i)+".jpg", bytes.Repeat(content, 100))
|
t.Fatalf("failed to insert source row %d: %v", i, err)
|
||||||
storeEvictionTestVariant(t, cache, VariantKey("aabbccdd000"+strconv.Itoa(i)),
|
}
|
||||||
bytes.Repeat(content, 10))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := cache.db.ExecContext(ctx,
|
keys, err := cache.variantKeysAfter(ctx, "")
|
||||||
`UPDATE cache_usage SET total_size_bytes = 1 WHERE id = 1`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to set a wrong total: %v", err)
|
t.Fatalf("variantKeysAfter failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rowsPerRead []int
|
if len(keys) != reconciliationPageSize {
|
||||||
|
t.Errorf("one read returned %d variant keys, want %d",
|
||||||
|
len(keys), reconciliationPageSize)
|
||||||
|
}
|
||||||
|
|
||||||
cache.reconciliationReadTestHook = func(rows int) {
|
hashes, err := cache.sourceContentHashesAfter(ctx, "")
|
||||||
rowsPerRead = append(rowsPerRead, rows)
|
if err != nil {
|
||||||
|
t.Fatalf("sourceContentHashesAfter failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hashes) != reconciliationPageSize {
|
||||||
|
t.Errorf("one read returned %d source hashes, want %d",
|
||||||
|
len(hashes), reconciliationPageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum, err := cache.sumContentSizeBytes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sumContentSizeBytes failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sum != 2*rowsPerTable {
|
||||||
|
t.Errorf("sumContentSizeBytes() = %d, want %d", sum, 2*rowsPerTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cache.reconcileAccounting(ctx)
|
err = cache.reconcileAccounting(ctx)
|
||||||
@@ -192,31 +213,12 @@ func TestReconciliationReadsAPageAtATime(t *testing.T) {
|
|||||||
t.Fatalf("reconcileAccounting failed: %v", err)
|
t.Fatalf("reconcileAccounting failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("rows covered by each read, in order: %v", rowsPerRead)
|
if n := countRows(t, cache, `SELECT COUNT(*) FROM variant_content`); n != 0 {
|
||||||
|
t.Errorf("%d variant rows without a file are left, want 0", n)
|
||||||
coveredRows := 0
|
|
||||||
|
|
||||||
for _, rows := range rowsPerRead {
|
|
||||||
if rows > pageSize {
|
|
||||||
t.Errorf("a read covered %d rows, want at most %d", rows, pageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
coveredRows += rows
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ten rows, each read once to check it and once to sum it.
|
if n := countRows(t, cache, `SELECT COUNT(*) FROM source_content`); n != 0 {
|
||||||
if coveredRows != 20 {
|
t.Errorf("%d source rows without a file are left, want 0", n)
|
||||||
t.Errorf("the reads covered %d rows in all, want 20", coveredRows)
|
|
||||||
}
|
|
||||||
|
|
||||||
usage, err := cache.UsageBytes(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UsageBytes failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if usage != 550 {
|
|
||||||
t.Errorf("UsageBytes() after reconciliation = %d, want 550 (5*100 + 5*10)",
|
|
||||||
usage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func (c *Cache) computeDefaultMaxBytes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Both terms are at most math.MaxInt64, so the sum cannot overflow.
|
// Both terms are at most math.MaxInt64, so the sum cannot overflow.
|
||||||
//nolint:gosec // G115: UsageBytes returns the total cache usage, never negative
|
//nolint:gosec // G115: UsageBytes sums file sizes, never negative
|
||||||
spaceBytes := min(freeBytes, math.MaxInt64) + uint64(usedBytes)
|
spaceBytes := min(freeBytes, math.MaxInt64) + uint64(usedBytes)
|
||||||
|
|
||||||
computed := spaceBytes / freeSpaceFractionDenominator * freeSpaceFractionNumerator
|
computed := spaceBytes / freeSpaceFractionDenominator * freeSpaceFractionNumerator
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ const DefaultEvictionInterval = 5 * time.Minute
|
|||||||
// and source blobs) one eviction pass fetches from the database.
|
// and source blobs) one eviction pass fetches from the database.
|
||||||
const evictionBatchSize = 100
|
const evictionBatchSize = 100
|
||||||
|
|
||||||
// defaultReconciliationPageSize is the most rows one read of the
|
// reconciliationPageSize is the most rows one read of the reconciliation
|
||||||
// reconciliation pass returns, unless a test sets
|
// pass returns. Each read is a query of its own, so a request waits for
|
||||||
// Cache.reconciliationPageSize smaller. Each read is a query of its own,
|
// one page at most, however large the cache is.
|
||||||
// so a request waits for one page at most, however large the cache is.
|
const reconciliationPageSize = 1000
|
||||||
const defaultReconciliationPageSize = 1000
|
|
||||||
|
|
||||||
// staleTempFileAge is how old an orphaned temp file (left behind by a
|
// staleTempFileAge is how old an orphaned temp file (left behind by a
|
||||||
// crashed write) must be before reconciliation removes it. Fresh temp
|
// crashed write) must be before reconciliation removes it. Fresh temp
|
||||||
@@ -677,10 +676,6 @@ func (c *Cache) reconcileVariantRows(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.reconciliationReadTestHook != nil {
|
|
||||||
c.reconciliationReadTestHook(len(keys))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -707,7 +702,7 @@ func (c *Cache) reconcileVariantRows(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// variantKeysAfter returns, in order, up to c.reconciliationPageSize
|
// variantKeysAfter returns, in order, up to reconciliationPageSize
|
||||||
// tracked variant cache keys that sort after the given one.
|
// tracked variant cache keys that sort after the given one.
|
||||||
func (c *Cache) variantKeysAfter(
|
func (c *Cache) variantKeysAfter(
|
||||||
ctx context.Context, after VariantKey,
|
ctx context.Context, after VariantKey,
|
||||||
@@ -715,7 +710,7 @@ func (c *Cache) variantKeysAfter(
|
|||||||
return queryStringColumn[VariantKey](ctx, c.db, `
|
return queryStringColumn[VariantKey](ctx, c.db, `
|
||||||
SELECT cache_key FROM variant_content
|
SELECT cache_key FROM variant_content
|
||||||
WHERE cache_key > ? ORDER BY cache_key LIMIT ?
|
WHERE cache_key > ? ORDER BY cache_key LIMIT ?
|
||||||
`, "variant keys", "variant key", string(after), c.reconciliationPageSize)
|
`, "variant keys", "variant key", string(after), reconciliationPageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryStringColumn runs a single-column query with args and returns the
|
// queryStringColumn runs a single-column query with args and returns the
|
||||||
@@ -828,10 +823,6 @@ func (c *Cache) reconcileSourceRows(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.reconciliationReadTestHook != nil {
|
|
||||||
c.reconciliationReadTestHook(len(hashes))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hashes) == 0 {
|
if len(hashes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -860,7 +851,7 @@ func (c *Cache) reconcileSourceRows(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sourceContentHashesAfter returns, in order, up to
|
// sourceContentHashesAfter returns, in order, up to
|
||||||
// c.reconciliationPageSize tracked source content hashes that sort after
|
// reconciliationPageSize tracked source content hashes that sort after
|
||||||
// the given one.
|
// the given one.
|
||||||
func (c *Cache) sourceContentHashesAfter(
|
func (c *Cache) sourceContentHashesAfter(
|
||||||
ctx context.Context, after ContentHash,
|
ctx context.Context, after ContentHash,
|
||||||
@@ -868,24 +859,20 @@ func (c *Cache) sourceContentHashesAfter(
|
|||||||
return queryStringColumn[ContentHash](ctx, c.db, `
|
return queryStringColumn[ContentHash](ctx, c.db, `
|
||||||
SELECT content_hash FROM source_content
|
SELECT content_hash FROM source_content
|
||||||
WHERE content_hash > ? ORDER BY content_hash LIMIT ?
|
WHERE content_hash > ? ORDER BY content_hash LIMIT ?
|
||||||
`, "source content hashes", "content hash", string(after),
|
`, "source content hashes", "content hash", string(after), reconciliationPageSize)
|
||||||
c.reconciliationPageSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each of these queries sums size_bytes over the next page of rows of
|
// Each of these queries sums size_bytes over the next page of rows of
|
||||||
// one content table, the rows that sort after a key, and returns the
|
// one content table, the rows that sort after a key, and returns the
|
||||||
// page's last key, the sum and the number of rows in the page. Past the
|
// page's last key with the sum. Past the last row the key is NULL.
|
||||||
// last row the page has no rows and the key is NULL.
|
|
||||||
const (
|
const (
|
||||||
sourceSizePageQuery = `
|
sourceSizePageQuery = `
|
||||||
SELECT MAX(content_hash), COALESCE(SUM(size_bytes), 0), COUNT(*)
|
SELECT MAX(content_hash), COALESCE(SUM(size_bytes), 0) FROM (
|
||||||
FROM (
|
|
||||||
SELECT content_hash, size_bytes FROM source_content
|
SELECT content_hash, size_bytes FROM source_content
|
||||||
WHERE content_hash > ? ORDER BY content_hash LIMIT ?
|
WHERE content_hash > ? ORDER BY content_hash LIMIT ?
|
||||||
)`
|
)`
|
||||||
variantSizePageQuery = `
|
variantSizePageQuery = `
|
||||||
SELECT MAX(cache_key), COALESCE(SUM(size_bytes), 0), COUNT(*)
|
SELECT MAX(cache_key), COALESCE(SUM(size_bytes), 0) FROM (
|
||||||
FROM (
|
|
||||||
SELECT cache_key, size_bytes FROM variant_content
|
SELECT cache_key, size_bytes FROM variant_content
|
||||||
WHERE cache_key > ? ORDER BY cache_key LIMIT ?
|
WHERE cache_key > ? ORDER BY cache_key LIMIT ?
|
||||||
)`
|
)`
|
||||||
@@ -921,19 +908,13 @@ func (c *Cache) sumSizeBytesInPages(
|
|||||||
|
|
||||||
var pageBytes int64
|
var pageBytes int64
|
||||||
|
|
||||||
var pageRows int
|
err := c.db.QueryRowContext(ctx, pageQuery, after, reconciliationPageSize).
|
||||||
|
Scan(&lastKey, &pageBytes)
|
||||||
err := c.db.QueryRowContext(ctx, pageQuery, after, c.reconciliationPageSize).
|
|
||||||
Scan(&lastKey, &pageBytes, &pageRows)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to sum cache content sizes: %w", err)
|
return 0, fmt.Errorf("failed to sum cache content sizes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.reconciliationReadTestHook != nil {
|
if !lastKey.Valid {
|
||||||
c.reconciliationReadTestHook(pageRows)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pageRows == 0 {
|
|
||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,13 +100,6 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
|||||||
allowHTTP = cfg.FetcherConfig.AllowHTTP
|
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
|
maxResponseSize := fetcherCfg.MaxResponseSize
|
||||||
processor := imageprocessor.New(imageprocessor.Params{
|
processor := imageprocessor.New(imageprocessor.Params{
|
||||||
MaxInputBytes: maxResponseSize,
|
MaxInputBytes: maxResponseSize,
|
||||||
|
|||||||
+5
-16
@@ -14,12 +14,11 @@
|
|||||||
# script/fmt-check: all the host needs, as
|
# script/fmt-check: all the host needs, as
|
||||||
# the checks compile pixa in Docker
|
# the checks compile pixa in Docker
|
||||||
# script/bootstrap --cgo git, make, Go, and a C compiler and the
|
# script/bootstrap --cgo git, make, Go, and a C compiler and the
|
||||||
# CGO image libraries (pkg-config, vips
|
# CGO image libraries (pkg-config, vips,
|
||||||
# with its JPEG XL support, libheif) for
|
# libheif) for the govips bindings instead
|
||||||
# the govips bindings instead of Node: to
|
# of Node: to compile pixa, in the
|
||||||
# compile pixa, in the Dockerfile's test
|
# Dockerfile's test phase and build stage,
|
||||||
# phase and build stage, which format
|
# which format nothing
|
||||||
# nothing
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
@@ -151,16 +150,6 @@ ensure_cgo_deps() {
|
|||||||
if ! pkg-config --exists vips; then
|
if ! pkg-config --exists vips; then
|
||||||
pkg_install vips libvips-dev vips vips-dev
|
pkg_install vips libvips-dev vips vips-dev
|
||||||
fi
|
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
|
if ! pkg-config --exists libheif; then
|
||||||
pkg_install libheif libheif-dev libheif libheif-dev
|
pkg_install libheif libheif-dev libheif libheif-dev
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user