Estado: ✅ Aceptada Fecha: 2026-05-08 Contexto que la motiva: auditoría de patrones de diseño para optimización de recursos (bloque 10 del roadmap interno) → commit 6cb958a.

🧭 Contexto

Pre-bloque-10 el cache de páginas del Pager era:

// storage.rs (pre-bloque-10)
pub struct Pager {
    cache: BTreeMap<u32, CachedPage>,  // ← crece sin freno
    ...
}
fn ensure_page_loaded(&mut self, no: u32) -> DbResult<()> {
    if self.cache.contains_key(&no) { return Ok(()); }
    // ... lee de disco ...
    self.cache.insert(no, CachedPage { data, dirty: false });  // ← nunca evicta
}

Sin política de eviction. Cada página leída se queda en RAM para siempre. En modo CLI (one-shot) no se nota, pero en gabysql-server -dir ./dbs corriendo días o semanas:

Es una fuga de memoria silenciosa, proporcional al working set, no a ningún parámetro configurable.

Restricciones del proyecto:

💡 Decisión

Reemplazar BTreeMap<u32, CachedPage> por un PageCache con:

  1. Capacidad fija (DEFAULT_CACHE_PAGES = 1024, configurable via Pager::set_cache_capacity(n)).
  2. LRU tracking via contador monótono: cada get/get_mut/insert bumpea counter: u64 y graba el valor en el slot accedido. La página menos recientemente usada es la del last_access más bajo.
  3. Eviction dirty-aware: cuando insert() ocurre con cache lleno, escanea las entradas, filtra solo las clean, evicta la del last_access mínimo. Las dirty nunca se evictan.
  4. Overflow controlado: si todas las entradas están dirty (edge case mid-tx con muchas writes), se permite que el cache exceda capacidad temporalmente. Drena solo en commit cuando mark_all_clean() vuelve toda la cache evictable.

Estructura:

struct PageCache {
    capacity: usize,
    map: HashMap<u32, CacheSlot>,
    counter: u64,
}
struct CacheSlot {
    page: CachedPage,
    last_access: u64,
}

API nueva en Pager (todo lo demás compatible):

🔄 Alternativas consideradas

Mantener BTreeMap sin límite

Usar el crate lru

Doubly-linked list manual con índices en Vec<Slot>

Eviction agresiva (también de dirty pages, vía writeback inmediato)

Capacity bounded + LRU clean-only (decisión)

📊 Consecuencias

Positivas

Negativas

Neutras

🔗 Referencias