review(p9-fb-19): 회차 1 nit 반영

- `App::search` 의 두 cache.lock() 호출이 mutex poison 시 silently
  bypass 하던 것을 `unwrap_or_else(|e| { warn!; e.into_inner() })`
  recovery 로 교체. cache 가 poison 됐어도 다음 호출은 정상이고
  한 번은 warn 로그가 남아 panic 흔적 추적 가능. lookup 후 lock
  drop → retriever 호출 → 재 lock 으로 lock granularity 도 짧게.
- `clear_search_cache` 도 같은 recovery 패턴.
- `SearchCacheKey` doc 에 spec 와 impl 의 naming 차이 (index_version
  vs corpus_revision) 명시 + HOTFIXES entry 추가. spec 의 index_
  version 명칭이 design §9 의 기존 `IndexVersion` newtype (embedding
  -index identity 라벨) 과 충돌해서 corpus_revision 으로 rename.

7 tests/search_lexical 통과. clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 05:07:48 +00:00
parent 0e408fb1b5
commit d36667589f
2 changed files with 57 additions and 17 deletions

View File

@@ -89,6 +89,14 @@ pub struct App {
/// Lexical mode has no embedding identity → empty string in that
/// slot, harmless because the rest of the key still distinguishes
/// queries.
///
/// **Naming note**: spec p9-fb-19 calls the invalidation counter
/// `index_version`, but the impl renames it to `corpus_revision` to
/// avoid confusion with the pre-existing `IndexVersion` newtype
/// (design §9 — embedding-index identity label, a completely
/// different concept). The `corpus_revision` row in the §9
/// versioning table documents the new dimension; HOTFIXES entry
/// tracks the rename.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct SearchCacheKey {
pub query_norm: String,
@@ -161,22 +169,35 @@ impl App {
// *subsequent* identical queries.
let key = self.build_cache_key(&query)?;
// Lock the cache long enough to lookup; clone the hit out so
// we can drop the lock before returning.
if let Ok(mut guard) = cache.lock() {
if let Some(hits) = guard.get(&key) {
tracing::debug!(
target: "kebab-app",
cache = "hit",
corpus_revision = key.corpus_revision,
"search served from LRU cache"
);
return Ok(hits.clone());
}
// we can drop the lock before returning. Mutex poison
// recovery: `into_inner()` of a poison error returns the
// (still-valid) underlying guard so we can keep using the
// cache after a panic in another thread. Log once so the
// poison itself is visible — the cache is still functional
// but a panic in a previous search is worth knowing about.
let mut guard = cache.lock().unwrap_or_else(|e| {
tracing::warn!(
target: "kebab-app",
"search_cache mutex was poisoned; recovering and continuing — \
a previous search-thread panic preceded this call"
);
e.into_inner()
});
if let Some(hits) = guard.get(&key) {
tracing::debug!(
target: "kebab-app",
cache = "hit",
corpus_revision = key.corpus_revision,
"search served from LRU cache"
);
return Ok(hits.clone());
}
// Drop the lock before the (potentially slow) retriever call
// so other in-flight searches can use the cache concurrently.
drop(guard);
let hits = self.search_uncached(query)?;
if let Ok(mut guard) = cache.lock() {
guard.put(key, hits.clone());
}
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
guard.put(key, hits.clone());
Ok(hits)
}
@@ -375,9 +396,8 @@ impl App {
/// clear` admin command). No-op when the cache is disabled.
pub fn clear_search_cache(&self) {
if let Some(cache) = self.search_cache.as_ref() {
if let Ok(mut guard) = cache.lock() {
guard.clear();
}
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
guard.clear();
}
}

View File

@@ -14,6 +14,26 @@ historical contract that was implemented; this file accumulates the
deltas so phase 5+ readers can find the live behavior without diffing
git history.
## 2026-05-03 — p9-fb-19 spec `index_version` → impl `corpus_revision` rename
**Spec amended**: `tasks/p9/p9-fb-19-search-cache.md` (frozen — original
contract uses `index_version` for the monotonic counter that ingest
bumps and `App::search` snapshots into its cache key).
**Why renamed**: design §9 already has an `index_version` identifier
(`IndexVersion` newtype, used in the §4.2 `index_id` recipe and on
`SearchHit`) — a *string label* for embedding-index identity. Reusing
the name for the monotonic u64 counter would collide silently on every
grep / type-search.
**Live name**: `corpus_revision` (added as a new row in design §9
versioning table). `SqliteStore::corpus_revision()` /
`bump_corpus_revision()` methods + `kv['corpus_revision']` row.
`SearchCacheKey.corpus_revision` field on `App`.
**Behavior unchanged**: every other detail (monotonic, ingest-commit
bump, in-key snapshot, no-bump on no-op reingest) matches the spec.
## 2026-05-02 — Config defaults: LLM = gemma4:e4b + workspace.root tilde expansion
**Discovered**: 사용자가 도그푸딩 환경에 `kebab init` 으로 생성된 `~/.config/kebab/config.toml` 검토하던 중.