// finfowarm.go — build the file-history index off the request path.
//
// zit answers `log CHECKIN PATH` from an index it derives on first use
// (zit#188, shipped 0.3.14, made durable across commits in 0.3.15).
// Measured on this fleet's SQLite mirror: 104s to build, then ~0.6s for
// ANY path, because the index is per repository rather than per path.
//
// 104 seconds is not a page. So the build is done here, beside the
// listing warm and under the same guard, rather than by whichever
// reader happens to open a file's history first.
package main
import (
"context"
"log"
"time"
"craftdesign.group/quarry/internal/obs"
"craftdesign.group/quarry/internal/zitcli"
)
// warmFinfoPath is the path the warming query asks about.
//
// Deliberately one that cannot exist. A PATH query builds the whole
// index before it discovers the path is not in the tree — verified on
// this fleet — so the index is warmed without reading any real file's
// history, and without quarry needing to know a tracked path to name.
// The leading dot and the name say what it is if it ever shows up in a
// log or a trace.
const warmFinfoPath = ".quarry-warm-file-index"
// warmFinfoTimeout bounds the build.
//
// Most stores never reach it any more. Since zit 0.3.18 the index is
// built by the DERIVE, as each check-in is linked on clone, import or
// append, so it already exists when this runs and the warm is a
// sub-second confirmation. What remains is the fallback: a store whose
// cache predates that release still builds on the first query.
//
// That fallback now resumes. It checkpoints every 8,192 entries and
// records how much of the log it covers, so a build this deadline cuts
// off keeps what it walked and the next attempt continues from there.
// The bound is therefore about not holding a core indefinitely in the
// background, NOT about a build that would otherwise be wasted — it
// used to be both, and the difference is why a store is retried now
// rather than given up on.
//
// It ran on a bare context.Background() until this was added, which is
// how the pkgsrc mirror paid for a full non-resumable build on every
// one of roughly fifteen deploys in a day and still had no index at the
// end of it. Measured there (218,444 check-ins): 34.3 min under 0.3.16,
// ~11 min under 0.3.17, and under 0.3.18 the cost moves into the derive
// the clone already pays. Fifteen minutes leaves room for the slowest
// fallback this fleet has.
// A var, not a const, so the test can prove the deadline fires.
var warmFinfoTimeout = 15 * time.Minute
// warmFileHistory builds or extends the index, and says how long it took
// when that was long enough to matter.
//
// A failure that is not the deadline is dropped on purpose: the query
// names a path that does not exist, so "no such path" IS the success
// case, and a store that cannot answer at all has already announced
// itself through ingest. The only cost is that the next reader of a
// file's history pays what this would have.
func warmFileHistory(cli *zitcli.Client, tip, repo string) {
ctx, cancel := context.WithTimeout(
obs.WithReason(context.Background(), obs.ReasonWarm), warmFinfoTimeout)
defer cancel()
start := time.Now()
_, _ = cli.FileHistory(ctx, tip, warmFinfoPath, 1)
// The deadline is read from the context rather than from the error,
// because FileHistory turns a killed subprocess into an ordinary
// failure and "no such path" into no error at all. The context is
// the only thing here that knows WHY the call ended.
if ctx.Err() != nil {
// Not a dead end: zit 0.3.18 checkpoints the partial index, so
// the next tip move continues this walk instead of repeating
// it. Said once per cut-off so an operator can see a store
// converging across several rather than assume it is stuck.
log.Printf("file-history index for %s did not finish within %s; "+
"progress is checkpointed and the next attempt resumes it",
repo, warmFinfoTimeout)
return
}
// A warm index answers in well under a second; anything near the
// build cost means this repository had none, which is worth one
// line saying where the time went.
if d := time.Since(start); d > 5*time.Second {
log.Printf("file-history index for %s built in %s", repo, d.Round(time.Second))
}
}