Skip to content
quarry Timeline
Login

Timeline

10 most recent check-ins occurring on or after 2026-08-17 20:51:22.

More ↑
2026-08-18
01:59
repo: a create flow a person can actually use

Nobody picks a filesystem path when they make a repository on a forge, and requiring one made repo.add usable only by the machine's operator. --repos-root gives the server somewhere to put them; path is now optional and the response says where it landed.

Repositories gain a description — every forge has one and quarry had nowhere to put it, so the listing could only show names and counts.

Three surfaces on one op: quarry repo new|clone|list|remove drives the HTTP API rather than the database, so the CLI has no privileged path and anything it can do a client can; /new is the same three questions a forge always asks; and an empty repository shows how to push to it instead of a bare file table, because it is waiting, not broken.

A rejected form comes back as a form with the reason on it — an error page throws the reader's input away. check-in: 87a492fe4f user: dmestas tags: trunk

01:47
repo.add: create an empty repository or adopt one by cloning

Two cases quarry could not do at all. create runs zit init at the path — the 'host my project here' case. clone copies a remote in and records a mirror for it, because adopting a remote repository and then not tracking it is almost never what anyone meant.

Cloning takes the bulk lane: a large repository is minutes of network, and an admin asking for it is not a reason to run it unbounded.

Both refuse a non-empty target and undo what they made if a later step fails — the store, the OLTP row, or both. Verified: sqlite.org/docsrc adopted in 14s (5,116 check-ins, serving in 2.24ms), and a clone from an unreachable URL leaves no directory, no registration and no row. check-in: 9f78830644 user: dmestas tags: trunk

01:24
repo: add and remove repositories without restarting

Adding a repository meant editing the server's command line and restarting, which took every other repository down with it. The registry now lives in OLTP where 0001 §8 always placed it, and repo.add / repo.remove (admin) register and tear down at runtime.

registrar owns a repository's whole lifetime — index handle, pipeline, sync backend, poll goroutine. The startup loop could not: it relied on defer ix.Close() reaching serve's return and on a poll loop that never stops, and both leak the moment a repository is removed.

Add persists before it serves, and rolls the record back if opening fails: a registration that serves without being recorded vanishes at the next restart with no trace of why. Remove stops serving and forgets, but leaves the store and index on disk — 'stop serving' and 'destroy' are different intentions and only one is reversible.

--repo stays as bootstrap and override. check-in: 2c157378a3 user: dmestas tags: trunk

01:15
api: make the repo set a concurrent registry; serve rejects positionals

Deps.Repos was a bare map, read by every op on the request path and written only at startup. Adding repositories at runtime — the point of the registry ticket — makes that a data race with no symptom until it corrupts one. api.Repos owns the lock; a *Repo is published complete or not at all, and Delete hands back the entry so the caller can close the index and stop the pipeline.

serve now refuses leftover arguments. Go's flag package stops at the first non-flag and returns the rest, so one unquoted shell variable silently dropped twelve --repo flags and quarry served the remaining eleven without a word. check-in: 4a479e6ce2 user: dmestas tags: trunk

2026-08-17
22:47
ingest: bound how many stores stream at once

zit frames streams a WHOLE store: 507 MB peak on sqlite, more on tcl. quarry ran one ingest goroutine per repository with nothing between them, so a cold start — or a mirror round landing artifacts in several big stores at once — ran that many censuses simultaneously and the memory added. Observed as zit spiking to gigabytes after pulling sqlite and tcl together.

--ingest-limit (default 3) puts them in a work lane. The queue is generous and ingest is never shed: dropping a round would leave an index behind its store until the next poll, trading a correctness surprise for a saving the limit already delivers. check-in: e6fd32e82e user: dmestas tags: trunk

21:32
web: map op error kinds instead of collapsing them to 500

fail() mapped only ErrNotFound, so every other kind rendered as 500 — and a 500 tells a client the request failed, not to try again. Two retryable paths lost that distinction: a single-writer lease refusal (0001 §3) and a shed from a saturated work lane.

ErrInvalid now renders 400 and ErrUnavailable 503 with Retry-After, matching what the API router has always done.

Verified with the archive endpoint, live for the first time now that zit 10a1236a7a fixes tarball -R: twenty concurrent requests give ten 200s and ten 503s with Retry-After, RSS bounded at 247 MB. check-in: e9d441a552 user: dmestas tags: trunk

21:26
work: bound bulk operations; footer times keep their precision

Nothing in quarry limited concurrency — every zit subprocess ran in the request goroutine with the arrival rate as the only bound. work.Lane is a concurrency limit plus a queue cap that SHEDS past it: a request accepted into a queue it cannot be served from consumes the capacity that would have served someone else.

Wired to archive first, which buffers 256 MiB per request on an anonymous route. That is latent rather than live — archive 503s on zit#97 — which is why the bound belongs there now, so the endpoint is born bounded rather than acquiring a limit after someone finds it.

Footer precision: '<1ms' was true and useless once most pages went sub-millisecond, collapsing the interesting range into one label. timing.Format keeps ~3 significant figures across five orders of magnitude, so 900µs and 90µs no longer read the same. Fossil has the mirror-image bug: three decimals of seconds prints everything faster than a millisecond as 0.000s. check-in: 45bb75ade2 user: dmestas tags: trunk

21:14
zitcli: byte-bounded content cache for hash-versioned reads

zit cat costs ~147ms even for a small file, against a 3ms process floor, and every file view forked one. Under load that capped the forge at 32 req/s, degrading to 24 at twenty concurrent readers.

hash-versioned file page: 393ms -> 18ms 50 concurrent: 201 req/s, still climbing

Bounded by BYTES, not entries: values are file contents, and an entry-counted cache holding a 30MB blob is a memory leak with a polite name. Per repository, so a busy store cannot evict a quiet one's entries.

resolveVersion only expands hash PREFIXES. An earlier draft fell back to the index's branch table for names, which served tk a README from 2020: tk renamed its trunk to main, so a branch literally called trunk still exists and is six years stale, while zit resolves the word correctly. 0001 §2 makes zit the authority on names and that is not negotiable — so a symbolic version stays uncached, and the way to fix that is to make links carry the resolved hash. check-in: 0cf9a57e88 user: dmestas tags: trunk

21:00
index: fold ticket summaries at write time

The ticket list derived itself per request: GROUP BY over every change row (tk: 35,903), two correlated subqueries per ticket to find the newest title and status — 11,618, each with its own temp b-tree — and a HAVING that filtered after grouping, so paging saved nothing.

tk 200ms -> 1ms sqlite 1ms fossil <1ms

Written from refreshTicketDoc, which already folds exactly this state for the search doc, so the summary cannot disagree with what search sees. Backfilled at open for indexes written before the table existed.

Second instance of the same bug the timeline had, so plan_test now covers this query too. check-in: 4c973ff323 user: dmestas tags: trunk

20:51
web: report where a request's time went, not just how long

The footer said 'rendered in 344ms'; it now says 'rendered in 344ms (zit 270ms)'. One number says a page is slow, not which layer to look at, and quarry's layers differ by three orders of magnitude — an index read is microseconds, zit ls --long on tk is 2.6 seconds.

The accumulator rides the request context, which is what makes it correct under load: package-level counters would blend concurrent requests together, and concurrency is what this exists to measure.

zit read calls take a context now and use exec.CommandContext, so an abandoned request kills its subprocess instead of leaving it to finish a 2.6-second listing nobody will read.

SQL is not instrumented yet; the index does not take a context. check-in: 85b9909d1c user: dmestas tags: trunk

More ↓