Serve a cache hit with no database read or write (closes #309)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A hit opens the variant file and nothing else: its content type comes from the file's first bytes, and a file in no output format is a miss and is made again. Its access time and hit count are handed, without waiting, to the goroutine that wrote late counts, which now writes every count and the access times once a second; the access times wait in a channel of 10,000 that drops the rest. checkNegativeCache leaves expired rows to CleanExpired. The .meta files, metaCache, Lookup and golang-lru go, with their tests; tests that read counts after Get wait for the background write. make bench runs a parallel cache hit benchmark, outside make check. Model: opus-5-5
This commit is contained in:
@@ -76,9 +76,9 @@ database and the disk cache:
|
||||
pixa runs as (uid 65532) and sets its mode to `750`; outside it, that user
|
||||
must be able to write the directory.
|
||||
- `cache_max_bytes` limits the source and transformed images together. The
|
||||
database, the `.meta` file beside each transformed image and files still being
|
||||
written come on top, and eviction runs in the background, so the cache can
|
||||
pass the limit for a while: leave room on the volume beyond it.
|
||||
database and files still being written come on top, and eviction runs in the
|
||||
background, so the cache can pass the limit for a while: leave room on the
|
||||
volume beyond it.
|
||||
- Set `cache_max_bytes` for a lasting deployment. Its default, worked out each
|
||||
time pixa starts, is 75% of the sum of the space free on the volume and the
|
||||
space the cached images already take, so a restart keeps the limit the cache
|
||||
@@ -90,11 +90,12 @@ which answers 200 whenever pixa is running, in maintenance mode too (see
|
||||
|
||||
On SIGTERM or SIGINT pixa stops accepting connections, gives the requests in
|
||||
progress and the images being processed 5 seconds to finish, then writes to the
|
||||
database the counts of cache hits, misses, fetches and conversions that requests
|
||||
could not write by their deadline, and exits: with 0, or with 1 when images were
|
||||
still being processed after those 5 seconds, some of those counts could not be
|
||||
written, or 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.
|
||||
database the counts of cache hits, misses, fetches and conversions and the times
|
||||
cached images were last served that are not written yet (see Storage), and
|
||||
exits: with 0, or with 1 when images were still being processed after those 5
|
||||
seconds, some of those counts could not be written, or 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.16) and libheif to run, as
|
||||
it uses libvips through CGO. pixad does not start unless libvips has its JPEG XL
|
||||
@@ -181,8 +182,7 @@ allowlisted source hosts for open access.
|
||||
file for each source URL
|
||||
- **Database**: `<state_dir>/state.sqlite3` (SQLite)
|
||||
- **Transformed images**:
|
||||
`<state_dir>/cache/variants/<ab>/<cd>/<sha256 of host, path, query, size, format, quality and fit>`,
|
||||
each with a `.meta` file beside it holding its content type
|
||||
`<state_dir>/cache/variants/<ab>/<cd>/<sha256 of host, path, query, size, format, quality and fit>`
|
||||
|
||||
`<ab>` and `<cd>` are the first and second pairs of characters of the file's
|
||||
name.
|
||||
@@ -193,22 +193,29 @@ bytes are stored twice, once under each URL. A source image is stored by writing
|
||||
its file and then its row; when the row cannot be written, the file is removed
|
||||
again.
|
||||
|
||||
Source and transformed image files, and the `.meta` file beside each transformed
|
||||
image, are written under a temporary name, synced to disk, and only then renamed
|
||||
into place, so a crash leaves no file under its own name with part of its data.
|
||||
When pixa starts, and every five minutes after, it removes each cached source or
|
||||
transformed image whose file is empty, or not the size the database records for
|
||||
it, with a warning naming the file; the next request for it fetches the source
|
||||
or converts the image again. Looking for those files reads the database once for
|
||||
every cached file, so pixa skips each five-minute look that would start less
|
||||
than ten times as long after the last look ended as that look took. After a look
|
||||
that took one minute, the next one starts 10 to 15 minutes after that look
|
||||
ended.
|
||||
Source and transformed image files are written under a temporary name, synced to
|
||||
disk, and only then renamed into place, so a crash leaves no file under its own
|
||||
name with part of its data. When pixa starts, and every five minutes after, it
|
||||
removes each cached source or transformed image whose file is empty, or not the
|
||||
size the database records for it, with a warning naming the file; the next
|
||||
request for it fetches the source or converts the image again. Looking for those
|
||||
files reads the database once for every cached file, so pixa skips each
|
||||
five-minute look that would start less than ten times as long after the last
|
||||
look ended as that look took. After a look that took one minute, the next one
|
||||
starts 10 to 15 minutes after that look ended.
|
||||
|
||||
pixa's target is 1-5k r/s, which has not been measured at that rate (see Load
|
||||
Test). Toward it, pixa keeps in memory the content types of the 10,000
|
||||
transformed images most recently cached or served, so a cache hit on one of them
|
||||
reads only the image file from disk and not the metadata file stored beside it.
|
||||
Test). Toward it, a cache hit opens the transformed image's file and nothing
|
||||
else, and makes no database call. The content type comes from the file's first
|
||||
bytes, which are those of the format pixa wrote it in; a file whose first bytes
|
||||
are not those of a format pixa writes is not served, and the image is converted
|
||||
again. The time the image was last served, which eviction orders by, and the hit
|
||||
count are handed to a background writer without waiting for it, as are the
|
||||
counts of misses, fetches and conversions. Once a second that writer writes the
|
||||
waiting times in one transaction and the counts in one statement. While 10,000
|
||||
times wait to be written, the time of a further hit is dropped, and that image
|
||||
only looks less recently used to eviction. `script/bench` measures how many hits
|
||||
pixa serves per second (see Load Test).
|
||||
|
||||
### Routes
|
||||
|
||||
@@ -846,6 +853,9 @@ provide:
|
||||
- `script/docker-smoke` — build the image, start it, wait for it to be healthy
|
||||
- `script/loadtest` — measure pixad's throughput, latency and peak memory; a
|
||||
benchmark, not part of `script/check` (see Load Test)
|
||||
- `script/bench` — run the Go benchmark that serves cache hits in parallel
|
||||
through the image handler: build the `bench` phase of the `Dockerfile`; a
|
||||
benchmark, not part of `script/check` (see Load Test)
|
||||
- `script/cibuild` — CI entrypoint: run `script/bootstrap` (without `--cgo`),
|
||||
then `script/check`, then build the image as `script/docker` does
|
||||
- `script/precommit` — pre-commit checks (`go mod tidy` guard, then
|
||||
@@ -860,7 +870,7 @@ instead of coming from the build cache. Above the line are the steps that
|
||||
install system packages and download the Go modules; Docker keeps their layers
|
||||
until the base image, the step itself, `go.mod` or `go.sum` changes. Go's build
|
||||
cache is kept from build to build: the steps that compile mount it, so a package
|
||||
whose inputs are unchanged is not compiled again, and both `go test` runs pass
|
||||
whose inputs are unchanged is not compiled again, and every `go test` run passes
|
||||
`-count=1` so that no stored test result is replayed. The lint step also mounts
|
||||
golangci-lint's cache, which reuses a package's findings only when its files,
|
||||
those of the packages it imports, `go.mod`, the linters and their settings in
|
||||
@@ -926,6 +936,16 @@ The numbers depend on the machine and on whatever else runs on it. The first
|
||||
measurement, made on a shared machine with few clients, is in `TODO.md`; it says
|
||||
nothing about the target.
|
||||
|
||||
`script/bench` (or `make bench`) measures cache hits alone, without the network
|
||||
and with no pixad process: a Go benchmark in `internal/handlers` serves one
|
||||
cached image through the `/v1/image/` handler from as many goroutines at once as
|
||||
the machine has CPUs, and reports the hits served per second as `hits/s`. Each
|
||||
run starts with an empty cache and an empty database, a SQLite file of its own,
|
||||
and puts the image in the cache before it measures. The benchmark fails if
|
||||
serving the hits ran any statement on the database. Like the load test it is not
|
||||
part of `script/check`; it needs Docker, as it runs in the `bench` phase of the
|
||||
`Dockerfile`, and prints the result with the build's output.
|
||||
|
||||
## TODO
|
||||
|
||||
See [TODO.md](TODO.md) for the full prioritized task list.
|
||||
|
||||
Reference in New Issue
Block a user