Skip to content

Commit a3068ee

Browse files
pepicrftclaude
andauthored
feat: Implement cache eviction with LRU, LFU, and TTL policies (#68)
* feat: Implement cache eviction with LRU, LFU, and TTL policies Add automatic cache eviction to prevent unbounded disk usage. When the cache exceeds the configured max_size, Fabrik automatically removes objects based on the configured eviction policy. ## Features - Three eviction policies: LRU (Least Recently Used), LFU (Least Frequently Used, default), and TTL (Time To Live) - Configurable max cache size with human-readable format (5GB, 100MB, etc.) - Configurable TTL with duration format (7d, 24h, 30m, etc.) - Target ratio eviction (evicts until 90% of max_size) - Metadata tracking: size, created_at, accessed_at, access_count ## Integration Eviction is now integrated across all Fabrik surfaces: - daemon command - server command - exec command - cas CLI command - kv CLI command - C API (with new fabrik_cache_init_with_eviction function) ## Configuration ```toml [cache] dir = ".fabrik/cache" max_size = "5GB" eviction_policy = "lfu" # lru | lfu | ttl default_ttl = "7d" ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add async background eviction task - Add EvictableStorage trait for storage backends to support eviction - Implement background eviction task that runs every 30 seconds - Remove blocking eviction from put() operations - Update daemon, server, and exec commands to spawn eviction task - Add graceful shutdown handling for background eviction - Update documentation to explain async eviction behavior Eviction now runs asynchronously in a dedicated tokio task, ensuring that put() operations are never blocked by eviction. The task periodically checks cache size and evicts objects according to the configured policy (LRU, LFU, or TTL) when needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: Remove redundant [fabrik] prefix from log messages The custom FabrikFormatter in src/logging.rs already adds (fabrik) to all log output, so the manual [fabrik] prefix in log messages was redundant. Also: - Move docs/c-api.md to docs/reference/c-api.md - Add C API to documentation sidebar - Add logging conventions to CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 56a31b5 commit a3068ee

File tree

32 files changed

+1992
-103
lines changed

32 files changed

+1992
-103
lines changed

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,12 @@ fabrik config show --config config.toml --config-upstream s3://override
21502150
- Use `clippy` for linting (zero warnings policy)
21512151
- Prioritize safety, idiomatic patterns, and zero-cost abstractions
21522152
2153+
### Logging Conventions
2154+
- Use the `tracing` crate (`info!`, `debug!`, `warn!`, `error!`) for all logging
2155+
- **Do NOT add `[fabrik]` prefix to log messages** - the custom `FabrikFormatter` in `src/logging.rs` automatically adds `(fabrik)` to all log output
2156+
- For CLI output (`println!`/`eprintln!`), use `fabrik_prefix()` from `src/cli_utils.rs` which provides colored output
2157+
- Use structured fields for machine-parseable logs (see `src/logging.rs` for field constants)
2158+
21532159
### Project Principles
21542160
- **Performance**: Low latency (target: <10ms p99), high throughput
21552161
- **Reliability**: Data integrity, fault tolerance, graceful degradation

docs/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default defineConfig({
2525
{
2626
text: "Cache",
2727
items: [
28+
{ text: "Eviction", link: "/cache/eviction" },
2829
{ text: "Peer to Peer", link: "/cache/p2p" },
2930
{
3031
text: "Build Systems",
@@ -104,6 +105,7 @@ export default defineConfig({
104105
{ text: "CLI Commands", link: "/reference/cli" },
105106
{ text: "Configuration File", link: "/reference/config-file" },
106107
{ text: "API Reference", link: "/reference/api" },
108+
{ text: "C API", link: "/reference/c-api" },
107109
],
108110
},
109111
],

docs/cache/eviction.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Cache Eviction
2+
3+
Fabrik provides automatic cache eviction to prevent unbounded disk usage. When the cache exceeds the configured `max_size`, Fabrik automatically removes objects based on the configured eviction policy.
4+
5+
## Configuration
6+
7+
Configure eviction in your `fabrik.toml`:
8+
9+
```toml
10+
[cache]
11+
dir = ".fabrik/cache"
12+
max_size = "5GB" # Maximum cache size
13+
eviction_policy = "lfu" # lru | lfu | ttl
14+
default_ttl = "7d" # Default time-to-live for TTL policy
15+
```
16+
17+
## Eviction Policies
18+
19+
### LFU (Least Frequently Used) - Default
20+
21+
The LFU policy evicts objects with the lowest access count first. This is the default policy because it tends to preserve frequently-accessed build artifacts.
22+
23+
```toml
24+
[cache]
25+
eviction_policy = "lfu"
26+
```
27+
28+
**Best for:**
29+
- Build caches with stable dependency trees
30+
- Projects where common artifacts are accessed repeatedly
31+
- CI environments with shared caches
32+
33+
### LRU (Least Recently Used)
34+
35+
The LRU policy evicts objects that haven't been accessed for the longest time.
36+
37+
```toml
38+
[cache]
39+
eviction_policy = "lru"
40+
```
41+
42+
**Best for:**
43+
- Development environments with rapidly changing dependencies
44+
- Projects with distinct build phases
45+
- When recent builds are more important than frequency
46+
47+
### TTL (Time To Live)
48+
49+
The TTL policy evicts objects older than the configured TTL. Objects are evicted based on creation time, not access time.
50+
51+
```toml
52+
[cache]
53+
eviction_policy = "ttl"
54+
default_ttl = "7d" # Evict objects older than 7 days
55+
```
56+
57+
**Best for:**
58+
- Compliance requirements with data retention limits
59+
- Ensuring cache freshness
60+
- Periodic cache invalidation scenarios
61+
62+
## How Eviction Works
63+
64+
Eviction runs **asynchronously in a background task**, ensuring that `put()` operations are never blocked by eviction:
65+
66+
1. **Background Task**: A dedicated tokio task periodically checks cache size (default: every 30 seconds)
67+
2. **Trigger**: When cache exceeds `max_size`, the background task evicts objects
68+
3. **Target**: Fabrik evicts until the cache is at 90% of `max_size` (configurable via `target_ratio`)
69+
4. **Selection**: Objects are selected based on the configured policy
70+
5. **Batch Processing**: Up to 1000 objects are evicted per run to avoid overwhelming the system
71+
6. **Non-blocking**: `put()` operations proceed immediately without waiting for eviction
72+
73+
### Metadata Tracking
74+
75+
Fabrik tracks the following metadata for each cached object:
76+
- **Size**: Object size in bytes
77+
- **Created At**: When the object was first cached
78+
- **Accessed At**: Last access timestamp (updated on get/exists)
79+
- **Access Count**: Number of times the object was accessed
80+
81+
This metadata is stored efficiently in RocksDB secondary indexes for fast eviction candidate selection.
82+
83+
## Size Format
84+
85+
The `max_size` configuration accepts human-readable size strings:
86+
87+
| Format | Example | Bytes |
88+
|--------|---------|-------|
89+
| Terabytes | `1TB` | 1,099,511,627,776 |
90+
| Gigabytes | `5GB` | 5,368,709,120 |
91+
| Megabytes | `500MB` | 524,288,000 |
92+
| Kilobytes | `512KB` | 524,288 |
93+
| Bytes | `1024` | 1,024 |
94+
95+
## TTL Format
96+
97+
The `default_ttl` configuration accepts duration strings:
98+
99+
| Format | Example | Seconds |
100+
|--------|---------|---------|
101+
| Days | `7d` | 604,800 |
102+
| Hours | `24h` | 86,400 |
103+
| Minutes | `30m` | 1,800 |
104+
| Seconds | `3600s` or `3600` | 3,600 |
105+
106+
## Monitoring Eviction
107+
108+
Fabrik logs eviction activity at the INFO level:
109+
110+
```
111+
INFO [fabrik] Eviction manager initialized: policy=lfu, max_size=5120MB, target_ratio=0.9
112+
INFO [fabrik] Eviction complete: evicted 42 objects (128 MB) in 25ms
113+
```
114+
115+
## Environment Variable Overrides
116+
117+
You can override eviction settings via environment variables:
118+
119+
```bash
120+
# Override max cache size
121+
export FABRIK_CONFIG_CACHE_MAX_SIZE=10GB
122+
123+
# Override eviction policy
124+
export FABRIK_CONFIG_CACHE_EVICTION_POLICY=lru
125+
126+
# Override default TTL
127+
export FABRIK_CONFIG_CACHE_DEFAULT_TTL=14d
128+
```
129+
130+
## C API Support
131+
132+
The C API provides two initialization functions:
133+
134+
```c
135+
// Basic initialization with default eviction (5GB, LFU, 7 days)
136+
FabrikCache* cache = fabrik_cache_init("/path/to/cache");
137+
138+
// Custom eviction settings
139+
FabrikCache* cache = fabrik_cache_init_with_eviction(
140+
"/path/to/cache",
141+
10ULL * 1024 * 1024 * 1024, // 10GB max size
142+
1, // 0=LRU, 1=LFU, 2=TTL
143+
7 * 24 * 60 * 60 // 7 days TTL
144+
);
145+
```
146+
147+
## Best Practices
148+
149+
1. **Set realistic limits**: Choose a `max_size` that fits your available disk space while leaving room for other applications
150+
2. **Choose the right policy**: LFU works best for most build caches, but LRU may be better for development
151+
3. **Monitor eviction**: Watch the logs to ensure eviction is working as expected and adjust settings if needed
152+
4. **Consider TTL for compliance**: Use TTL policy when you need predictable cache expiration
153+
154+
> [!NOTE]
155+
> Eviction runs asynchronously in the background every 30 seconds. The cache may temporarily exceed `max_size` between eviction runs.
156+
157+
> [!WARNING]
158+
> Setting `max_size` too low may cause frequent eviction and reduce cache hit rates. Monitor your cache performance after changing eviction settings.

docs/c-api.md renamed to docs/reference/c-api.md

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22

33
The Fabrik C API provides a thread-safe interface for integrating Fabrik cache into C/C++ applications and other toolchains.
44

5-
## Features
6-
7-
- ✅ Thread-safe operations
8-
- ✅ Content-addressed storage
9-
- ✅ Simple error handling
10-
- ✅ Cross-platform (Linux, macOS, Windows)
11-
- ✅ Zero-copy where possible
12-
- ✅ Comprehensive error messages
13-
145
## Installation
156

167
### From Releases
@@ -132,7 +123,7 @@ typedef struct FabrikCache FabrikCache;
132123
133124
#### `fabrik_cache_init`
134125
135-
Initialize a new cache instance.
126+
Initialize a new cache instance with default eviction settings (5GB max size, LFU policy, 7 days TTL).
136127
137128
```c
138129
FabrikCache* fabrik_cache_init(const char *cache_dir);
@@ -155,6 +146,50 @@ if (!cache) {
155146

156147
---
157148

149+
#### `fabrik_cache_init_with_eviction`
150+
151+
Initialize a new cache instance with custom eviction settings.
152+
153+
```c
154+
FabrikCache* fabrik_cache_init_with_eviction(
155+
const char *cache_dir,
156+
uint64_t max_size_bytes,
157+
int eviction_policy,
158+
uint64_t ttl_seconds
159+
);
160+
```
161+
162+
**Parameters:**
163+
- `cache_dir`: Path to cache directory (NULL-terminated C string)
164+
- `max_size_bytes`: Maximum cache size in bytes (0 for default: 5GB)
165+
- `eviction_policy`: Eviction policy (0=LRU, 1=LFU, 2=TTL)
166+
- `ttl_seconds`: Default TTL in seconds (0 for default: 7 days)
167+
168+
**Returns:**
169+
- Pointer to `FabrikCache` on success
170+
- `NULL` on error (use `fabrik_last_error()` for details)
171+
172+
**Example:**
173+
```c
174+
// 10GB cache with LRU eviction and 14 day TTL
175+
FabrikCache *cache = fabrik_cache_init_with_eviction(
176+
"/home/user/.cache/fabrik",
177+
10ULL * 1024 * 1024 * 1024, // 10GB
178+
0, // LRU
179+
14 * 24 * 60 * 60 // 14 days
180+
);
181+
if (!cache) {
182+
fprintf(stderr, "Init failed: %s\n", fabrik_last_error());
183+
}
184+
```
185+
186+
**Eviction Policies:**
187+
- `0` (LRU): Least Recently Used - evicts objects not accessed for longest time
188+
- `1` (LFU): Least Frequently Used - evicts objects with lowest access count (default)
189+
- `2` (TTL): Time To Live - evicts objects older than specified TTL
190+
191+
---
192+
158193
#### `fabrik_cache_free`
159194

160195
Free a cache instance.

include/fabrik.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
- Async batched access tracking (touch operations)
9797
- Snappy compression for metadata
9898
- Column families for efficient indexing (LRU/LFU eviction)
99+
- Automatic eviction when cache exceeds max_size
99100
*/
100101
typedef struct FabrikFilesystemStorage FabrikFilesystemStorage;
101102

@@ -122,6 +123,29 @@ typedef struct FabrikFabrikCache {
122123
*/
123124
fabrik_ struct FabrikFabrikCache *fabrik_cache_init(const char *aCacheDir);
124125

126+
/*
127+
Initialize a new Fabrik cache instance with custom eviction settings
128+
129+
# Arguments
130+
* `cache_dir` - Path to cache directory (NULL-terminated C string)
131+
* `max_size_bytes` - Maximum cache size in bytes (0 for default: 5GB)
132+
* `eviction_policy` - Eviction policy: 0=LRU, 1=LFU, 2=TTL (default: LFU)
133+
* `ttl_seconds` - Default TTL in seconds (0 for default: 7 days)
134+
135+
# Returns
136+
* Pointer to FabrikCache on success
137+
* NULL on error (use `fabrik_last_error()` to get error message)
138+
139+
# Safety
140+
* `cache_dir` must be a valid NULL-terminated C string
141+
* Returned pointer must be freed with `fabrik_cache_free()`
142+
*/
143+
fabrik_
144+
struct FabrikFabrikCache *fabrik_cache_init_with_eviction(const char *aCacheDir,
145+
uint64_t aMaxSizeBytes,
146+
int aEvictionPolicy,
147+
uint64_t aTtlSeconds);
148+
125149
/*
126150
Free a Fabrik cache instance
127151

src/auth/provider.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ impl AuthProvider {
284284
self.oauth2_url.as_ref().expect("OAuth2 URL should be set")
285285
);
286286
if let Ok(Some(_)) = wrapper.get_token(&token_key) {
287-
tracing::debug!("[fabrik] Auto-detected OAuth2 (token found in storage)");
287+
tracing::debug!("Auto-detected OAuth2 (token found in storage)");
288288
return Ok(ConfigAuthProvider::OAuth2);
289289
}
290290
}
@@ -372,7 +372,7 @@ impl AuthProvider {
372372
))?
373373
.clone();
374374

375-
tracing::info!("[fabrik] Starting OAuth2 device authorization flow (RFC 8628)");
375+
tracing::info!("Starting OAuth2 device authorization flow (RFC 8628)");
376376

377377
let oauth2_url = self.oauth2_url.clone();
378378

@@ -402,7 +402,7 @@ impl AuthProvider {
402402
.await
403403
.map_err(|e| AuthenticationError::OAuth2Error(format!("Task join error: {}", e)))??;
404404

405-
tracing::info!("[fabrik] Successfully authenticated");
405+
tracing::info!("Successfully authenticated");
406406

407407
Ok(())
408408
}
@@ -426,12 +426,12 @@ impl AuthProvider {
426426
);
427427
wrapper.delete_token(&token_key)?;
428428

429-
tracing::info!("[fabrik] Successfully logged out");
429+
tracing::info!("Successfully logged out");
430430
Ok(())
431431
}
432432
ConfigAuthProvider::Token => {
433433
// For token-based auth, we don't store anything, so nothing to delete
434-
tracing::info!("[fabrik] Token-based authentication doesn't require logout");
434+
tracing::info!("Token-based authentication doesn't require logout");
435435
Ok(())
436436
}
437437
}

0 commit comments

Comments
 (0)