Skip to content

Commit d450a8a

Browse files
beetlebugorgclaude
andcommitted
fix(server): keep the cell index fresh on add / update / remove
The name→bbox index could drift: build() skipped already-indexed cells (so a re-imported cell kept stale bounds) and never dropped entries for removed cells. Now build() reconciles against ENC_ROOT — pruning entries whose cell is no longer on disk — and an import forget()s the cells it re-caches so the rebuild re-parses their bounds. (Search results were already remove-safe: serveCells lists live ENC_ROOT dirs, looking up bbox in the index, so a removed cell never appears even before the prune.) Add/update/remove all covered; verified by test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2ba0cd7 commit d450a8a

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

internal/engine/server/cellindex.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,16 @@ func (ci *cellIndex) build() {
102102
ci.mu.Unlock()
103103
return
104104
}
105-
n, added := 0, 0
105+
present := make(map[string]bool, len(entries))
106+
added := 0
106107
for _, e := range entries {
107108
if !e.IsDir() || !isCellName(e.Name()) {
108109
continue
109110
}
110111
name := e.Name()
112+
present[name] = true
111113
if _, ok := ci.get(name); ok {
112-
continue // already indexed
114+
continue // already indexed (forget() drops a re-imported cell so it re-parses)
113115
}
114116
data, err := os.ReadFile(filepath.Join(ci.encRoot, name, name+".000"))
115117
if err != nil {
@@ -127,10 +129,30 @@ func (ci *cellIndex) build() {
127129
if added%200 == 0 {
128130
ci.save() // periodic checkpoint for a long backfill
129131
}
130-
n++
131132
}
132-
if added > 0 {
133+
// Reconcile: drop entries for cells no longer on disk (removed packs/cells), so
134+
// the index never reports a chart that isn't installed anymore.
135+
removed := 0
136+
ci.mu.Lock()
137+
for name := range ci.bbox {
138+
if !present[name] {
139+
delete(ci.bbox, name)
140+
removed++
141+
}
142+
}
143+
ci.mu.Unlock()
144+
if added > 0 || removed > 0 {
133145
ci.save()
134-
log.Printf("cell index: backfilled %d cell bound(s) → %s", added, ci.path)
146+
log.Printf("cell index: +%d / -%d cell bound(s) → %s", added, removed, ci.path)
135147
}
136148
}
149+
150+
// forget drops cells from the index so the next build re-parses them — used when
151+
// an import re-caches a cell whose bounds may have changed.
152+
func (ci *cellIndex) forget(names []string) {
153+
ci.mu.Lock()
154+
for _, n := range names {
155+
delete(ci.bbox, n)
156+
}
157+
ci.mu.Unlock()
158+
}

internal/engine/server/cellindex_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,43 @@ func TestCellIndexBuild(t *testing.T) {
5353
t.Errorf("reload mismatch: %v vs %v (ok=%v)", bb2, bb, ok)
5454
}
5555
}
56+
57+
// TestCellIndexFreshness: rebuild prunes a removed cell, and forget() drops one so
58+
// it re-indexes — the add/update/remove freshness contract.
59+
func TestCellIndexFreshness(t *testing.T) {
60+
const cell = "US4MD81M"
61+
data, err := os.ReadFile("../../../testdata/" + cell + ".000")
62+
if err != nil {
63+
t.Skipf("testdata cell absent: %v", err)
64+
}
65+
dir := t.TempDir()
66+
cdir := filepath.Join(dir, "ENC_ROOT", cell)
67+
if err := os.MkdirAll(cdir, 0o755); err != nil {
68+
t.Fatal(err)
69+
}
70+
if err := os.WriteFile(filepath.Join(cdir, cell+".000"), data, 0o644); err != nil {
71+
t.Fatal(err)
72+
}
73+
ci := newCellIndex(dir)
74+
ci.build()
75+
if _, ok := ci.get(cell); !ok {
76+
t.Fatal("not indexed")
77+
}
78+
// forget → re-build re-parses it (update path).
79+
ci.forget([]string{cell})
80+
if _, ok := ci.get(cell); ok {
81+
t.Fatal("forget did not drop the entry")
82+
}
83+
ci.rebuild()
84+
if _, ok := ci.get(cell); !ok {
85+
t.Fatal("rebuild did not re-index after forget")
86+
}
87+
// remove the cell on disk → rebuild prunes it (remove path).
88+
if err := os.RemoveAll(cdir); err != nil {
89+
t.Fatal(err)
90+
}
91+
ci.rebuild()
92+
if _, ok := ci.get(cell); ok {
93+
t.Error("rebuild did not prune a removed cell")
94+
}
95+
}

internal/engine/server/import.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,12 @@ func (s *Server) cacheCells(cells map[string]baker.CellData) {
330330
}
331331
}
332332
if s.cellIdx != nil {
333-
go s.cellIdx.rebuild() // index the newly-cached cells' bounds in the background
333+
stems := make([]string, 0, len(cells))
334+
for name := range cells {
335+
stems = append(stems, strings.TrimSuffix(name, ".000"))
336+
}
337+
s.cellIdx.forget(stems) // re-imported cells: drop stale bounds so the rebuild re-parses
338+
go s.cellIdx.rebuild() // index the (re-)cached cells' bounds in the background
334339
}
335340
}
336341

0 commit comments

Comments
 (0)