Skip to main content

cqlite_core/memory/
mod.rs

1//! Memory management for CQLite
2
3use lru::LruCache;
4use parking_lot::RwLock;
5use std::collections::HashMap;
6use std::num::NonZeroUsize;
7use std::sync::Arc;
8
9use crate::{types::TableId, Config, Result, Value};
10
11/// Memory manager for caching and buffer management
12#[derive(Debug)]
13pub struct MemoryManager {
14    /// Block cache for storage blocks
15    block_cache: Arc<RwLock<BlockCache>>,
16
17    /// Row cache for frequently accessed rows
18    row_cache: Arc<RwLock<RowCache>>,
19
20    /// Buffer pool for memory allocation
21    buffer_pool: Arc<RwLock<BufferPool>>,
22
23    /// Memory statistics
24    stats: Arc<RwLock<MemoryStats>>,
25}
26
27/// Block cache for storage blocks
28struct BlockCache {
29    /// LRU cache for blocks (provides O(1) get/put)
30    cache: LruCache<BlockKey, Arc<Block>>,
31
32    /// Maximum cache size in bytes
33    max_size: usize,
34
35    /// Current size in bytes
36    current_size: usize,
37}
38
39/// Row cache for frequently accessed rows
40struct RowCache {
41    /// LRU cache for rows (provides O(1) get/put)
42    cache: LruCache<RowKey, Arc<CachedRow>>,
43
44    /// Maximum cache size in bytes
45    max_size: usize,
46
47    /// Current size in bytes
48    current_size: usize,
49}
50
51/// Buffer pool for memory allocation
52#[derive(Debug)]
53struct BufferPool {
54    /// Free buffers by size
55    free_buffers: HashMap<usize, Vec<Vec<u8>>>,
56
57    /// Allocated buffers
58    allocated_count: usize,
59
60    /// Total memory used
61    total_memory: usize,
62
63    /// Maximum memory allowed
64    max_memory: usize,
65}
66
67/// Block key for cache lookup
68#[derive(Debug, Clone, Hash, PartialEq, Eq)]
69struct BlockKey {
70    table_id: TableId,
71    block_id: u64,
72}
73
74/// Row key for cache lookup
75#[derive(Debug, Clone, Hash, PartialEq, Eq)]
76struct RowKey {
77    table_id: TableId,
78    row_key: String,
79}
80
81/// Cached block
82#[derive(Debug)]
83struct Block {
84    /// Block size
85    size: usize,
86
87    /// Last access time (reserved for future LRU enhancements)
88    _last_access: std::time::Instant,
89}
90
91/// Cached row
92#[derive(Debug)]
93struct CachedRow {
94    /// Row data
95    _data: Vec<Value>,
96
97    /// Row size estimate
98    size: usize,
99}
100
101impl MemoryManager {
102    /// Create a new memory manager
103    pub fn new(config: &Config) -> Result<Self> {
104        let block_cache = Arc::new(RwLock::new(BlockCache::new(
105            config.memory.block_cache.max_size as usize,
106        )));
107        let row_cache = Arc::new(RwLock::new(RowCache::new(
108            config.memory.row_cache.max_size as usize,
109        )));
110        let buffer_pool = Arc::new(RwLock::new(BufferPool::new(
111            config.memory.max_memory as usize,
112        )));
113
114        Ok(Self {
115            block_cache,
116            row_cache,
117            buffer_pool,
118            stats: Arc::new(RwLock::new(MemoryStats::default())),
119        })
120    }
121
122    /// Get a block from cache
123    pub fn get_block(&self, table_id: &TableId, block_id: u64) -> Option<Arc<Block>> {
124        let key = BlockKey {
125            table_id: table_id.clone(),
126            block_id,
127        };
128
129        let mut cache = self.block_cache.write();
130
131        // LruCache::get() is O(1) and automatically updates LRU order
132        if let Some(block) = cache.cache.get(&key) {
133            // Update stats
134            {
135                let mut stats = self.stats.write();
136                stats.block_cache_hits += 1;
137            }
138
139            Some(Arc::clone(block))
140        } else {
141            // Update stats
142            {
143                let mut stats = self.stats.write();
144                stats.block_cache_misses += 1;
145            }
146
147            None
148        }
149    }
150
151    /// Put a block in cache
152    pub fn put_block(&self, table_id: &TableId, block_id: u64, data: Vec<u8>) {
153        let key = BlockKey {
154            table_id: table_id.clone(),
155            block_id,
156        };
157
158        let block = Arc::new(Block {
159            size: data.len(),
160            _last_access: std::time::Instant::now(),
161        });
162
163        let mut cache = self.block_cache.write();
164
165        // Evict LRU entries until we have space
166        while cache.current_size + block.size > cache.max_size {
167            // LruCache::pop_lru() is O(1) and removes the least recently used entry
168            if let Some((_, evicted_block)) = cache.cache.pop_lru() {
169                cache.current_size -= evicted_block.size;
170            } else {
171                // Cache is empty, stop eviction
172                break;
173            }
174        }
175
176        // LruCache::put() is O(1) and automatically updates LRU order
177        cache.current_size += block.size;
178        cache.cache.put(key, block);
179    }
180
181    /// Get a row from cache
182    pub fn get_row(&self, table_id: &TableId, row_key: &str) -> Option<Arc<CachedRow>> {
183        let key = RowKey {
184            table_id: table_id.clone(),
185            row_key: row_key.to_string(),
186        };
187
188        let mut cache = self.row_cache.write();
189
190        // LruCache::get() is O(1) and automatically updates LRU order
191        if let Some(row) = cache.cache.get(&key) {
192            // Update stats
193            {
194                let mut stats = self.stats.write();
195                stats.row_cache_hits += 1;
196            }
197
198            Some(Arc::clone(row))
199        } else {
200            // Update stats
201            {
202                let mut stats = self.stats.write();
203                stats.row_cache_misses += 1;
204            }
205
206            None
207        }
208    }
209
210    /// Put a row in cache
211    pub fn put_row(&self, table_id: &TableId, row_key: &str, data: Vec<Value>) {
212        let key = RowKey {
213            table_id: table_id.clone(),
214            row_key: row_key.to_string(),
215        };
216
217        let size = self.estimate_row_size(&data);
218        let row = Arc::new(CachedRow { _data: data, size });
219
220        let mut cache = self.row_cache.write();
221
222        // Evict LRU entries until we have space
223        while cache.current_size + row.size > cache.max_size {
224            // LruCache::pop_lru() is O(1) and removes the least recently used entry
225            if let Some((_, evicted_row)) = cache.cache.pop_lru() {
226                cache.current_size -= evicted_row.size;
227            } else {
228                // Cache is empty, stop eviction
229                break;
230            }
231        }
232
233        // LruCache::put() is O(1) and automatically updates LRU order
234        cache.current_size += row.size;
235        cache.cache.put(key, row);
236    }
237
238    /// Allocate buffer from pool
239    pub fn allocate_buffer(&self, size: usize) -> Result<Vec<u8>> {
240        let mut pool = self.buffer_pool.write();
241
242        if let Some(buffers) = pool.free_buffers.get_mut(&size) {
243            if let Some(buffer) = buffers.pop() {
244                pool.allocated_count += 1;
245                pool.total_memory += size;
246
247                // Update stats
248                let mut stats = self.stats.write();
249                stats.buffer_allocations += 1;
250                stats.total_memory_used = pool.total_memory;
251
252                return Ok(buffer);
253            }
254        }
255
256        // Check memory limit before allocating new buffer
257        if pool.total_memory + size > pool.max_memory {
258            return Err(crate::Error::Memory(format!(
259                "Memory limit exceeded: requested {} bytes would exceed limit of {} bytes (current usage: {} bytes)",
260                size, pool.max_memory, pool.total_memory
261            )));
262        }
263
264        // Allocate new buffer
265        pool.allocated_count += 1;
266        pool.total_memory += size;
267
268        // Update stats
269        let mut stats = self.stats.write();
270        stats.buffer_allocations += 1;
271        stats.total_memory_used = pool.total_memory;
272
273        Ok(vec![0u8; size])
274    }
275
276    /// Return buffer to pool
277    pub fn deallocate_buffer(&self, mut buffer: Vec<u8>) {
278        let size = buffer.len();
279        buffer.clear();
280        // Don't shrink_to_fit() as we want to preserve capacity for reuse
281        buffer.resize(size, 0);
282
283        let mut pool = self.buffer_pool.write();
284        pool.total_memory -= size;
285        pool.free_buffers.entry(size).or_default().push(buffer);
286        pool.allocated_count -= 1;
287
288        // Update stats
289        let mut stats = self.stats.write();
290        stats.buffer_deallocations += 1;
291        stats.total_memory_used = pool.total_memory;
292    }
293
294    /// Get memory statistics
295    pub fn stats(&self) -> Result<MemoryStats> {
296        let stats = self.stats.read();
297        Ok(stats.clone())
298    }
299
300    /// Clear all caches
301    pub fn clear_caches(&self) {
302        {
303            let mut cache = self.block_cache.write();
304            cache.cache.clear();
305            cache.current_size = 0;
306        }
307
308        {
309            let mut cache = self.row_cache.write();
310            cache.cache.clear();
311            cache.current_size = 0;
312        }
313    }
314
315    /// Estimate row size
316    fn estimate_row_size(&self, data: &[Value]) -> usize {
317        estimate_row_size(data)
318    }
319}
320
321/// Estimate the logical size, in bytes, of a row's values.
322///
323/// This is the single estimator reused by the row cache (eviction accounting)
324/// and by the SELECT executor's byte-bounded result budget (issue #1582), so
325/// both surfaces measure "row bytes" identically. It sums the per-value
326/// estimate over the row; it is a *logical* content estimate and deliberately
327/// does not model container overhead (HashMap slots, `Arc`/`String` capacity),
328/// which is why the executor's byte budget sits well below the process memory
329/// target to leave headroom for that overhead.
330pub(crate) fn estimate_row_size(data: &[Value]) -> usize {
331    data.iter().map(estimate_value_size).sum()
332}
333
334/// Estimate the logical size, in bytes, of a single CQL value.
335pub(crate) fn estimate_value_size(value: &Value) -> usize {
336    match value {
337        Value::Null => 1,
338        Value::Boolean(_) => 1,
339        Value::Integer(_) => 4,
340        Value::BigInt(_) => 8,
341        Value::Counter(_) => 8,
342        Value::Float(_) => 8,
343        Value::Text(s) => s.len(),
344        Value::Blob(b) => b.len(),
345        Value::Timestamp(_) => 8,
346        Value::Date(_) => 4,
347        Value::Time(_) => 8,
348        Value::Uuid(_) => 16,
349        Value::Inet(bytes) => bytes.len(),
350        Value::Json(json) => json.to_string().len(),
351        Value::List(items) => items.iter().map(estimate_value_size).sum(),
352        Value::Map(map) => map
353            .iter()
354            .map(|(k, v)| estimate_value_size(k) + estimate_value_size(v))
355            .sum(),
356        Value::TinyInt(_) => 1,
357        Value::SmallInt(_) => 2,
358        Value::Float32(_) => 4,
359        Value::Set(items) => items.iter().map(estimate_value_size).sum(),
360        Value::Tuple(items) => items.iter().map(estimate_value_size).sum(),
361        Value::Udt(udt) => udt
362            .fields
363            .iter()
364            .map(|f| f.value.as_ref().map_or(0, estimate_value_size))
365            .sum(),
366        Value::Frozen(boxed_value) => estimate_value_size(boxed_value),
367        Value::Varint(data) => data.len(),
368        Value::Decimal { unscaled, .. } => 4 + unscaled.len(), // scale + unscaled data
369        Value::Duration { .. } => 12,                          // 3 * 4 bytes
370        Value::Tombstone(_) => 16,                             // timestamp + type + optional TTL
371    }
372}
373
374impl std::fmt::Debug for BlockCache {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("BlockCache")
377            .field("max_size", &self.max_size)
378            .field("current_size", &self.current_size)
379            .field("cache_len", &self.cache.len())
380            .finish()
381    }
382}
383
384impl BlockCache {
385    fn new(max_size: usize) -> Self {
386        // LruCache requires NonZeroUsize for capacity
387        // We use a reasonable default capacity (1000 entries) for the LRU structure
388        // The actual memory limit is enforced separately via max_size
389        let capacity = NonZeroUsize::new(1000).expect("capacity must be non-zero");
390        Self {
391            cache: LruCache::new(capacity),
392            max_size,
393            current_size: 0,
394        }
395    }
396}
397
398impl std::fmt::Debug for RowCache {
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        f.debug_struct("RowCache")
401            .field("max_size", &self.max_size)
402            .field("current_size", &self.current_size)
403            .field("cache_len", &self.cache.len())
404            .finish()
405    }
406}
407
408impl RowCache {
409    fn new(max_size: usize) -> Self {
410        // LruCache requires NonZeroUsize for capacity
411        // We use a reasonable default capacity (1000 entries) for the LRU structure
412        // The actual memory limit is enforced separately via max_size
413        let capacity = NonZeroUsize::new(1000).expect("capacity must be non-zero");
414        Self {
415            cache: LruCache::new(capacity),
416            max_size,
417            current_size: 0,
418        }
419    }
420}
421
422impl BufferPool {
423    fn new(max_memory: usize) -> Self {
424        Self {
425            free_buffers: HashMap::new(),
426            allocated_count: 0,
427            total_memory: 0,
428            max_memory,
429        }
430    }
431}
432
433/// Memory statistics
434#[derive(Debug, Clone, Default)]
435pub struct MemoryStats {
436    /// Block cache hits
437    pub block_cache_hits: u64,
438
439    /// Block cache misses
440    pub block_cache_misses: u64,
441
442    /// Row cache hits
443    pub row_cache_hits: u64,
444
445    /// Row cache misses
446    pub row_cache_misses: u64,
447
448    /// Total memory used
449    pub total_memory_used: usize,
450
451    /// Buffer pool allocations
452    pub buffer_allocations: u64,
453
454    /// Buffer pool deallocations
455    pub buffer_deallocations: u64,
456}
457
458impl MemoryStats {
459    /// Calculate block cache hit rate
460    pub fn block_cache_hit_rate(&self) -> f64 {
461        let total = self.block_cache_hits + self.block_cache_misses;
462        if total > 0 {
463            self.block_cache_hits as f64 / total as f64
464        } else {
465            0.0
466        }
467    }
468
469    /// Calculate row cache hit rate
470    pub fn row_cache_hit_rate(&self) -> f64 {
471        let total = self.row_cache_hits + self.row_cache_misses;
472        if total > 0 {
473            self.row_cache_hits as f64 / total as f64
474        } else {
475            0.0
476        }
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::types::TableId;
484
485    #[test]
486    fn test_memory_manager_creation() {
487        let config = Config::default();
488        let manager = MemoryManager::new(&config).unwrap();
489
490        let stats = manager.stats().unwrap();
491        assert_eq!(stats.block_cache_hits, 0);
492        assert_eq!(stats.block_cache_misses, 0);
493    }
494
495    #[test]
496    fn test_block_cache() {
497        let config = Config::default();
498        let manager = MemoryManager::new(&config).unwrap();
499
500        let table_id = TableId::new("test_table");
501        let block_id = 1;
502        let data = vec![1, 2, 3, 4, 5];
503
504        // Cache miss
505        let result = manager.get_block(&table_id, block_id);
506        assert!(result.is_none());
507
508        // Put block
509        manager.put_block(&table_id, block_id, data.clone());
510
511        // Cache hit
512        let result = manager.get_block(&table_id, block_id);
513        assert!(result.is_some());
514        assert_eq!(result.unwrap().size, data.len());
515    }
516
517    #[test]
518    fn test_block_cache_eviction_updates_stats() {
519        let mut config = Config::default();
520        config.memory.block_cache.max_size = 8;
521        let manager = MemoryManager::new(&config).unwrap();
522
523        let table_id = TableId::new("ks_table");
524
525        manager.put_block(&table_id, 1, vec![0u8; 8]);
526        manager.put_block(&table_id, 2, vec![0u8; 4]); // triggers eviction of block 1
527
528        assert!(manager.get_block(&table_id, 1).is_none());
529        assert!(manager.get_block(&table_id, 2).is_some());
530
531        let stats = manager.stats().unwrap();
532        assert_eq!(stats.block_cache_hits, 1);
533        assert_eq!(stats.block_cache_misses, 1);
534    }
535
536    #[test]
537    fn test_row_cache() {
538        let config = Config::default();
539        let manager = MemoryManager::new(&config).unwrap();
540
541        let table_id = TableId::new("test_table");
542        let row_key = "test_key";
543        let data = vec![Value::Integer(42), Value::Text("hello".to_string())];
544
545        // Cache miss
546        let result = manager.get_row(&table_id, row_key);
547        assert!(result.is_none());
548
549        // Put row
550        manager.put_row(&table_id, row_key, data.clone());
551
552        // Cache hit
553        let result = manager.get_row(&table_id, row_key);
554        assert!(result.is_some());
555        assert_eq!(result.unwrap()._data, data);
556    }
557
558    #[test]
559    fn test_row_cache_eviction_and_stats() {
560        let mut config = Config::default();
561        config.memory.row_cache.max_size = 8;
562        let manager = MemoryManager::new(&config).unwrap();
563
564        let table_id = TableId::new("ks_table");
565
566        manager.put_row(&table_id, "k1", vec![Value::Text("abcd".into())]);
567        manager.put_row(&table_id, "k2", vec![Value::Text("efgh".into())]);
568        manager.put_row(&table_id, "k3", vec![Value::Text("ijkl".into())]);
569
570        assert!(manager.get_row(&table_id, "k1").is_none());
571        assert!(manager.get_row(&table_id, "k3").is_some());
572
573        let stats = manager.stats().unwrap();
574        assert_eq!(stats.row_cache_hits, 1);
575        assert_eq!(stats.row_cache_misses, 1);
576    }
577
578    #[test]
579    fn test_buffer_pool() {
580        let config = Config::default();
581        let manager = MemoryManager::new(&config).unwrap();
582
583        let size = 1024;
584        let buffer = manager.allocate_buffer(size).unwrap();
585        assert_eq!(buffer.len(), size);
586
587        manager.deallocate_buffer(buffer);
588
589        // Should reuse buffer
590        let buffer2 = manager.allocate_buffer(size).unwrap();
591        assert_eq!(buffer2.len(), size);
592    }
593
594    #[test]
595    fn test_clear_caches() {
596        let mut config = Config::default();
597        config.memory.block_cache.max_size = 8;
598        config.memory.row_cache.max_size = 8;
599        let manager = MemoryManager::new(&config).unwrap();
600
601        let table_id = TableId::new("ks_table");
602        manager.put_block(&table_id, 1, vec![0u8; 8]);
603        manager.put_row(&table_id, "k1", vec![Value::Text("abcd".into())]);
604
605        manager.clear_caches();
606
607        assert!(manager.get_block(&table_id, 1).is_none());
608        assert!(manager.get_row(&table_id, "k1").is_none());
609    }
610
611    #[test]
612    fn test_memory_limit_enforcement() {
613        let mut config = Config::default();
614        config.memory.max_memory = 128 * 1024 * 1024; // 128MB
615        let manager = MemoryManager::new(&config).unwrap();
616
617        // Allocate buffers up to the limit
618        let buffer1 = manager
619            .allocate_buffer(64 * 1024 * 1024)
620            .expect("first 64MB should succeed");
621        let buffer2 = manager
622            .allocate_buffer(64 * 1024 * 1024)
623            .expect("second 64MB should succeed");
624
625        // Try to exceed the limit
626        let result = manager.allocate_buffer(1024);
627        assert!(result.is_err(), "allocation exceeding limit should fail");
628
629        // Verify error message
630        if let Err(e) = result {
631            let err_msg = e.to_string();
632            assert!(
633                err_msg.contains("Memory limit exceeded"),
634                "error should mention memory limit"
635            );
636        }
637
638        // Verify stats
639        let stats = manager.stats().unwrap();
640        assert_eq!(
641            stats.buffer_allocations, 2,
642            "should have 2 successful allocations"
643        );
644        assert_eq!(
645            stats.total_memory_used,
646            128 * 1024 * 1024,
647            "should be at memory limit"
648        );
649
650        // Deallocate and verify we can allocate again
651        manager.deallocate_buffer(buffer1);
652        let stats = manager.stats().unwrap();
653        assert_eq!(stats.buffer_deallocations, 1);
654        assert_eq!(
655            stats.total_memory_used,
656            64 * 1024 * 1024,
657            "memory should be freed"
658        );
659
660        // Should be able to allocate again after freeing
661        let buffer3 = manager
662            .allocate_buffer(32 * 1024 * 1024)
663            .expect("allocation after free should succeed");
664
665        // Clean up remaining buffers
666        manager.deallocate_buffer(buffer2);
667        manager.deallocate_buffer(buffer3);
668
669        let final_stats = manager.stats().unwrap();
670        assert_eq!(
671            final_stats.total_memory_used, 0,
672            "all memory should be freed"
673        );
674    }
675
676    #[test]
677    fn test_memory_limit_with_buffer_reuse() {
678        let mut config = Config::default();
679        config.memory.max_memory = 128 * 1024 * 1024; // 128MB
680        let manager = MemoryManager::new(&config).unwrap();
681
682        // Allocate two 64MB buffers to reach limit
683        let buffer1 = manager
684            .allocate_buffer(64 * 1024 * 1024)
685            .expect("first 64MB should succeed");
686        let buffer2 = manager
687            .allocate_buffer(64 * 1024 * 1024)
688            .expect("second 64MB should succeed");
689
690        // Deallocate first buffer - it goes to free pool
691        manager.deallocate_buffer(buffer1);
692
693        let stats = manager.stats().unwrap();
694        assert_eq!(
695            stats.total_memory_used,
696            64 * 1024 * 1024,
697            "should have 64MB in use after deallocation"
698        );
699
700        // Allocate same size - should REUSE buffer from free pool
701        let buffer3 = manager
702            .allocate_buffer(64 * 1024 * 1024)
703            .expect("reuse should succeed");
704
705        // Critical: reused buffer should still count toward memory limit
706        let stats = manager.stats().unwrap();
707        assert_eq!(
708            stats.total_memory_used,
709            128 * 1024 * 1024,
710            "reused buffer should count toward memory limit"
711        );
712
713        // Now at limit again - allocation should fail
714        let result = manager.allocate_buffer(1024);
715        assert!(
716            result.is_err(),
717            "allocation should fail when limit reached via buffer reuse"
718        );
719
720        // Verify error message
721        if let Err(e) = result {
722            let err_msg = e.to_string();
723            assert!(
724                err_msg.contains("Memory limit exceeded"),
725                "error should mention memory limit"
726            );
727        }
728
729        // Clean up
730        manager.deallocate_buffer(buffer2);
731        manager.deallocate_buffer(buffer3);
732
733        let final_stats = manager.stats().unwrap();
734        assert_eq!(final_stats.total_memory_used, 0, "all memory freed");
735    }
736}