llm_optimizer_processor/state/
sled_backend.rs

1//! Sled-based persistent state backend
2//!
3//! This module provides a persistent state backend using the Sled embedded database.
4//! It's ideal for production deployments where state needs to survive restarts.
5
6use async_trait::async_trait;
7use sled::Db;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{debug, error, info, trace, warn};
12
13use super::backend::StateBackend;
14use super::compression::{compress, decompress, CompressionConfig};
15use crate::error::{StateError, StateResult};
16
17/// Statistics for the Sled backend
18#[derive(Debug, Clone, Default)]
19pub struct SledBackendStats {
20    /// Number of get operations
21    pub get_count: u64,
22    /// Number of put operations
23    pub put_count: u64,
24    /// Number of delete operations
25    pub delete_count: u64,
26    /// Number of flush operations
27    pub flush_count: u64,
28    /// Total bytes written (after compression)
29    pub bytes_written: u64,
30    /// Total bytes read (before decompression)
31    pub bytes_read: u64,
32    /// Total bytes compressed
33    pub bytes_compressed: u64,
34    /// Total bytes decompressed
35    pub bytes_decompressed: u64,
36    /// Total bytes before compression
37    pub bytes_before_compression: u64,
38    /// Total bytes after compression
39    pub bytes_after_compression: u64,
40}
41
42/// Configuration for the Sled backend
43#[derive(Debug, Clone)]
44pub struct SledConfig {
45    /// Path to the database directory
46    pub path: PathBuf,
47    /// Cache size in bytes (default: 128MB)
48    pub cache_capacity: u64,
49    /// Compression configuration (default: balanced preset)
50    pub compression: CompressionConfig,
51    /// Flush every N operations (default: 1000)
52    pub flush_every: u64,
53    /// Segment size for log files (default: 8MB)
54    pub segment_size: usize,
55}
56
57impl Default for SledConfig {
58    fn default() -> Self {
59        Self {
60            path: PathBuf::from("/tmp/sled_state"),
61            cache_capacity: 128 * 1024 * 1024, // 128MB
62            compression: CompressionConfig::balanced(),
63            flush_every: 1000,
64            segment_size: 8 * 1024 * 1024, // 8MB
65        }
66    }
67}
68
69impl SledConfig {
70    /// Create a new configuration with the given path
71    pub fn new<P: AsRef<Path>>(path: P) -> Self {
72        Self {
73            path: path.as_ref().to_path_buf(),
74            ..Default::default()
75        }
76    }
77
78    /// Set the cache capacity
79    pub fn with_cache_capacity(mut self, capacity: u64) -> Self {
80        self.cache_capacity = capacity;
81        self
82    }
83
84    /// Set compression configuration
85    pub fn with_compression(mut self, config: CompressionConfig) -> Self {
86        self.compression = config;
87        self
88    }
89
90    /// Disable compression
91    pub fn without_compression(mut self) -> Self {
92        self.compression = CompressionConfig::none();
93        self
94    }
95
96    /// Use fast compression (LZ4)
97    pub fn with_fast_compression(mut self) -> Self {
98        self.compression = CompressionConfig::fast();
99        self
100    }
101
102    /// Use balanced compression (Snappy)
103    pub fn with_balanced_compression(mut self) -> Self {
104        self.compression = CompressionConfig::balanced();
105        self
106    }
107
108    /// Use best compression (Zstd)
109    pub fn with_best_compression(mut self) -> Self {
110        self.compression = CompressionConfig::best_size();
111        self
112    }
113
114    /// Set the flush interval
115    pub fn with_flush_every(mut self, count: u64) -> Self {
116        self.flush_every = count;
117        self
118    }
119}
120
121/// Sled-based persistent state backend
122///
123/// This backend uses the Sled embedded database for persistent state storage.
124/// It's designed for production use where state must survive process restarts.
125///
126/// ## Features
127///
128/// - **Persistent**: State survives restarts
129/// - **ACID**: Atomic operations with durability guarantees
130/// - **Efficient**: Lock-free data structures and zero-copy operations
131/// - **Compression**: Optional compression to save disk space
132/// - **Crash recovery**: Automatic recovery from crashes
133///
134/// ## Example
135///
136/// ```rust,no_run
137/// use processor::state::{SledStateBackend, SledConfig, StateBackend};
138///
139/// #[tokio::main]
140/// async fn main() -> anyhow::Result<()> {
141///     // Create configuration with best compression
142///     let config = SledConfig::new("/var/lib/optimizer/state")
143///         .with_cache_capacity(256 * 1024 * 1024) // 256MB
144///         .with_best_compression(); // Use Zstd for maximum compression
145///
146///     // Open database
147///     let backend = SledStateBackend::open(config).await?;
148///
149///     // Use like any other backend
150///     backend.put(b"window:123", b"aggregated_data").await?;
151///
152///     // Data persists across restarts
153///     backend.flush().await?;
154///
155///     Ok(())
156/// }
157/// ```
158pub struct SledStateBackend {
159    /// The Sled database instance
160    db: Arc<Db>,
161    /// Configuration
162    config: SledConfig,
163    /// Statistics
164    stats: Arc<RwLock<SledBackendStats>>,
165    /// Operation counter for periodic flushes
166    op_counter: Arc<RwLock<u64>>,
167}
168
169impl SledStateBackend {
170    /// Open a Sled database with the given configuration
171    ///
172    /// # Arguments
173    ///
174    /// * `config` - Database configuration
175    ///
176    /// # Returns
177    ///
178    /// * `Ok(SledStateBackend)` - Successfully opened database
179    /// * `Err(StateError)` - Failed to open database
180    ///
181    /// # Example
182    ///
183    /// ```rust,no_run
184    /// # use processor::state::{SledStateBackend, SledConfig};
185    /// # #[tokio::main]
186    /// # async fn main() -> anyhow::Result<()> {
187    /// let config = SledConfig::new("/tmp/test_db");
188    /// let backend = SledStateBackend::open(config).await?;
189    /// # Ok(())
190    /// # }
191    /// ```
192    pub async fn open(config: SledConfig) -> StateResult<Self> {
193        info!("Opening Sled database at {:?}", config.path);
194
195        // Create parent directory if it doesn't exist
196        if let Some(parent) = config.path.parent() {
197            tokio::fs::create_dir_all(parent).await.map_err(|e| {
198                StateError::StorageError {
199                    backend_type: "sled".to_string(),
200                    details: format!("Failed to create directory: {}", e),
201                }
202            })?;
203        }
204
205        // Configure and open Sled
206        // Note: We disable Sled's built-in compression since we handle compression
207        // at a higher level with more control over algorithms and thresholds
208        let db = sled::Config::new()
209            .path(&config.path)
210            .cache_capacity(config.cache_capacity)
211            .use_compression(false) // Disable Sled's built-in compression
212            .segment_size(config.segment_size)
213            .open()
214            .map_err(|e| StateError::StorageError {
215                backend_type: "sled".to_string(),
216                details: format!("Failed to open database: {}", e),
217            })?;
218
219        info!(
220            "Sled database opened successfully, {} keys",
221            db.len()
222        );
223
224        Ok(Self {
225            db: Arc::new(db),
226            config,
227            stats: Arc::new(RwLock::new(SledBackendStats::default())),
228            op_counter: Arc::new(RwLock::new(0)),
229        })
230    }
231
232    /// Create a temporary Sled backend for testing
233    ///
234    /// This creates a database in a temporary directory that will be cleaned up
235    /// when the backend is dropped.
236    ///
237    /// # Returns
238    ///
239    /// A new temporary Sled backend
240    ///
241    /// # Example
242    ///
243    /// ```rust,no_run
244    /// # use processor::state::SledStateBackend;
245    /// # #[tokio::main]
246    /// # async fn main() -> anyhow::Result<()> {
247    /// let backend = SledStateBackend::temporary().await?;
248    /// // Use for testing...
249    /// # Ok(())
250    /// # }
251    /// ```
252    pub async fn temporary() -> StateResult<Self> {
253        let temp_dir = std::env::temp_dir().join(format!("sled_test_{}", uuid::Uuid::new_v4()));
254        Self::open(SledConfig::new(temp_dir)).await
255    }
256
257    /// Flush all pending writes to disk
258    ///
259    /// This ensures that all writes are persisted to disk. It's called automatically
260    /// after every N operations (configured by `flush_every`), but can also be called
261    /// manually for critical checkpoints.
262    ///
263    /// # Returns
264    ///
265    /// * `Ok(usize)` - Number of bytes flushed
266    /// * `Err(StateError)` - Flush failed
267    ///
268    /// # Example
269    ///
270    /// ```rust,no_run
271    /// # use processor::state::{SledStateBackend, SledConfig};
272    /// # #[tokio::main]
273    /// # async fn main() -> anyhow::Result<()> {
274    /// # let backend = SledStateBackend::temporary().await?;
275    /// backend.put(b"important", b"data").await?;
276    /// let flushed = backend.flush().await?;
277    /// println!("Flushed {} bytes", flushed);
278    /// # Ok(())
279    /// # }
280    /// ```
281    pub async fn flush(&self) -> StateResult<usize> {
282        trace!("Flushing Sled database");
283
284        let flushed = tokio::task::block_in_place(|| {
285            self.db.flush().map_err(|e| StateError::StorageError {
286                backend_type: "sled".to_string(),
287                details: format!("Flush failed: {}", e),
288            })
289        })?;
290
291        let mut stats = self.stats.write().await;
292        stats.flush_count += 1;
293
294        debug!("Flushed {} bytes to disk", flushed);
295        Ok(flushed)
296    }
297
298    /// Get database statistics
299    ///
300    /// # Returns
301    ///
302    /// Current backend statistics
303    pub async fn stats(&self) -> SledBackendStats {
304        self.stats.read().await.clone()
305    }
306
307    /// Get compression ratio as a percentage
308    ///
309    /// # Returns
310    ///
311    /// Compression ratio (0.0-100.0), or None if no data has been compressed yet
312    ///
313    /// # Example
314    ///
315    /// A ratio of 30.0 means compressed data is 30% of the original size
316    pub async fn compression_ratio(&self) -> Option<f64> {
317        let stats = self.stats.read().await;
318        if stats.bytes_before_compression > 0 {
319            Some((stats.bytes_after_compression as f64 / stats.bytes_before_compression as f64) * 100.0)
320        } else {
321            None
322        }
323    }
324
325    /// Get total space saved by compression in bytes
326    ///
327    /// # Returns
328    ///
329    /// Number of bytes saved by compression
330    pub async fn space_saved(&self) -> u64 {
331        let stats = self.stats.read().await;
332        stats.bytes_before_compression.saturating_sub(stats.bytes_after_compression)
333    }
334
335    /// Get the size of the database on disk in bytes
336    ///
337    /// # Returns
338    ///
339    /// Database size in bytes
340    pub async fn size_on_disk(&self) -> StateResult<u64> {
341        let path = &self.config.path;
342        let mut total_size = 0u64;
343
344        let entries = tokio::fs::read_dir(path).await.map_err(|e| {
345            StateError::StorageError {
346                backend_type: "sled".to_string(),
347                details: format!("Failed to read directory: {}", e),
348            }
349        })?;
350
351        let mut entries = entries;
352        while let Some(entry) = entries.next_entry().await.map_err(|e| {
353            StateError::StorageError {
354                backend_type: "sled".to_string(),
355                details: format!("Failed to read entry: {}", e),
356            }
357        })? {
358            if let Ok(metadata) = entry.metadata().await {
359                total_size += metadata.len();
360            }
361        }
362
363        Ok(total_size)
364    }
365
366    /// Create a checkpoint of the current database state
367    ///
368    /// This exports all key-value pairs for backup or migration.
369    ///
370    /// # Returns
371    ///
372    /// Vector of (key, value) pairs
373    pub async fn checkpoint(&self) -> StateResult<Vec<(Vec<u8>, Vec<u8>)>> {
374        info!("Creating Sled checkpoint");
375
376        let checkpoint = tokio::task::block_in_place(|| {
377            self.db
378                .iter()
379                .map(|result| {
380                    result
381                        .map(|(k, v)| (k.to_vec(), v.to_vec()))
382                        .map_err(|e| StateError::StorageError {
383                            backend_type: "sled".to_string(),
384                            details: format!("Checkpoint iteration failed: {}", e),
385                        })
386                })
387                .collect::<StateResult<Vec<_>>>()
388        })?;
389
390        info!("Checkpoint created with {} entries", checkpoint.len());
391        Ok(checkpoint)
392    }
393
394    /// Restore from a checkpoint
395    ///
396    /// This clears the current database and restores from the provided checkpoint.
397    ///
398    /// # Arguments
399    ///
400    /// * `checkpoint` - Vector of (key, value) pairs to restore
401    ///
402    /// # Returns
403    ///
404    /// * `Ok(())` - Restore successful
405    /// * `Err(StateError)` - Restore failed
406    pub async fn restore(&self, checkpoint: Vec<(Vec<u8>, Vec<u8>)>) -> StateResult<()> {
407        info!("Restoring Sled from checkpoint with {} entries", checkpoint.len());
408
409        tokio::task::block_in_place(|| {
410            // Clear existing data
411            self.db.clear().map_err(|e| StateError::StorageError {
412                backend_type: "sled".to_string(),
413                details: format!("Clear failed: {}", e),
414            })?;
415
416            // Restore checkpoint
417            for (key, value) in checkpoint {
418                self.db.insert(key, value).map_err(|e| StateError::StorageError {
419                    backend_type: "sled".to_string(),
420                    details: format!("Insert failed during restore: {}", e),
421                })?;
422            }
423
424            // Flush to disk
425            self.db.flush().map_err(|e| StateError::StorageError {
426                backend_type: "sled".to_string(),
427                details: format!("Flush failed after restore: {}", e),
428            })?;
429
430            Ok(())
431        })?;
432
433        info!("Restore completed successfully");
434        Ok(())
435    }
436
437    /// Check if automatic flush is needed and perform it
438    async fn maybe_flush(&self) -> StateResult<()> {
439        let mut counter = self.op_counter.write().await;
440        *counter += 1;
441
442        if *counter >= self.config.flush_every {
443            *counter = 0;
444            drop(counter); // Release lock before flush
445            self.flush().await?;
446        }
447
448        Ok(())
449    }
450}
451
452#[async_trait]
453impl StateBackend for SledStateBackend {
454    async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
455        trace!("Getting key from Sled: {:?}", key);
456
457        let result = tokio::task::block_in_place(|| {
458            self.db.get(key).map_err(|e| StateError::StorageError {
459                backend_type: "sled".to_string(),
460                details: format!("Get failed: {}", e),
461            })
462        })?;
463
464        let mut stats = self.stats.write().await;
465        stats.get_count += 1;
466
467        if let Some(compressed_value) = result {
468            let compressed_len = compressed_value.len() as u64;
469            stats.bytes_read += compressed_len;
470
471            // Decompress the value
472            let decompressed = decompress(&compressed_value, &self.config.compression)
473                .map_err(|e| StateError::StorageError {
474                    backend_type: "sled".to_string(),
475                    details: format!("Decompression failed: {}", e),
476                })?;
477
478            let decompressed_len = decompressed.len() as u64;
479            stats.bytes_decompressed += decompressed_len;
480
481            trace!(
482                "Decompressed {} bytes to {} bytes (ratio: {:.2}%)",
483                compressed_len,
484                decompressed_len,
485                (compressed_len as f64 / decompressed_len as f64) * 100.0
486            );
487
488            Ok(Some(decompressed))
489        } else {
490            Ok(None)
491        }
492    }
493
494    async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
495        trace!("Putting key to Sled: {:?}, value size: {} bytes", key, value.len());
496
497        let original_len = value.len() as u64;
498
499        // Compress the value
500        let compressed = compress(value, &self.config.compression)
501            .map_err(|e| StateError::StorageError {
502                backend_type: "sled".to_string(),
503                details: format!("Compression failed: {}", e),
504            })?;
505
506        let compressed_len = compressed.len() as u64;
507
508        trace!(
509            "Compressed {} bytes to {} bytes (ratio: {:.2}%)",
510            original_len,
511            compressed_len,
512            (compressed_len as f64 / original_len as f64) * 100.0
513        );
514
515        tokio::task::block_in_place(|| {
516            self.db.insert(key, compressed.as_slice()).map_err(|e| StateError::StorageError {
517                backend_type: "sled".to_string(),
518                details: format!("Put failed: {}", e),
519            })
520        })?;
521
522        let mut stats = self.stats.write().await;
523        stats.put_count += 1;
524        stats.bytes_written += (key.len() as u64 + compressed_len);
525        stats.bytes_compressed += original_len;
526        stats.bytes_before_compression += original_len;
527        stats.bytes_after_compression += compressed_len;
528        drop(stats);
529
530        self.maybe_flush().await?;
531
532        Ok(())
533    }
534
535    async fn delete(&self, key: &[u8]) -> StateResult<()> {
536        trace!("Deleting key from Sled: {:?}", key);
537
538        tokio::task::block_in_place(|| {
539            self.db.remove(key).map_err(|e| StateError::StorageError {
540                backend_type: "sled".to_string(),
541                details: format!("Delete failed: {}", e),
542            })
543        })?;
544
545        let mut stats = self.stats.write().await;
546        stats.delete_count += 1;
547        drop(stats);
548
549        self.maybe_flush().await?;
550
551        Ok(())
552    }
553
554    async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
555        trace!("Listing keys with prefix: {:?}", prefix);
556
557        let keys = tokio::task::block_in_place(|| {
558            if prefix.is_empty() {
559                // List all keys
560                self.db
561                    .iter()
562                    .keys()
563                    .map(|result| {
564                        result
565                            .map(|k| k.to_vec())
566                            .map_err(|e| StateError::StorageError {
567                                backend_type: "sled".to_string(),
568                                details: format!("Key iteration failed: {}", e),
569                            })
570                    })
571                    .collect::<StateResult<Vec<_>>>()
572            } else {
573                // List keys with prefix
574                self.db
575                    .scan_prefix(prefix)
576                    .keys()
577                    .map(|result| {
578                        result
579                            .map(|k| k.to_vec())
580                            .map_err(|e| StateError::StorageError {
581                                backend_type: "sled".to_string(),
582                                details: format!("Prefix scan failed: {}", e),
583                            })
584                    })
585                    .collect::<StateResult<Vec<_>>>()
586            }
587        })?;
588
589        Ok(keys)
590    }
591
592    async fn clear(&self) -> StateResult<()> {
593        debug!("Clearing Sled database");
594
595        tokio::task::block_in_place(|| {
596            self.db.clear().map_err(|e| StateError::StorageError {
597                backend_type: "sled".to_string(),
598                details: format!("Clear failed: {}", e),
599            })
600        })?;
601
602        self.flush().await?;
603
604        Ok(())
605    }
606
607    async fn count(&self) -> StateResult<usize> {
608        Ok(self.db.len())
609    }
610
611    async fn contains(&self, key: &[u8]) -> StateResult<bool> {
612        Ok(tokio::task::block_in_place(|| {
613            self.db.contains_key(key).unwrap_or(false)
614        }))
615    }
616}
617
618// Implement Drop to ensure clean shutdown
619impl Drop for SledStateBackend {
620    fn drop(&mut self) {
621        if let Err(e) = self.db.flush() {
622            error!("Failed to flush Sled database on drop: {}", e);
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::state::backend::tests::*;
631
632    async fn create_test_backend() -> SledStateBackend {
633        SledStateBackend::temporary().await.unwrap()
634    }
635
636    #[tokio::test]
637    async fn test_sled_backend_basic() {
638        let backend = create_test_backend().await;
639        test_backend_basic_ops(backend).await;
640    }
641
642    #[tokio::test]
643    async fn test_sled_backend_list_keys() {
644        let backend = create_test_backend().await;
645        test_backend_list_keys(backend).await;
646    }
647
648    #[tokio::test]
649    async fn test_sled_backend_clear() {
650        let backend = create_test_backend().await;
651        test_backend_clear(backend).await;
652    }
653
654    #[tokio::test]
655    async fn test_sled_backend_contains() {
656        let backend = create_test_backend().await;
657        test_backend_contains(backend).await;
658    }
659
660    #[tokio::test]
661    async fn test_sled_persistence() {
662        let temp_dir = std::env::temp_dir().join(format!("sled_persist_test_{}", uuid::Uuid::new_v4()));
663        let config = SledConfig::new(&temp_dir);
664
665        // Create backend and add data
666        {
667            let backend = SledStateBackend::open(config.clone()).await.unwrap();
668            backend.put(b"key1", b"value1").await.unwrap();
669            backend.put(b"key2", b"value2").await.unwrap();
670            backend.flush().await.unwrap();
671            // Backend dropped here
672        }
673
674        // Reopen and verify data persisted
675        {
676            let backend = SledStateBackend::open(config).await.unwrap();
677            assert_eq!(backend.count().await.unwrap(), 2);
678            assert_eq!(
679                backend.get(b"key1").await.unwrap(),
680                Some(b"value1".to_vec())
681            );
682            assert_eq!(
683                backend.get(b"key2").await.unwrap(),
684                Some(b"value2".to_vec())
685            );
686        }
687
688        // Cleanup
689        tokio::fs::remove_dir_all(&temp_dir).await.ok();
690    }
691
692    #[tokio::test]
693    async fn test_checkpoint_restore() {
694        let backend = create_test_backend().await;
695
696        backend.put(b"key1", b"value1").await.unwrap();
697        backend.put(b"key2", b"value2").await.unwrap();
698
699        // Create checkpoint
700        let checkpoint = backend.checkpoint().await.unwrap();
701        assert_eq!(checkpoint.len(), 2);
702
703        // Clear and restore
704        backend.clear().await.unwrap();
705        assert_eq!(backend.count().await.unwrap(), 0);
706
707        backend.restore(checkpoint).await.unwrap();
708        assert_eq!(backend.count().await.unwrap(), 2);
709
710        assert_eq!(
711            backend.get(b"key1").await.unwrap(),
712            Some(b"value1".to_vec())
713        );
714    }
715
716    #[tokio::test]
717    async fn test_stats() {
718        let backend = create_test_backend().await;
719
720        backend.put(b"key1", b"value1").await.unwrap();
721        backend.put(b"key2", b"value2").await.unwrap();
722
723        backend.get(b"key1").await.unwrap();
724        backend.delete(b"key1").await.unwrap();
725
726        let stats = backend.stats().await;
727        assert_eq!(stats.put_count, 2);
728        assert_eq!(stats.get_count, 1);
729        assert_eq!(stats.delete_count, 1);
730        assert!(stats.bytes_written > 0);
731        assert!(stats.bytes_read > 0);
732    }
733
734    #[tokio::test]
735    async fn test_size_on_disk() {
736        let backend = create_test_backend().await;
737
738        let initial_size = backend.size_on_disk().await.unwrap();
739
740        // Add some data
741        for i in 0..100 {
742            let key = format!("key:{}", i).into_bytes();
743            let value = vec![0u8; 1000]; // 1KB value
744            backend.put(&key, &value).await.unwrap();
745        }
746
747        backend.flush().await.unwrap();
748
749        let final_size = backend.size_on_disk().await.unwrap();
750        assert!(final_size > initial_size);
751    }
752
753    #[tokio::test]
754    async fn test_concurrent_access() {
755        use std::sync::Arc;
756
757        let backend = Arc::new(create_test_backend().await);
758        let mut handles = vec![];
759
760        // Spawn multiple tasks writing concurrently
761        for i in 0..10 {
762            let backend = Arc::clone(&backend);
763            let handle = tokio::spawn(async move {
764                for j in 0..10 {
765                    let key = format!("key:{}:{}", i, j).into_bytes();
766                    let value = format!("value:{}:{}", i, j).into_bytes();
767                    backend.put(&key, &value).await.unwrap();
768                }
769            });
770            handles.push(handle);
771        }
772
773        // Wait for all tasks
774        for handle in handles {
775            handle.await.unwrap();
776        }
777
778        backend.flush().await.unwrap();
779
780        // Verify all entries were written
781        assert_eq!(backend.count().await.unwrap(), 100);
782    }
783
784    // ============================================================================
785    // Compression Tests
786    // ============================================================================
787
788    #[tokio::test]
789    async fn test_compression_roundtrip() {
790        let backend = create_test_backend().await;
791
792        let test_data = b"This is some test data that should be compressed";
793
794        // Write and read back
795        backend.put(b"test_key", test_data).await.unwrap();
796        let result = backend.get(b"test_key").await.unwrap();
797
798        assert_eq!(result, Some(test_data.to_vec()));
799    }
800
801    #[tokio::test]
802    async fn test_compression_algorithms() {
803        let test_data = vec![b'x'; 10000]; // 10KB of repeated data
804
805        // Test with LZ4
806        {
807            let temp_dir = std::env::temp_dir().join(format!("sled_lz4_test_{}", uuid::Uuid::new_v4()));
808            let config = SledConfig::new(&temp_dir).with_fast_compression();
809            let backend = SledStateBackend::open(config).await.unwrap();
810
811            backend.put(b"key", &test_data).await.unwrap();
812            let result = backend.get(b"key").await.unwrap().unwrap();
813            assert_eq!(result, test_data);
814
815            tokio::fs::remove_dir_all(&temp_dir).await.ok();
816        }
817
818        // Test with Snappy
819        {
820            let temp_dir = std::env::temp_dir().join(format!("sled_snappy_test_{}", uuid::Uuid::new_v4()));
821            let config = SledConfig::new(&temp_dir).with_balanced_compression();
822            let backend = SledStateBackend::open(config).await.unwrap();
823
824            backend.put(b"key", &test_data).await.unwrap();
825            let result = backend.get(b"key").await.unwrap().unwrap();
826            assert_eq!(result, test_data);
827
828            tokio::fs::remove_dir_all(&temp_dir).await.ok();
829        }
830
831        // Test with Zstd
832        {
833            let temp_dir = std::env::temp_dir().join(format!("sled_zstd_test_{}", uuid::Uuid::new_v4()));
834            let config = SledConfig::new(&temp_dir).with_best_compression();
835            let backend = SledStateBackend::open(config).await.unwrap();
836
837            backend.put(b"key", &test_data).await.unwrap();
838            let result = backend.get(b"key").await.unwrap().unwrap();
839            assert_eq!(result, test_data);
840
841            tokio::fs::remove_dir_all(&temp_dir).await.ok();
842        }
843
844        // Test with no compression
845        {
846            let temp_dir = std::env::temp_dir().join(format!("sled_none_test_{}", uuid::Uuid::new_v4()));
847            let config = SledConfig::new(&temp_dir).without_compression();
848            let backend = SledStateBackend::open(config).await.unwrap();
849
850            backend.put(b"key", &test_data).await.unwrap();
851            let result = backend.get(b"key").await.unwrap().unwrap();
852            assert_eq!(result, test_data);
853
854            tokio::fs::remove_dir_all(&temp_dir).await.ok();
855        }
856    }
857
858    #[tokio::test]
859    async fn test_compression_stats() {
860        let backend = create_test_backend().await;
861
862        // Write compressible data
863        let data = vec![b'A'; 10000]; // 10KB of repeated data
864        backend.put(b"key", &data).await.unwrap();
865
866        let stats = backend.stats().await;
867        assert!(stats.bytes_compressed > 0);
868        assert!(stats.bytes_before_compression >= 10000);
869        assert!(stats.bytes_after_compression > 0);
870
871        // Compression should have saved space
872        assert!(stats.bytes_after_compression < stats.bytes_before_compression);
873    }
874
875    #[tokio::test]
876    async fn test_compression_ratio() {
877        let backend = create_test_backend().await;
878
879        // Write highly compressible data
880        let data = vec![0u8; 100000]; // 100KB of zeros
881        backend.put(b"key", &data).await.unwrap();
882
883        let ratio = backend.compression_ratio().await;
884        assert!(ratio.is_some());
885
886        let ratio_value = ratio.unwrap();
887        assert!(ratio_value > 0.0);
888        assert!(ratio_value < 100.0); // Should be compressed
889
890        println!("Compression ratio: {:.2}%", ratio_value);
891    }
892
893    #[tokio::test]
894    async fn test_space_saved() {
895        let backend = create_test_backend().await;
896
897        // Write compressible data
898        let data = vec![b'X'; 50000]; // 50KB of repeated data
899        backend.put(b"key", &data).await.unwrap();
900
901        let space_saved = backend.space_saved().await;
902        assert!(space_saved > 0);
903
904        println!("Space saved: {} bytes", space_saved);
905    }
906
907    #[tokio::test]
908    async fn test_large_data_compression() {
909        let backend = create_test_backend().await;
910
911        // Create 1MB of repeated data (highly compressible)
912        let large_data = vec![b'Z'; 1024 * 1024];
913
914        backend.put(b"large_key", &large_data).await.unwrap();
915        let result = backend.get(b"large_key").await.unwrap().unwrap();
916
917        assert_eq!(result.len(), large_data.len());
918        assert_eq!(result, large_data);
919
920        // Verify compression was effective
921        let stats = backend.stats().await;
922        let compression_ratio = (stats.bytes_after_compression as f64
923            / stats.bytes_before_compression as f64) * 100.0;
924
925        println!("Large data compression ratio: {:.2}%", compression_ratio);
926
927        // With highly compressible data, ratio should be very low
928        assert!(compression_ratio < 10.0, "Compression ratio should be less than 10% for repeated data");
929    }
930
931    #[tokio::test]
932    async fn test_mixed_data_compression() {
933        let backend = create_test_backend().await;
934
935        // Mix of compressible and incompressible data
936        let compressible = vec![b'A'; 5000];
937        let random = (0..5000).map(|i| (i % 256) as u8).collect::<Vec<_>>();
938
939        backend.put(b"compressible", &compressible).await.unwrap();
940        backend.put(b"random", &random).await.unwrap();
941
942        // Verify both round-trip correctly
943        assert_eq!(
944            backend.get(b"compressible").await.unwrap().unwrap(),
945            compressible
946        );
947        assert_eq!(
948            backend.get(b"random").await.unwrap().unwrap(),
949            random
950        );
951
952        let stats = backend.stats().await;
953        assert!(stats.bytes_compressed > 0);
954        assert!(stats.bytes_decompressed > 0);
955    }
956
957    #[tokio::test]
958    async fn test_compression_persistence() {
959        let temp_dir = std::env::temp_dir().join(format!("sled_compress_persist_{}", uuid::Uuid::new_v4()));
960        let config = SledConfig::new(&temp_dir).with_best_compression();
961
962        let test_data = vec![b'B'; 20000];
963
964        // Write with compression
965        {
966            let backend = SledStateBackend::open(config.clone()).await.unwrap();
967            backend.put(b"key", &test_data).await.unwrap();
968            backend.flush().await.unwrap();
969        }
970
971        // Read back after restart
972        {
973            let backend = SledStateBackend::open(config).await.unwrap();
974            let result = backend.get(b"key").await.unwrap().unwrap();
975            assert_eq!(result, test_data);
976        }
977
978        // Cleanup
979        tokio::fs::remove_dir_all(&temp_dir).await.ok();
980    }
981
982    #[tokio::test]
983    async fn test_compression_with_custom_config() {
984        use super::compression::{CompressionAlgorithm, CompressionConfig};
985
986        let temp_dir = std::env::temp_dir().join(format!("sled_custom_compress_{}", uuid::Uuid::new_v4()));
987
988        // Custom compression: Zstd level 15 with 512 byte threshold
989        let compression_config = CompressionConfig {
990            algorithm: CompressionAlgorithm::Zstd,
991            level: 15,
992            threshold: 512,
993            enable_stats: true,
994        };
995
996        let config = SledConfig::new(&temp_dir).with_compression(compression_config);
997        let backend = SledStateBackend::open(config).await.unwrap();
998
999        // Small data (below threshold) - should not be compressed
1000        let small_data = vec![b'S'; 256];
1001        backend.put(b"small", &small_data).await.unwrap();
1002
1003        // Large data (above threshold) - should be compressed
1004        let large_data = vec![b'L'; 10000];
1005        backend.put(b"large", &large_data).await.unwrap();
1006
1007        // Verify both round-trip correctly
1008        assert_eq!(
1009            backend.get(b"small").await.unwrap().unwrap(),
1010            small_data
1011        );
1012        assert_eq!(
1013            backend.get(b"large").await.unwrap().unwrap(),
1014            large_data
1015        );
1016
1017        // Cleanup
1018        tokio::fs::remove_dir_all(&temp_dir).await.ok();
1019    }
1020
1021    #[tokio::test]
1022    async fn test_empty_value_compression() {
1023        let backend = create_test_backend().await;
1024
1025        // Empty value
1026        backend.put(b"empty", b"").await.unwrap();
1027        let result = backend.get(b"empty").await.unwrap();
1028        assert_eq!(result, Some(vec![]));
1029    }
1030
1031    #[tokio::test]
1032    async fn test_compression_stats_accuracy() {
1033        let backend = create_test_backend().await;
1034
1035        let data1 = vec![b'1'; 5000];
1036        let data2 = vec![b'2'; 8000];
1037        let data3 = vec![b'3'; 12000];
1038
1039        backend.put(b"key1", &data1).await.unwrap();
1040        backend.put(b"key2", &data2).await.unwrap();
1041        backend.put(b"key3", &data3).await.unwrap();
1042
1043        let stats = backend.stats().await;
1044
1045        // Total uncompressed should be at least 25000 bytes
1046        assert!(stats.bytes_before_compression >= 25000);
1047
1048        // Compressed should be less (for repeated data)
1049        assert!(stats.bytes_after_compression < stats.bytes_before_compression);
1050
1051        // Read them all back
1052        backend.get(b"key1").await.unwrap();
1053        backend.get(b"key2").await.unwrap();
1054        backend.get(b"key3").await.unwrap();
1055
1056        let stats = backend.stats().await;
1057        assert!(stats.bytes_decompressed >= 25000);
1058    }
1059}