pywatt_sdk 0.5.3

Standardized SDK for building PyWatt modules in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
use crate::data::cache::{CacheConfig, CacheResult, CacheService, CacheStats, CacheType};
use async_trait::async_trait;
use dashmap::DashMap;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use uuid::Uuid;

/// Cache entry with value and expiration time
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheEntry {
    /// The stored value
    value: Vec<u8>,
    /// When the entry was created (Unix timestamp in seconds)
    #[allow(dead_code)]
    created_at_unix_secs: u64,
    /// When the entry will expire (Unix timestamp in seconds, if any)
    expires_at_unix_secs: Option<u64>,
    #[allow(dead_code)]
    frequency: u64, // For LFU
}

impl CacheEntry {
    /// Create a new cache entry
    fn new(value: Vec<u8>, ttl: Option<Duration>) -> Self {
        let now_unix_secs = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let expires_at_unix_secs = ttl.map(|d| now_unix_secs + d.as_secs());

        Self {
            value,
            created_at_unix_secs: now_unix_secs,
            expires_at_unix_secs,
            frequency: 0, // For LFU
        }
    }

    /// Check if the entry is expired
    fn is_expired(&self) -> bool {
        if let Some(exp_secs) = self.expires_at_unix_secs {
            let now_unix_secs = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            return now_unix_secs >= exp_secs;
        }
        false
    }
}

/// In-memory cache implementation using DashMap
pub struct InMemoryCache {
    /// The cache storage
    cache: Arc<DashMap<String, CacheEntry>>,
    /// Default TTL for cache entries
    default_ttl: Duration,
    /// Namespace prefix for cache keys
    namespace: Option<String>,
    /// Maximum number of items to store (LRU-like eviction if set)
    max_items: Option<usize>,
    /// Cache hit counter
    hits: Arc<AtomicU64>,
    /// Cache miss counter
    misses: Arc<AtomicU64>,
    /// Last cleanup time
    last_cleanup: Arc<std::sync::Mutex<Instant>>,
}

impl InMemoryCache {
    /// Create a new in-memory cache with provided configuration
    pub fn new(config: &CacheConfig) -> Self {
        // Use helper accessor that resolves duration from CacheConfig
        let default_ttl = config.get_default_ttl();
        let max_items = config.max_size_bytes.map(|bytes| {
            bytes / 100
        });

        let cache = Self {
            cache: Arc::new(DashMap::new()),
            default_ttl,
            namespace: config.namespace.clone(),
            max_items,
            hits: Arc::new(AtomicU64::new(0)),
            misses: Arc::new(AtomicU64::new(0)),
            last_cleanup: Arc::new(std::sync::Mutex::new(Instant::now())),
        };

        // Spawn background cleanup task if TTL is set
        if default_ttl.as_secs() > 0 {
            let cache_ref = cache.cache.clone();
            let cleanup_interval = Duration::from_secs(
                // Clean up every 10% of default TTL or every minute, whichever is greater
                std::cmp::max(default_ttl.as_secs() / 10, 60),
            );
            let last_cleanup = cache.last_cleanup.clone();

            tokio::spawn(async move {
                let mut interval = tokio::time::interval(cleanup_interval);
                loop {
                    interval.tick().await;

                    // Only proceed if it's been at least the cleanup interval
                    let mut last = last_cleanup.lock().unwrap();
                    if Instant::now().duration_since(*last) < cleanup_interval {
                        continue;
                    }
                    *last = Instant::now();

                    // Remove expired entries
                    cache_ref.retain(|_, entry| !entry.is_expired());
                }
            });
        }

        cache
    }

    /// Add namespace prefix to key if configured
    fn prefix_key(&self, key: &str) -> String {
        if let Some(ns) = &self.namespace {
            format!("{}:{}", ns, key)
        } else {
            key.to_string()
        }
    }

