Batch cache hit writes - #383
Conversation
b21b7bd to
4827cb9
Compare
Every cached download wrote its hit in its own UPDATE. SQLite runs on one connection, so each download was a write transaction the rest queued behind, holding a warm npm cache near 1,000 requests a second with the CPU two thirds idle. Hits are now counted in memory and written in one transaction every database.hit_flush_interval, "1s" by default, "0" for a write per hit. A failed write keeps the hits for the next flush, and Close writes what is pending. The same test then served about 2,200 requests a second. Closes git-pkgs#322.
andrew
left a comment
There was a problem hiding this comment.
One fix needed before merge: writeHits in internal/database/hits.go unconditionally replaces last_accessed_at and updated_at with the batch timestamp. When proxies share a database, an older batch can flush after a newer hit has already been persisted and move both timestamps backwards. This can change LRU eviction order.
I reproduced this through two running proxy processes sharing a temporary SQLite database. One buffered a cache hit with hit_flush_interval: "1h"; the other then recorded a newer hit with batching disabled. Shutting down the first proxy flushed its older timestamp over the newer one. The hit count correctly increased from 1 to 2, but last_accessed_at moved from 12:14:02.360091 back to 12:14:02.238994.
Please keep the count increment while preserving the later of the stored and incoming timestamps, including when the stored access time is NULL. Add a regression test where an older batch flushes after a newer hit has been persisted, covering both SQLite and PostgreSQL.
With proxies sharing a database, a batch can flush after a newer hit is already written, and overwriting last_accessed_at and updated_at with its older time could reorder LRU eviction. Both now keep the later of the stored and incoming times. The count still adds up.
|
Fixed in 55ed6b9. |
Every cached download records its hit with its own
UPDATE. The proxy uses one SQLite connection, so each download is a write transaction, and every other download queues behind it. Serving npm tarballs from a warm cache on an 8 vCPU VM, that held the proxy near 1,000 requests a second with the CPU about two thirds idle. Turning off request logging and the access log changed little.This counts hits in memory and writes them in one transaction per interval.
Measured with this branch on that VM, toggling only
hit_flush_interval. The cache was warm, with 5 simultaneous simulatednpminstalls (15 connections each) from one client VM. Each run was 60 s, with two runs per setting, alternating.SQLite
hit_flush_interval"0"(each hit)"1s"Postgres 18 (default settings, the proxy's pool of 32 connections)
hit_flush_interval"0"(each hit)"1s"On SQLite, batching more than doubles throughput and cuts median latency by about 4x. On Postgres, batching brings no throughput gain; the win is half the commits and about 40% less Postgres CPU. The p99 varied from run to run on Postgres with either setting.
A separate pool of read-only SQLite connections on top of this made no measurable difference to throughput, so the single connection stays.
database.hit_flush_intervalsets the interval. It defaults to"1s", and"0"writes each hit as it happens, as before. It's also available asPROXY_DATABASE_HIT_FLUSH_INTERVAL, and it applies to both databases.Closewrites whatever is pending. Up to one interval of hits is lost if the process is killed.server.New. Tests and the mirror command open the database directly and keep writing each hit immediately.hit_count,last_accessed_atandupdated_atare delayed. They feed the stats pages and LRU eviction, which don't need them to the second.last_accessed_atorupdated_atbackwards: each keeps the later of the stored and incoming time.Tested with
go test ./..., plus-raceand a real Postgres 18 forinternal/database.Closes #322.