1 Commits
Author SHA1 Message Date
clawbot dc1a78324a Serve a cache hit with no database read or write (closes #309)
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.
Its access time and hit count, and the access time of a cached source a
miss uses, are handed without waiting to the goroutine that wrote late
counts, which now writes every count and access time once a second;
each kind of access time waits 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. make bench
runs a parallel cache hit benchmark with Go on the host, outside make
check.

Model: opus-5-5
2026-10-21 15:35:22 +00:00
2 changed files with 35 additions and 32 deletions
+27 -23
View File
@@ -512,16 +512,12 @@ func (c *Cache) reconcileAccounting(ctx context.Context) error {
return c.reconcileUsageTotal(ctx) return c.reconcileUsageTotal(ctx)
} }
// reconcileFiles walks baseDir, one of the cache's storage directories, // reconcileVariantFiles walks the variant storage directory, checking
// sweeping stale temp files and passing each other file, with its name, to // each variant file against its accounting row (reconcileVariantFile)
// reconcileFile. Once ctx is cancelled it stops at the next file and // and sweeping stale temp files.
// returns ctx's error. func (c *Cache) reconcileVariantFiles(ctx context.Context) error {
func (c *Cache) reconcileFiles(
ctx context.Context, baseDir string,
reconcileFile func(path string, entry fs.DirEntry, name string) error,
) error {
return filepath.WalkDir( return filepath.WalkDir(
baseDir, c.variants.baseDir,
func(path string, entry fs.DirEntry, err error) error { func(path string, entry fs.DirEntry, err error) error {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
@@ -539,21 +535,11 @@ func (c *Cache) reconcileFiles(
return nil return nil
} }
return reconcileFile(path, entry, name) return c.reconcileVariantFile(ctx, path, entry, imagerequest.VariantKey(name))
}, },
) )
} }
// reconcileVariantFiles walks the variant storage directory, checking
// each variant file against its accounting row (reconcileVariantFile)
// and sweeping stale temp files.
func (c *Cache) reconcileVariantFiles(ctx context.Context) error {
return c.reconcileFiles(ctx, c.variants.baseDir,
func(path string, entry fs.DirEntry, name string) error {
return c.reconcileVariantFile(ctx, path, entry, imagerequest.VariantKey(name))
})
}
// reconcileVariantFile checks one variant file against its accounting // reconcileVariantFile checks one variant file against its accounting
// row. A file without a row is adopted into the accounting, unless it is // row. A file without a row is adopted into the accounting, unless it is
// empty. A file that is empty, or not the size its row records, as a // empty. A file that is empty, or not the size its row records, as a
@@ -710,10 +696,28 @@ func queryStringColumn[T ~string](
// each blob file against its source_content row (reconcileSourceFile) // each blob file against its source_content row (reconcileSourceFile)
// and sweeping stale temp files. // and sweeping stale temp files.
func (c *Cache) reconcileSourceFiles(ctx context.Context) error { func (c *Cache) reconcileSourceFiles(ctx context.Context) error {
return c.reconcileFiles(ctx, c.srcContent.baseDir, return filepath.WalkDir(
func(path string, entry fs.DirEntry, name string) error { c.srcContent.baseDir,
func(path string, entry fs.DirEntry, err error) error {
if ctx.Err() != nil {
return ctx.Err()
}
if err != nil || entry.IsDir() {
return err
}
name := entry.Name()
if strings.HasPrefix(name, tempFilePrefix) {
c.sweepStaleTempFile(path, entry)
return nil
}
return c.reconcileSourceFile(ctx, path, entry, imagerequest.SourceKey(name)) return c.reconcileSourceFile(ctx, path, entry, imagerequest.SourceKey(name))
}) },
)
} }
// reconcileSourceFile checks one source blob file against its // reconcileSourceFile checks one source blob file against its
@@ -155,12 +155,11 @@ func TestService_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy(t *testing.T) {
waitForCounters(t, svc.cache, cacheStatsCounters{missCount: 1}) waitForCounters(t, svc.cache, cacheStatsCounters{missCount: 1})
} }
// TestIncrementStats_ManyCountsLeaveOneWriteWaiting holds the database's one // TestIncrementStats_ManyCountsPastTheirDeadlineLeaveOneWriteWaiting holds the
// connection while many misses are added to the pending counts, which the // database's one connection while many misses are counted past their
// goroutine StartPendingCountWrites starts writes to the database. At most one // deadline. At most one write may then wait for the connection, and every
// write may then wait for the connection, and every miss must reach the // miss must reach the database once it is free.
// database once it is free. func TestIncrementStats_ManyCountsPastTheirDeadlineLeaveOneWriteWaiting(t *testing.T) {
func TestIncrementStats_ManyCountsLeaveOneWriteWaiting(t *testing.T) {
t.Parallel() t.Parallel()
cache, _ := newEvictionTestCache(t, 1<<30) cache, _ := newEvictionTestCache(t, 1<<30)
@@ -195,9 +194,9 @@ func TestIncrementStats_ManyCountsLeaveOneWriteWaiting(t *testing.T) {
waitForCounters(t, cache, cacheStatsCounters{missCount: misses}) waitForCounters(t, cache, cacheStatsCounters{missCount: misses})
} }
// TestStats_IncludesPendingCounts adds hits and misses to the pending counts, // TestStats_IncludesPendingCounts counts hits and misses whose writes miss
// with no goroutine started to write them, and checks that Stats adds them to // their deadline, and checks that Stats adds them to the database's counts
// the database's counts before they are written. // before they are written.
func TestStats_IncludesPendingCounts(t *testing.T) { func TestStats_IncludesPendingCounts(t *testing.T) {
t.Parallel() t.Parallel()