    /// Check if an entry exists and is not expired
    fn check_entry(&self, key: &str) -> bool {
        let key = self.prefix_key(key);
        if let Some(entry) = self.cache.get(&key) {
            if entry.is_expired() {
                // Don't remove here, let `get` handle it or the background task
                // self.cache.remove(&key);
                self.misses.fetch_add(1, Ordering::Relaxed);
                false
            } else {
                self.hits.fetch_add(1, Ordering::Relaxed);
                true
            }
        } else {
            self.misses.fetch_add(1, Ordering::Relaxed);
            false
        }
    }

    /// Perform LRU eviction if needed
    fn maybe_evict(&self) {
        if let Some(max_items) = self.max_items {
            if self.cache.len() >= max_items {
                // Simple random eviction strategy - evict ~10% of max items
                let evict_count = max_items / 10;
                if evict_count > 0 {
                    // Get all keys
                    let keys: Vec<String> =
                        self.cache.iter().map(|entry| entry.key().clone()).collect();

                    // Use vector indexing to avoid choose_multiple issues
                    #[allow(deprecated)]
                    let mut rng = rand::thread_rng();
                    let mut indices: Vec<usize> = (0..keys.len()).collect();
                    indices.shuffle(&mut rng);
                    let indices = indices.iter().take(evict_count);

                    // Extract keys by random indices
                    for &idx in indices {
                        if idx < keys.len() {
                            self.cache.remove(&keys[idx]);
                        }
                    }
                }
            }
        }
    }
}

#[async_trait]
impl CacheService for InMemoryCache {
    async fn get(&self, key: &str) -> CacheResult<Option<Vec<u8>>> {
        let key = self.prefix_key(key);

        if let Some(entry) = self.cache.get(&key) {
            if entry.is_expired() {
                // We must drop the read guard before attempting to mutate the map.
                self.misses.fetch_add(1, Ordering::Relaxed);
                // Explicitly release the guard to avoid deadlock.
                drop(entry);
                // Now it is safe to remove the expired entry.
                self.cache.remove(&key);
                Ok(None)
            } else {
                self.hits.fetch_add(1, Ordering::Relaxed);
                Ok(Some(entry.value.clone()))
            }
        } else {
            self.misses.fetch_add(1, Ordering::Relaxed);
            Ok(None)
        }
    }

    async fn set(&self, key: &str, value: &[u8], ttl: Option<Duration>) -> CacheResult<()> {
        let key = self.prefix_key(key);
        let ttl = ttl.or(Some(self.default_ttl));

        // Create new entry
        let entry = CacheEntry::new(value.to_vec(), ttl);

        // Check if we need eviction
        self.maybe_evict();

        // Insert entry
        self.cache.insert(key, entry);

        Ok(())
    }

    async fn delete(&self, key: &str) -> CacheResult<bool> {
        let key = self.prefix_key(key);
        Ok(self.cache.remove(&key).is_some())
    }

    async fn exists(&self, key: &str) -> CacheResult<bool> {
        Ok(self.check_entry(key))
    }

    async fn set_nx(&self, key: &str, value: &[u8], ttl: Option<Duration>) -> CacheResult<bool> {
        // Prepare fully-qualified key once to avoid accidental double prefixing.
        let namespaced_key = self.prefix_key(key);

        // Fast path – check if a *valid* entry already exists.
        if let Some(entry) = self.cache.get(&namespaced_key) {
            if !entry.is_expired() {
                // Existing and still valid – NX condition fails.
                return Ok(false);
            }
            // Entry is expired – drop guard before we mutate the map.
            drop(entry);
            // Remove stale item so we can insert the fresh value.
            self.cache.remove(&namespaced_key);
        }

        // Either the key was missing or we just removed an expired value – insert the new one.
        let ttl = ttl.or(Some(self.default_ttl));
        let entry = CacheEntry::new(value.to_vec(), ttl);

        // Evict if necessary before inserting.
        self.maybe_evict();

        self.cache.insert(namespaced_key, entry);
        Ok(true)
    }

