Storage Tiers¶
Grafeo's section data (LPG store, RDF store, vector indexes, ring index, etc.) lives in one of three tiers at any moment. The buffer manager moves data between tiers in response to memory pressure; configuration overrides let you pin specific sections.
The three tiers¶
| Tier | Where the data lives | When |
|---|---|---|
InMemory | Heap (Rust Arcs, Vecs, HashMaps) | Default for fresh inserts |
OnDisk | Memory-mapped file in the spill directory | After a spill |
Uninitialized | Section registered but holds no data yet | Right after database open with no inserts |
Reads work the same in either tier. The on-disk path serves through the OS page cache; cold pages fault in lazily.
Default behavior (Auto)¶
By default every section is TierOverride::Auto. The buffer manager tracks a unified budget (default 75% of system RAM) and reacts to four pressure levels:
| Level | Allocated | Action |
|---|---|---|
| Normal | < 70% | No action |
| Moderate | 70-85% | Proactive eviction of cold data |
| High | 85-95% | Aggressive eviction, trigger spilling |
| Critical | > 95% | Block new allocations |
When pressure crosses High, the buffer manager picks consumers in priority order (lowest-priority first, e.g., query caches before graph storage) and asks each to spill or evict until the budget recovers.
Explicit overrides¶
Use Config::with_section_tier to pin a specific section to a tier:
use grafeo_engine::{Config, GrafeoDB};
use grafeo_common::storage::{SectionType, TierOverride};
// Force the vector indexes to mmap mode at database open.
let config = Config::persistent("/path/to/db.grafeo")
.with_section_tier(SectionType::VectorStore, TierOverride::ForceDisk);
let db = GrafeoDB::with_config(config)?;
| Override | Behavior |
|---|---|
Auto | Default. Buffer manager decides based on pressure. |
ForceDisk | At database open, the matching section is spilled immediately. Subsequent reads serve from mmap. |
ForceRam | This section is pinned in RAM. The buffer manager skips it in every spill path (pressure-driven, explicit spill_all, targeted spill_consumer_by_name). When pressure exceeds the budget and no other spillable consumers exist, allocations fail rather than spilling a ForceRam consumer. |
ForceDisk is targeted: only the matching section is spilled, other sections are unaffected. Configure each section type that should start on disk; the rest follow the Auto policy. Only CompactStore, VectorStore, TextIndex, RdfRing and PropertyIndex can be moved to disk: Config::validate (and so the open) refuses ForceDisk for Catalog, LpgStore, RdfStore, OverlayDeletions and TimeIndex, and set_stored_config refuses to store it.
You can also pair the tier with a hard max_ram cap:
use grafeo_common::storage::SectionMemoryConfig;
let config = Config::in_memory().with_section_config(
SectionType::VectorStore,
SectionMemoryConfig {
max_ram: Some(500 * 1024 * 1024), // 500 MB cap on vector index heap
tier: TierOverride::Auto,
},
);
Reading the current tier¶
db.storage_tiers() returns the tier of every registered section consumer:
use grafeo_common::storage::SectionType;
use grafeo_common::memory::buffer::StorageTier;
let tiers = db.storage_tiers();
match tiers.get(&SectionType::VectorStore) {
Some(StorageTier::OnDisk) => println!("vector index is on disk"),
Some(StorageTier::InMemory) => println!("vector index is in RAM"),
Some(StorageTier::Uninitialized) | None => println!("no vector index registered"),
}
This is observability only: it doesn't move data. Useful for tests, dashboards, and confirming a ForceDisk config took effect.
Bringing data back: reload_eligible¶
After memory pressure drops (a long-running workload finishes, a checkpoint freed mutation overlay state) you can ask the buffer manager to bring spilled sections back into RAM:
// Reload as long as projected usage stays below 70% of the budget.
// Walks consumers highest-priority-first; stops when budget would exceed target.
let count = db.reload_eligible(0.7);
println!("reloaded {count} sections");
reload_eligible is best-effort: a per-consumer reload that fails (e.g., spill file missing) is logged-and-skipped. The walk visits highest-priority consumers first (graph storage before index buffers before query caches) so the most-important data comes back first.
The reload is synchronous; for large sections, call from a background thread.
Spill directory¶
The spill directory holds the files of spilled sections and of queries that spill. It's set via Config::with_spill_path, or derived from the path of a file database.
A spilled vector column keeps its values: the column reads them through a cache file, so queries (RETURN n.embedding), vector search, checkpoints and copies (save, to_memory, backups, snapshots) see them, and every checkpoint writes them into the database file. As for any data, the file holds them as of its last checkpoint: the commits since are in the WAL beside it until the next one, so a copy of the file alone, taken while the database is open, is not a complete backup. The cache is never data. A reload deletes its file, and the cache files and their directories are removed when the database is dropped. A closed database still reads its spilled values until then, but spills nothing more. A cache that a crash left in <file>.spill/cache/ is removed by the next read-write open of the same file; one left in a configured spill path or in the temp directory stays until removed by hand.
| Database | Spill directory | Vector cache |
|---|---|---|
With Config::with_spill_path(dir) | dir | dir/grafeo-<name>-<pid>-<random>/, one per open, so several databases can share dir |
| File database, read-write | <file>.spill/ | <file>.spill/cache/ |
| File database, read-only | grafeo-<name>-<pid>-<random>.spill/ in the system temp directory | the same directory |
| In memory, without a spill path | none | none |
A read-only open writes nothing beside the database. Its directory in the temp directory is created at open, under a new random name, and only if no file or link of that name exists, so another user of a shared temp directory cannot prepare it; on Unix only its user can enter it. When it cannot be created (a missing or read-only temp directory, as on read-only media or in a locked-down container), the open warns and opens without spilling. On Linux the system temp directory (/tmp) is often RAM-backed (tmpfs), where spilling frees no memory: for large read-only workloads, set a spill path on disk.
An encrypted database (Config::encryption) has no spill directory, as spill files are not encrypted: none is derived, and setting one together with encryption is a configuration error. Its sections stay in RAM.
let config = Config::persistent("/var/lib/grafeo/db.grafeo")
.with_spill_path("/var/lib/grafeo/spill");
Tracing¶
When the tracing feature is enabled, tier transitions emit events under the grafeo::buffer and grafeo::tier targets:
| Event target / level | When |
|---|---|
grafeo::buffer info | A consumer spills via spill_consumer_by_name |
grafeo::buffer info | A consumer reloads in reload_eligible |
grafeo::buffer warn | A consumer's reload returned an error |
grafeo::tier info | A ForceDisk override fires at database open |
Wire your tracing subscriber (e.g., tracing-subscriber) to these targets to log or export tier transitions.