cache headers and data versioning #4
Labels
No labels
claude-wip
map-report
map:is
map:se
needs-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
art/ismap#4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
mapsrv needs cache headers/etags. I'm guessing the fonts, icons and style and such should be pretty trivial, but for the actual tiles we need to figure out some data versioning scheme.
night-shift — worked this issue unattended.
Issue #4 — cache headers and data versioning
Branch:
night/issue-4-cache-headers(commit85ba0d4, not pushed, not merged)State: done
What changed
New
mapsrv/cache.goplus small edits totiles.go,assets.go,main.go,sprite.goandcompress.go. One policy, applied per handler kind:public, max-age=60, stale-while-revalidate=604800+ weak ETaghtml/(index, style.json)no-cache, revalidated onhttp.FileServer'sLast-Modified/api/*no-storeFonts/sprites (the "trivial" half): the ETag is a hash of the bytes. They are a
pure function of the mapsrv binary, so content is its own version and a rebuild that
changes one sprite invalidates exactly that sprite. The glyph cache now stores an
asset(content type + body + etag) instead of a bare[]byte, so the hash iscomputed once per range rather than per request.
Tiles — the data versioning scheme. The tile ETag is a fingerprint of the
database, not of the tile, so a 304 is answered from a cached string with no
ST_AsMVTcall and no postgres round trip at all.The importer already produces the signal for free: every layer goes live by an atomic
swap that DROPs the old table and RENAMEs a freshly built one into its place
(
layer.swapSQL/swapSQLShared), so a live table's OID changes on every import,regeneralize or partition swap and at no other moment.
dataVersionSQLmd5s thoseOIDs together with the source text of the
zxy_*functions (which areCREATE OR REPLACEd, so they keep their OID while changing what they return).%_newtables are excluded so the version does not flap for the hours an importspends building them. mapsrv re-reads it at most every 30 s, by whichever request
finds it stale; everyone else keeps serving the version they have rather than
queueing behind a catalog read.
Why the catalog and not a metadata table the importer writes: it works on a
database imported before this code existed (including the current one — no reimport
is pending for this change), it needs no importer change at all, and there is no
second mechanism to keep in sync. Two accepted costs, both documented in CLAUDE.md:
it is one version for the whole database rather than per source (a tile mixes every
layer of its source anyway, and an import touches many layers), and it moves when a
staging table is rebuilt, which changes no served byte — one extra conditional GET
per tile, after an import that was going to invalidate them regardless.
Why a validator and a 60 s window rather than a long max-age: none of these URLs
carries a version.
/tiles/map/11/1096/553means "the current tile", and the onlyhonest way to keep it current without versioning the URL is to let the client ask.
max-age=60exists to keep a single panning session off the network, not to cacheacross imports.
Two smaller things fell out:
cacheable()after the queryreturns), so a 503 from a missing
zxy_*function is never stored under the tile'sURL.
Vary: Accept-Encodingis now set unconditionally by the gzip middleware, notonly when it compresses. It describes what the server looked at, and now that these
responses are cacheable a shared cache must not hand a gzipped tile to a client that
never asked for one. (Pre-existing bug, harmless until now.)
Documented in CLAUDE.md ("Caching, and the data version", under Running the server,
plus the two new files in the key-files list) and measured in
docs/performance.md.Verification
make build,cd mapsrv && go test ./...,go vet ./...— all clean.mapsrv/cache_test.go: weak/strong/list/*If-None-Match comparison; a tile304 served by a server with a nil pool (if the handler reached the database the
test would panic — that is the assertion that a 304 costs no query); data-version
TTL expiry; sprite revalidation and that the two scales do not share an ETag;
static
no-cache+Last-Modified→ 304;/api/*no-storeon both success anderror;
Varypresent with and withoutAccept-Encoding.:8099against the live database (the user's:8080serverwas left running its old binary and untouched). Every route checked with curl:
tile 200 →
ETag: W/"36959ccc…"+ policy; same ETag back →304, empty body;stale ETag → 200. Sprites, glyphs, style.json and
/api/issuesall as designed.psql. A rolled-back
BEGIN; CREATE TABLE …; ROLLBACKprobe confirmed thefingerprint moves when a public table appears and does not move for a
_newtable, and that the database was unchanged afterwards. Independently, the tables
with the highest OIDs in the live database are exactly
map_roads_*_pl, the onesthe last commit on main swapped — which is the mechanism working.
/?map=seloadedtwice: 66 style layers, 7315 rendered features, no console errors either time, and
sprites came from the browser cache on the second load. (Tile and glyph requests
are issued from MapLibre's worker, so they do not appear in the page session's
Network events — those were verified with curl instead.)
Left undone / uncertain
probe and by the OID evidence from the last commit's swap, but nobody has watched
the version actually move across a live
make import-*. Worth a glance at thenext import: the startup log line
mapsrv: data version <hash>should differbefore and after, and
make restartis needed for that log line (the value itselfrefreshes on its own within 30 s).
oversight. If per-source granularity is ever wanted, the honest way is for the
importer to record a version per layer and for mapsrv to learn which layers each
source has — which today it cannot, since
conf/mapsrv.yamllists sources only.max-age=60is a judgement call with no data behind it. It is the knob to turnif this ever goes behind a CDN, and it needs no ETag change.
mapsrv/sprite.gohas a pre-existinggofmtcomplaint (comment alignment in theshieldstable) that predates this branch; left alone to keep the diff honest.night-shift — worked this issue unattended.
Result is in #32 — #32
Branch
night/issue-4-cache-headers; the review note is the pull request description.