    async fn get_set(&self, key: &str, value: &[u8]) -> CacheResult<Option<Vec<u8>>> {
        let key = self.prefix_key(key);

        // Get existing value
        let old_value = if let Some(entry) = self.cache.get(&key) {
            if entry.is_expired() {
                self.misses.fetch_add(1, Ordering::Relaxed);
                None
            } else {
                self.hits.fetch_add(1, Ordering::Relaxed);
                Some(entry.value.clone())
            }
        } else {
            self.misses.fetch_add(1, Ordering::Relaxed);
            None
        };

        // Set new value
        let entry = CacheEntry::new(value.to_vec(), Some(self.default_ttl));

        // Check if we need eviction
        self.maybe_evict();

        // Insert entry
        self.cache.insert(key, entry);

        Ok(old_value)
    }

    async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
        let key = self.prefix_key(key);

        // Lock the entire operation to ensure atomicity
        let value = {
            // Get current value if exists
            let current_value = if let Some(entry) = self.cache.get(&key) {
                if entry.is_expired() {
                    self.misses.fetch_add(1, Ordering::Relaxed);
                    // Start from 0 if expired
                    0
                } else {
                    self.hits.fetch_add(1, Ordering::Relaxed);
                    // Parse current value
                    let bytes = &entry.value;
                    String::from_utf8_lossy(bytes).parse::<i64>().unwrap_or(0)
                }
            } else {
                self.misses.fetch_add(1, Ordering::Relaxed);
                // Start from 0 if not exists
                0
            };

            // Calculate new value
            let new_value = current_value + delta;

            // Store new value
            let entry = CacheEntry::new(new_value.to_string().into_bytes(), Some(self.default_ttl));

            // Check if we need eviction
            self.maybe_evict();

            // Insert entry
            self.cache.insert(key, entry);

            new_value
        };

