7 Commits
Author SHA1 Message Date
clawbot 31ed20ec9e Count a late hit, and report no totals on a disabled cache (closes #56)
check / check (push) Successful in 3m12s
A hit is now counted with the request context detached from its
cancellation, as the miss already is, so a hit served after the client
left or the request timed out still moves the hit count. On a disabled
disk cache, Stats reports no items and no size, whatever rows an earlier
run left in the database; the size already followed that rule.

Model: opus-5-5
2026-09-29 02:46:31 +00:00
clawbot 3daa416f4b Test that a late hit and a disabled cache are counted right (closes #56)
A hit served after the request context has ended must still move the hit
count, and a disabled disk cache must report no items and no size even
when its database holds rows from an earlier run.

Model: opus-5-5
2026-09-29 02:46:31 +00:00
clawbot 614bcd9eb6 Count interrupted misses and the upstream bytes they read (closes #56)
The miss and transform counters are written with context.WithoutCancel,
so a client disconnect or the request timeout during or after the work
no longer loses them. A failed read of the upstream body now returns
the bytes read before the error, so an over-size or cut-off body still
moves the upstream fetch counters. processFromSourceOrFetch passes the
cached source's length directly instead of through a local named
fetchBytes.

Model: opus-5-5
2026-09-29 02:46:30 +00:00
clawbot 64108c05bb Test that cache stats count interrupted misses (closes #56)
Checks every cache_stats counter after a miss whose request context
ends during or after the upstream fetch, and after one whose upstream
body is over the size limit.

Model: opus-5-5
2026-09-29 02:46:30 +00:00
clawbot ae8b45e93f Make the cache stats count what is cached, fetched and transcoded (closes #56)
Stats read request_cache and output_content, which nothing writes, so
TotalItems and TotalSizeBytes were always 0. They now count
source_content plus variant_content, the size through UsageBytes; a
failed query is still logged at warn. Get counts a miss after the work,
passing the bytes fetched from upstream (0 for a cached source; still
counted when the fetched source then fails), so upstream_fetch_count and
upstream_fetch_bytes move. transform_count is incremented after each
successful image processor call. request_cache and output_content stay
in the schema; dropping them is a separate decision.

Model: opus-5-5
2026-09-29 02:46:30 +00:00
clawbot 19e018b037 Test that the cache stats counters and totals move (closes #56)
Failing tests, committed ahead of the fix. Stats totals are checked
after storing a source image and two processed variants. A walk through
Service.Get (a miss that fetches, a hit, a miss that reuses the cached
source, a source failing the magic byte check, a source not found)
checks every cache_stats counter after each step. The warn-log test for
the Stats queries now drops source_content and variant_content, the
tables Stats will read.

Model: opus-5-5
2026-09-29 02:46:30 +00:00
clawbot ed3f8770e6 Give pixad a fixed uid and gid 65532 (closes #151)
check / check (push) Successful in 20s
adduser took the first free uid, 1000, and the entrypoint gives a
bind-mounted /var/lib/pixa to pixad, so on the host a person's login
account ended up owning pixa's database and cache. The image now creates
the pixad group with gid 65532 and the pixad user with uid 65532, which
host login and system accounts do not use. The first-run step of
"Running under upaas" in README.md names the uid and gid.

Model: opus-5-5
2026-09-29 04:44:49 +02:00
5 changed files with 39 additions and 22 deletions
+6 -2
View File
@@ -68,8 +68,12 @@ RUN apk add --no-cache \
COPY --from=builder /pixad /usr/local/bin/pixad
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# Create non-root user, config directory, and data directory
RUN adduser -D -H -s /sbin/nologin pixad && \
# Create non-root user, config directory, and data directory. pixad
# gets uid and gid 65532, which host login and system accounts do not
# use: a bind-mounted /var/lib/pixa is given to pixad, and on the host
# it must not belong to a person's account.
RUN addgroup -g 65532 pixad && \
adduser -D -H -s /sbin/nologin -u 65532 -G pixad pixad && \
mkdir -p /var/lib/pixa /etc/pixa && \
chown pixad:pixad /var/lib/pixa
+4 -2
View File
@@ -58,8 +58,10 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for pixa needs:
`healthy`. The probe uses the port from `PORT` (default `8080`), so a
port changed only in a mounted config file is not seen by it: change
the port with `PORT`.
- **First run:** create the host directory. It may be owned by root: the
container gives it to its `pixad` user when it starts.
- **First run:** create the host directory, owned by root or by uid
`65532` and gid `65532`. The server runs as the container's `pixad`
user, which has that uid and gid, and the container gives the
directory to `pixad` when it starts.
## Rationale
+13 -6
View File
@@ -30,6 +30,11 @@ exhaustion
# Completed Steps
- 2026-09-29 fixed uid and gid for `pixad` (closes #151): the image creates the
`pixad` group with gid 65532 and the `pixad` user with uid 65532, instead of
the first free uid 1000, so a bind-mounted `/var/lib/pixa` given to `pixad`
is not owned on the host by a person's login account; the first-run step of
"Running under upaas" in `README.md` names the uid and gid.
- 2026-09-29 `max-age` never outlives an expiring URL (closes #63): both image
routes build `Cache-Control` from the request's `Expires`, which an encrypted
URL's expiry now fills too; `max-age` is one year, or the whole seconds left
@@ -41,12 +46,14 @@ exhaustion
counts the cached source images and processed variants (`source_content`
plus `variant_content`) and takes their size from `Cache.UsageBytes`,
instead of reading `request_cache` and `output_content`, which nothing
writes; those two tables are left in the schema. A miss is counted after
it is served or fails, even when the request context has ended by then,
with the bytes it read from upstream, so `upstream_fetch_count` and
`upstream_fetch_bytes` move, including for an upstream body that fails
partway or a fetched source that then fails the magic byte check;
`transform_count` counts each image the image processor transcodes.
writes; those two tables are left in the schema; a disabled disk cache
reports no items and no size. A hit is counted even when the request
context has ended. A miss is counted after it is served or fails, also
when the request context has ended by then, with the bytes it read from
upstream, so `upstream_fetch_count` and `upstream_fetch_bytes` move,
including for an upstream body that fails partway or a fetched source
that then fails the magic byte check; `transform_count` counts each image
the image processor transcodes.
- 2026-09-28 strip metadata from processed images (closes #82): every output is
exported with govips' `StripMetadata`, so it carries no EXIF, XMP, IPTC or ICC
profile; the image is first turned upright with `AutoRotate` (before sizes are
+14 -11
View File
@@ -419,18 +419,21 @@ func (c *Cache) Stats(ctx context.Context) (*CacheStats, error) {
return nil, fmt.Errorf("failed to get cache stats: %w", err)
}
// Count and size the cached source images and processed variants
err = c.db.QueryRowContext(ctx, `
SELECT (SELECT COUNT(*) FROM source_content)
+ (SELECT COUNT(*) FROM variant_content)
`).Scan(&stats.TotalItems)
if err != nil {
c.log.Warn("failed to count cache items for stats", "error", err)
}
// Count and size the cached source images and processed variants. A
// disabled cache holds none, whatever rows an earlier run left.
if !c.disabled {
err = c.db.QueryRowContext(ctx, `
SELECT (SELECT COUNT(*) FROM source_content)
+ (SELECT COUNT(*) FROM variant_content)
`).Scan(&stats.TotalItems)
if err != nil {
c.log.Warn("failed to count cache items for stats", "error", err)
}
stats.TotalSizeBytes, err = c.UsageBytes(ctx)
if err != nil {
c.log.Warn("failed to sum cache size for stats", "error", err)
stats.TotalSizeBytes, err = c.UsageBytes(ctx)
if err != nil {
c.log.Warn("failed to sum cache size for stats", "error", err)
}
}
// Compute hit rate as a ratio
+2 -1
View File
@@ -143,7 +143,8 @@ func (s *Service) Get(ctx context.Context, req *ImageRequest) (*ImageResponse, e
s.log.Error("failed to get cached variant", "key", result.CacheKey, "error", err)
// Fall through to re-process
} else {
s.cache.IncrementStats(ctx, true, 0)
// Counted also when the request context has ended meanwhile
s.cache.IncrementStats(context.WithoutCancel(ctx), true, 0)
return &ImageResponse{
Content: reader,