        Ok(value)
    }

    async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
        // Decrement by delegating to increment with negative delta.
        self.increment(key, -delta).await
    }

    async fn set_many(
        &self,
        items: &HashMap<String, Vec<u8>>,
        ttl: Option<Duration>,
    ) -> CacheResult<()> {
        if items.is_empty() {
            return Ok(());
        }

        let ttl = ttl.or(Some(self.default_ttl));

        // Check if we might need eviction
        if let Some(max_items) = self.max_items {
            if self.cache.len() + items.len() > max_items {
                // Evict at least enough to make room for new items
                let min_evict = (self.cache.len() + items.len() - max_items) + 1;

                // Get all keys
                let keys: Vec<String> =
                    self.cache.iter().map(|entry| entry.key().clone()).collect();

                // Use vector indexing to avoid choose_multiple issues
                #[allow(deprecated)]
                let mut rng = rand::thread_rng();
                let mut indices: Vec<usize> = (0..keys.len()).collect();
                indices.shuffle(&mut rng);
                let indices = indices.iter().take(min_evict);

                // Extract keys by random indices
                for &idx in indices {
                    if idx < keys.len() {
                        self.cache.remove(&keys[idx]);
                    }
                }
            }
        }

        // Insert all items
        for (key, value) in items {
            let key = self.prefix_key(key);
            let entry = CacheEntry::new(value.clone(), ttl);
            self.cache.insert(key, entry);
        }

        Ok(())
    }

    async fn get_many(&self, keys: &[String]) -> CacheResult<HashMap<String, Vec<u8>>> {
        if keys.is_empty() {
            return Ok(HashMap::new());
        }

        let mut result = HashMap::with_capacity(keys.len());

        for key in keys {
            let prefixed_key = self.prefix_key(key);

            if let Some(entry) = self.cache.get(&prefixed_key) {
                if !entry.is_expired() {
                    self.hits.fetch_add(1, Ordering::Relaxed);
                    result.insert(key.clone(), entry.value.clone());
                } else {
                    self.misses.fetch_add(1, Ordering::Relaxed);
                    // Ensure we release the guard before mutation.
                    drop(entry);
                    // Remove expired entry
                    self.cache.remove(&prefixed_key);
                }
            } else {
                self.misses.fetch_add(1, Ordering::Relaxed);
            }
        }

        Ok(result)
    }

    async fn delete_many(&self, keys: &[String]) -> CacheResult<u64> {
        if keys.is_empty() {
            return Ok(0);
        }

        let mut count = 0;

        for key in keys {
            let prefixed_key = self.prefix_key(key);
            if self.cache.remove(&prefixed_key).is_some() {
                count += 1;
            }
        }

        Ok(count)
    }

    async fn clear(&self, namespace: Option<&str>) -> CacheResult<()> {
        // Check if we need to clear the entire cache or just a namespace
        if namespace.is_none() && self.namespace.is_none() {
            // Clear everything
            self.cache.clear();
            return Ok(());
        }

        // Use namespace from parameter, or object's namespace if not specified
        let ns_prefix = if let Some(ns) = namespace {
            format!("{}:", ns)
        } else if let Some(ns) = &self.namespace {
            format!("{}:", ns)
        } else {
            // No namespace, nothing to do
            return Ok(());
        };

        // Get all keys matching the namespace
        let keys_to_delete: Vec<String> = self
            .cache
            .iter()
            .map(|entry| entry.key().clone())
            .filter(|key| key.starts_with(&ns_prefix))
            .collect();

        // Delete matching keys
        for key in keys_to_delete {
            self.cache.remove(&key);
        }

        Ok(())
    }

    async fn lock(&self, key: &str, ttl: Duration) -> CacheResult<Option<String>> {
        let lock_key = self.prefix_key(&format!("lock:{}", key));
        let token = Uuid::new_v4().to_string();

        // Using a simpler approach to match the set_nx pattern but with explicit lock semantics

        // First, check if there's an existing valid lock
        let existing_lock = if let Some(entry) = self.cache.get(&lock_key) {
            !entry.is_expired()
        } else {
            false
        };

        // If a valid lock exists, return None (couldn't acquire)
        if existing_lock {
            return Ok(None);
        }

        // Otherwise, create a new lock with our token
        let entry = CacheEntry::new(token.clone().into_bytes(), Some(ttl));

        // Check if we need eviction before inserting
        self.maybe_evict();

        // Insert our lock and token
        self.cache.insert(lock_key, entry);

        // Return the token for release later
        Ok(Some(token))
    }

    async fn unlock(&self, key: &str, lock_token: &str) -> CacheResult<bool> {
        let lock_key = self.prefix_key(&format!("lock:{}", key));

        // Check if lock exists and token matches
        if let Some(entry) = self.cache.get(&lock_key) {
            if !entry.is_expired() {
                let current_token = String::from_utf8_lossy(&entry.value);
                if current_token == lock_token {
                    // Release the read guard before mutating the map to avoid deadlock.
                    drop(entry);
                    // Remove the lock
                    self.cache.remove(&lock_key);
                    return Ok(true);
                }
            }
        }

        Ok(false)
    }

    fn get_cache_type(&self) -> CacheType {
        CacheType::InMemory
    }

    async fn ping(&self) -> CacheResult<()> {
        // In-memory cache is always available
        Ok(())
    }

    async fn close(&self) -> CacheResult<()> {
        // No need to close an in-memory cache
        Ok(())
    }

    fn get_default_ttl(&self) -> Duration {
        self.default_ttl
    }

    async fn stats(&self) -> CacheResult<CacheStats> {
        let mut memory_usage = 0;
        let mut item_count = 0;

        // Calculate approximate memory usage
        for entry in self.cache.iter() {
            // Each entry has overhead plus the key and value sizes
            let entry_size = entry.key().len() + entry.value.len() + 32; // 32 bytes approximate overhead
            memory_usage += entry_size;
            item_count += 1;
        }

        Ok(CacheStats {
            item_count: Some(item_count as u64),
            memory_used_bytes: Some(memory_usage as u64),
            hits: Some(self.hits.load(Ordering::Relaxed)),
            misses: Some(self.misses.load(Ordering::Relaxed)),
            sets: None,
            deletes: None,
            additional_metrics: HashMap::new(),
        })
    }

    async fn flush(&self) -> CacheResult<()> {
        self.cache.clear();
        Ok(())
    }
}