cal_redis/
cache.rs

1use crate::local_cache::{build_account_identifiers_locally, build_region_identifiers_locally};
2use crate::proxy::{get_proxies, get_proxy_by_id};
3use crate::redis_cache::RedisCache;
4use crate::{
5    get_account_by_id, get_accounts, get_asset, get_assets, get_ddi, get_ddis, get_device,
6    get_devices, get_hook, get_hooks, get_region_by_id, get_regions, get_trunk, get_trunk_and_ddi,
7    get_trunks,
8};
9use cal_core::device::device::DeviceStruct;
10use cal_core::{AccountLite, Asset, FlowState, Hook, Proxy, RedisEvent, Region, Trunk, DDI};
11use moka::sync::Cache;
12use redis::aio::MultiplexedConnection;
13use redis::{AsyncCommands, PushKind, RedisError, Value};
14use std::collections::HashMap;
15use std::env;
16use std::time::Duration;
17use tokio::sync::mpsc::unbounded_channel;
18
19/// Main cache structure that combines local in-memory caching with remote Redis storage.
20///
21/// Provides a unified interface for accessing data with automatic fallback to Redis
22/// when items aren't found in the local cache.
23#[derive(Clone)]
24pub struct CallableCache {
25    pub local_cache: LocalCache,
26    pub remote_cache: RedisCache,
27}
28
29/// In-memory cache for storing frequently accessed data.
30///
31/// Uses Moka cache for efficient memory usage with TTL-based expiration.
32#[derive(Clone)]
33pub struct LocalCache {
34    /// Cache for flow state objects (longer TTL)
35    pub flow_state: Cache<String, FlowState>,
36    /// Cache for region objects
37    pub regions: Cache<String, Region>,
38    /// Cache for region identifier mappings
39    pub region_idents: Cache<String, String>,
40    /// Cache for account objects
41    pub accounts: Cache<String, AccountLite>,
42    /// Cache for account identifier mappings
43    pub account_idents: Cache<String, String>,
44    /// Cache for device objects
45    pub devices: Cache<String, DeviceStruct>,
46    /// Cache for trunk objects
47    pub trunks: Cache<String, Trunk>,
48    /// Cache for DDI objects
49    pub ddis: Cache<String, DDI>,
50    /// Cache for hook objects
51    pub hooks: Cache<String, Hook>,
52    /// Cache for asset objects
53    pub assets: Cache<String, Asset>,
54    /// Cache for trunk_ddi identifier mappings
55    pub trunk_ddi_idents: Cache<String, String>,
56    pub proxies: Cache<String, Proxy>,
57}
58
59impl CallableCache {
60    /// Creates a new CallableCache instance with local and remote caches.
61    ///
62    /// # Returns
63    /// * `CallableCache` - The new cache instance
64    pub async fn new() -> CallableCache {
65        let (remote_cache, local_cache) = create_pool().await;
66        CallableCache {
67            local_cache,
68            remote_cache,
69        }
70    }
71
72    /// Retrieves a region by its ID, first checking the local cache and then Redis.
73    ///
74    /// # Arguments
75    /// * `id` - Region ID to look up
76    ///
77    /// # Returns
78    /// * `Result<Option<Region>, RedisError>` - Region if found, None if not found, or a Redis error
79    pub async fn get_region_by_id(self, id: &str) -> Result<Option<Region>, RedisError> {
80        // Try to get the resolved ID from the local cache
81        if let Some(resolved_id) = self.local_cache.region_idents.get(id) {
82            // If we have a resolved ID, try to get the region from the local cache
83            if let Some(region) = self.local_cache.regions.get(resolved_id.as_str()) {
84                return Ok(Some(region));
85            }
86
87            // If not in local cache, fetch from Redis using the resolved ID
88            match get_region_by_id(self.remote_cache.connection.clone(), &resolved_id).await? {
89                Some(region) => {
90                    // Cache the retrieved region for future use
91                    build_region_identifiers_locally(self.local_cache, &region.clone());
92                    Ok(Some(region))
93                }
94                None => Ok(None),
95            }
96        } else {
97            // If region identifier not in local cache, try Redis
98            match get_region_by_id(self.remote_cache.connection.clone(), id).await? {
99                Some(region) => {
100                    // Cache the retrieved region for future use
101                    build_region_identifiers_locally(self.local_cache, &region.clone());
102                    Ok(Some(region))
103                }
104                None => Ok(None),
105            }
106        }
107    }
108
109    /// Retrieves an account by trunk IP and DDI, first checking the local cache and then Redis.
110    ///
111    /// This function:
112    /// 1. Checks if the trunk-DDI combination exists in the local cache
113    /// 2. If found, retrieves the account ID and uses it to look up the account
114    /// 3. If not found in local cache, falls back to Redis lookup
115    /// 4. Caches the result for future use
116    ///
117    /// # Arguments
118    /// * `ddi_id` - DDI identifier (typically a phone number)
119    /// * `trunk_ip` - IP address of the trunk
120    ///
121    /// # Returns
122    /// * `Result<Option<AccountLite>, RedisError>` - Account if found, None if not found, or a Redis error
123    pub async fn get_account_by_trunk_ddi(
124        self,
125        ddi_id: &str,
126        trunk_ip: &str,
127    ) -> Result<Option<AccountLite>, RedisError> {
128        // Create a composite key for the trunk-DDI combination
129        let trunk_ddi_key = format!("{}:{}", trunk_ip, ddi_id);
130
131        // Try to get the account ID from the local trunk-DDI cache
132        if let Some(account_id) = self.local_cache.trunk_ddi_idents.get(&trunk_ddi_key) {
133            // If we have an account ID, try to get the account from the local account cache
134            if let Some(account) = self.local_cache.accounts.get(account_id.as_str()) {
135                return Ok(Some(account.clone()));
136            }
137
138            // If account not in local cache, but we have the ID, fetch from Redis
139            match get_account_by_id(self.remote_cache.connection.clone(), account_id.as_str())
140                .await?
141            {
142                Some(account) => {
143                    // Cache the retrieved account for future use
144                    build_account_identifiers_locally(self.local_cache, &account.clone());
145                    Ok(Some(account))
146                }
147                None => Ok(None),
148            }
149        } else {
150            // If trunk-DDI combination not in local cache, try Redis
151            match get_trunk_and_ddi(self.remote_cache.connection.clone(), ddi_id, trunk_ip).await? {
152                Some(account) => {
153                    // Cache the retrieved account and trunk-DDI mapping for future use
154                    build_account_identifiers_locally(self.local_cache.clone(), &account.clone());
155
156                    // Add the trunk-DDI mapping to local cache
157                    self.local_cache
158                        .trunk_ddi_idents
159                        .insert(trunk_ddi_key, account.id.clone());
160
161                    Ok(Some(account))
162                }
163                None => Ok(None),
164            }
165        }
166    }
167
168    /// Retrieves an account by its ID, first checking the local cache and then Redis.
169    ///
170    /// # Arguments
171    /// * `id` - Account ID to look up
172    ///
173    /// # Returns
174    /// * `Result<Option<AccountLite>, RedisError>` - Account if found, None if not found, or a Redis error
175    pub async fn get_account_by_id(self, id: &str) -> Result<Option<AccountLite>, RedisError> {
176        // Try to get the resolved ID from the local cache
177        if let Some(resolved_id) = self.local_cache.account_idents.get(id) {
178            // If we have a resolved ID, try to get the account from the local cache
179            if let Some(account) = self.local_cache.accounts.get(resolved_id.as_str()) {
180                return Ok(Some(account));
181            }
182
183            // If not in local cache, fetch from Redis using the resolved ID
184            match get_account_by_id(self.remote_cache.connection.clone(), &resolved_id).await? {
185                Some(account) => {
186                    // Cache the retrieved account for future use
187                    build_account_identifiers_locally(self.local_cache, &account.clone());
188                    Ok(Some(account))
189                }
190                None => Ok(None),
191            }
192        } else {
193            // If account identifier not in local cache, try Redis
194            match get_account_by_id(self.remote_cache.connection.clone(), id).await? {
195                Some(account) => {
196                    // Cache the retrieved account for future use
197                    build_account_identifiers_locally(self.local_cache, &account.clone());
198                    Ok(Some(account))
199                }
200                None => Ok(None),
201            }
202        }
203    }
204
205    /// Retrieves all regions from Redis.
206    ///
207    /// # Returns
208    /// * `Result<Vec<Region>, RedisError>` - Vec of Regions
209    pub async fn get_regions(self) -> Result<Vec<Region>, RedisError> {
210        let regions = get_regions(self.remote_cache.connection).await?;
211
212        for region in &regions {
213            build_region_identifiers_locally(self.local_cache.clone(), region);
214        }
215
216        Ok(regions)
217    }
218
219    /// Retrieves all accounts from Redis.
220    ///
221    /// # Returns
222    /// * `Result<Vec<AccountLite>, RedisError>` - Vec of AccountLite
223    pub async fn get_accounts(self) -> Result<Vec<AccountLite>, RedisError> {
224        let accounts = get_accounts(self.remote_cache.connection).await?;
225
226        for account in &accounts {
227            build_account_identifiers_locally(self.local_cache.clone(), account);
228        }
229
230        Ok(accounts)
231    }
232
233    /// Retrieves a string value from Redis.
234    ///
235    /// # Arguments
236    /// * `key` - Redis key
237    ///
238    /// # Returns
239    /// * `Result<Option<String>, RedisError>` - String value if found, None if not found, or a Redis error
240    pub async fn get_str(self, key: &str) -> Result<Option<String>, RedisError> {
241        get_str(self.remote_cache.connection, key).await
242    }
243
244    /// Retrieves a field from a Redis hash.
245    ///
246    /// # Arguments
247    /// * `key` - Redis hash key
248    /// * `field` - Field to retrieve
249    ///
250    /// # Returns
251    /// * `Result<Option<String>, RedisError>` - Field value if found, None if not found, or a Redis error
252    pub async fn get_hash(self, key: &str, field: &str) -> Result<Option<String>, RedisError> {
253        get_hash(self.remote_cache.connection, key, field).await
254    }
255
256    /// Retrieves all devices for an account.
257    ///
258    /// # Arguments
259    /// * `account_id` - Account ID
260    ///
261    /// # Returns
262    /// * `Result<Vec<DeviceStruct>, RedisError>` - List of devices or a Redis error
263    pub async fn get_devices(self, account_id: &str) -> Result<Vec<DeviceStruct>, RedisError> {
264        // Get from Redis and update local cache
265        let devices = get_devices(self.remote_cache.connection.clone(), account_id).await?;
266
267        // Cache each device locally for future use
268        for device in &devices {
269            let cache_key = format!("{}:{}", account_id, device.id);
270            self.local_cache.devices.insert(cache_key, device.clone());
271        }
272
273        Ok(devices)
274    }
275
276    /// Retrieves a specific device for an account.
277    ///
278    /// # Arguments
279    /// * `account_id` - Account ID
280    /// * `device_id` - Device ID to retrieve
281    ///
282    /// # Returns
283    /// * `Result<Option<DeviceStruct>, RedisError>` - The device if found, None if not found, or a Redis error
284    pub async fn get_device(
285        self,
286        account_id: &str,
287        device_id: &str,
288    ) -> Result<Option<DeviceStruct>, RedisError> {
289        // Try local cache first
290        let cache_key = format!("{}:{}", account_id, device_id);
291        if let Some(device) = self.local_cache.devices.get(&cache_key) {
292            return Ok(Some(device));
293        }
294
295        // If not in local cache, try Redis
296        match get_device(self.remote_cache.connection.clone(), account_id, device_id).await? {
297            Some(device) => {
298                // Cache for future use
299                self.local_cache.devices.insert(cache_key, device.clone());
300                Ok(Some(device))
301            }
302            None => Ok(None),
303        }
304    }
305
306    /// Retrieves all trunks for an account.
307    ///
308    /// # Arguments
309    /// * `account_id` - Account ID
310    ///
311    /// # Returns
312    /// * `Result<Vec<Trunk>, RedisError>` - List of trunks or a Redis error
313    pub async fn get_trunks(self, account_id: &str) -> Result<Vec<Trunk>, RedisError> {
314        // Get from Redis and update local cache
315        let trunks = get_trunks(self.remote_cache.connection.clone(), account_id).await?;
316
317        // Cache each trunk locally for future use
318        for trunk in &trunks {
319            let cache_key = format!("{}:{}", account_id, trunk.id);
320            self.local_cache.trunks.insert(cache_key, trunk.clone());
321        }
322
323        Ok(trunks)
324    }
325
326    /// Retrieves a specific trunk for an account.
327    ///
328    /// # Arguments
329    /// * `account_id` - Account ID
330    /// * `trunk_id` - Trunk ID to retrieve
331    ///
332    /// # Returns
333    /// * `Result<Option<Trunk>, RedisError>` - The trunk if found, None if not found, or a Redis error
334    pub async fn get_trunk(
335        self,
336        account_id: &str,
337        trunk_id: &str,
338    ) -> Result<Option<Trunk>, RedisError> {
339        // Try local cache first
340        let cache_key = format!("{}:{}", account_id, trunk_id);
341        if let Some(trunk) = self.local_cache.trunks.get(&cache_key) {
342            return Ok(Some(trunk));
343        }
344
345        // If not in local cache, try Redis
346        match get_trunk(self.remote_cache.connection.clone(), account_id, trunk_id).await? {
347            Some(trunk) => {
348                // Cache for future use
349                self.local_cache.trunks.insert(cache_key, trunk.clone());
350                Ok(Some(trunk))
351            }
352            None => Ok(None),
353        }
354    }
355
356    /// Retrieves all DDIs for an account.
357    ///
358    /// # Arguments
359    /// * `account_id` - Account ID
360    ///
361    /// # Returns
362    /// * `Result<Vec<DDI>, RedisError>` - List of DDIs or a Redis error
363    pub async fn get_ddis(self, account_id: &str) -> Result<Vec<DDI>, RedisError> {
364        // Get from Redis and update local cache
365        let ddis = get_ddis(self.remote_cache.connection.clone(), account_id).await?;
366
367        // Cache each DDI locally for future use
368        for ddi in &ddis {
369            let cache_key = format!("{}:{}", account_id, ddi.id);
370            self.local_cache.ddis.insert(cache_key, ddi.clone());
371        }
372
373        Ok(ddis)
374    }
375
376    /// Retrieves a specific DDI for an account.
377    ///
378    /// # Arguments
379    /// * `account_id` - Account ID
380    /// * `ddi_id` - DDI ID to retrieve
381    ///
382    /// # Returns
383    /// * `Result<Option<DDI>, RedisError>` - The DDI if found, None if not found, or a Redis error
384    pub async fn get_ddi(self, account_id: &str, ddi_id: &str) -> Result<Option<DDI>, RedisError> {
385        // Try local cache first
386        let cache_key = format!("{}:{}", account_id, ddi_id);
387        if let Some(ddi) = self.local_cache.ddis.get(&cache_key) {
388            return Ok(Some(ddi));
389        }
390
391        // If not in local cache, try Redis
392        match get_ddi(self.remote_cache.connection.clone(), account_id, ddi_id).await? {
393            Some(ddi) => {
394                // Cache for future use
395                self.local_cache.ddis.insert(cache_key, ddi.clone());
396                Ok(Some(ddi))
397            }
398            None => Ok(None),
399        }
400    }
401
402    /// Retrieves all hooks for an account.
403    ///
404    /// # Arguments
405    /// * `account_id` - Account ID
406    ///
407    /// # Returns
408    /// * `Result<Vec<Hook>, RedisError>` - List of hooks or a Redis error
409    pub async fn get_hooks(self, account_id: &str) -> Result<Vec<Hook>, RedisError> {
410        // Get from Redis and update local cache
411        let hooks = get_hooks(self.remote_cache.connection.clone(), account_id).await?;
412
413        // Cache each hook locally for future use
414        for hook in &hooks {
415            let cache_key = format!("{}:{}", account_id, hook.id);
416            self.local_cache.hooks.insert(cache_key, hook.clone());
417        }
418
419        Ok(hooks)
420    }
421
422    /// Retrieves a specific hook for an account.
423    ///
424    /// # Arguments
425    /// * `account_id` - Account ID
426    /// * `hook_id` - Hook ID to retrieve
427    ///
428    /// # Returns
429    /// * `Result<Option<Hook>, RedisError>` - The hook if found, None if not found, or a Redis error
430    pub async fn get_hook(
431        self,
432        account_id: &str,
433        hook_id: &str,
434    ) -> Result<Option<Hook>, RedisError> {
435        // Try local cache first
436        let cache_key = format!("{}:{}", account_id, hook_id);
437        if let Some(hook) = self.local_cache.hooks.get(&cache_key) {
438            return Ok(Some(hook));
439        }
440
441        // If not in local cache, try Redis
442        match get_hook(self.remote_cache.connection.clone(), account_id, hook_id).await? {
443            Some(hook) => {
444                // Cache for future use
445                self.local_cache.hooks.insert(cache_key, hook.clone());
446                Ok(Some(hook))
447            }
448            None => Ok(None),
449        }
450    }
451
452    /// Retrieves all assets for an account.
453    ///
454    /// # Arguments
455    /// * `account_id` - Account ID
456    ///
457    /// # Returns
458    /// * `Result<Vec<Asset>, RedisError>` - List of assets or a Redis error
459    pub async fn get_assets(self, account_id: &str) -> Result<Vec<Asset>, RedisError> {
460        // Get from Redis and update local cache
461        let assets = get_assets(self.remote_cache.connection.clone(), account_id).await?;
462
463        // Cache each asset locally for future use
464        for asset in &assets {
465            let cache_key = format!("{}:{}", account_id, asset.id);
466            self.local_cache.assets.insert(cache_key, asset.clone());
467        }
468
469        Ok(assets)
470    }
471
472    /// Retrieves a specific asset for an account.
473    ///
474    /// # Arguments
475    /// * `account_id` - Account ID
476    /// * `asset_id` - Asset ID to retrieve
477    ///
478    /// # Returns
479    /// * `Result<Option<Asset>, RedisError>` - The asset if found, None if not found, or a Redis error
480    pub async fn get_asset(
481        self,
482        account_id: &str,
483        asset_id: &str,
484    ) -> Result<Option<Asset>, RedisError> {
485        // Try local cache first
486        let cache_key = format!("{}:{}", account_id, asset_id);
487        if let Some(asset) = self.local_cache.assets.get(&cache_key) {
488            return Ok(Some(asset));
489        }
490
491        // If not in local cache, try Redis
492        match get_asset(self.remote_cache.connection.clone(), account_id, asset_id).await? {
493            Some(asset) => {
494                // Cache for future use
495                self.local_cache.assets.insert(cache_key, asset.clone());
496                Ok(Some(asset))
497            }
498            None => Ok(None),
499        }
500    }
501
502    /// Retrieves a proxy by its ID, first checking the local cache and then Redis.
503    ///
504    /// # Arguments
505    /// * `id` - Proxy ID to look up
506    ///
507    /// # Returns
508    /// * `Result<Option<Proxy>, RedisError>` - Proxy if found, None if not found, or a Redis error
509    pub async fn get_proxy_by_id(&mut self, id: &str) -> Result<Option<Proxy>, RedisError> {
510        if let Some(proxy) = self.local_cache.proxies.get(id) {
511            Ok(Some(proxy))
512        } else {
513            // If proxy identifier not in local cache, try Redis
514            match get_proxy_by_id(self.remote_cache.connection.clone(), id).await? {
515                Some(proxy) => {
516                    self.local_cache
517                        .proxies
518                        .insert(id.to_string(), proxy.clone());
519                    Ok(Some(proxy))
520                }
521                None => Ok(None),
522            }
523        }
524    }
525    
526    pub async fn get_proxies(&mut self) -> Result<Vec<Proxy>, RedisError> {
527        let proxies = get_proxies(self.remote_cache.connection.clone()).await?;
528        self.local_cache.proxies.invalidate_all();
529        for proxy in &proxies {
530            self.local_cache.proxies.insert(proxy.id.clone(), proxy.clone());
531        }
532        Ok(proxies)
533    }
534}
535
536/// Gets the Redis host from the environment variable.
537///
538/// # Returns
539/// * `String` - Redis host URL
540///
541/// # Panics
542/// Panics if the environment variable is not set
543fn get_redis_host() -> String {
544    let name = "CAL_VALKEY_HOST";
545    env::var(name).unwrap_or_else(|_| panic!("${} is not set", name))
546}
547
548/// Creates Redis and local cache instances and sets up the event subscription.
549///
550/// # Returns
551/// * `(RedisCache, LocalCache)` - The created cache instances
552///
553/// # Panics
554/// Panics if Redis connection fails or if subscription fails
555pub async fn create_pool() -> (RedisCache, LocalCache) {
556    // Set up Redis connection
557    let client = redis::Client::open(get_redis_host()).expect("Failed to connect to Redis");
558
559    let (tx, mut rx) = unbounded_channel();
560    let config = redis::AsyncConnectionConfig::new().set_push_sender(tx);
561    let mut con = client
562        .get_multiplexed_async_connection_with_config(&config)
563        .await
564        .expect("Failed to get Redis connection");
565
566    con.subscribe(&["cal:events"])
567        .await
568        .expect("Failed to subscribe to events");
569
570    // Create cache configuration function to avoid repetition
571    fn create_cache<T: Clone + Send + Sync + 'static>(
572        capacity_mb: u64,
573        ttl_secs: u64,
574    ) -> Cache<String, T> {
575        Cache::builder()
576            .max_capacity(capacity_mb * 1024 * 1024)
577            .time_to_live(Duration::from_secs(ttl_secs))
578            .build()
579    }
580
581    // Initialize all caches with appropriate sizes
582    let local_cache = LocalCache {
583        flow_state: create_cache::<FlowState>(50, 60 * 60 * 4),
584        regions: create_cache::<Region>(5, 60),
585        region_idents: create_cache::<String>(5, 60),
586        accounts: create_cache::<AccountLite>(50, 60),
587        account_idents: create_cache::<String>(5, 60),
588        devices: create_cache::<DeviceStruct>(20, 60),
589        trunks: create_cache::<Trunk>(10, 60),
590        ddis: create_cache::<DDI>(10, 60),
591        hooks: create_cache::<Hook>(10, 60),
592        assets: create_cache::<Asset>(10, 60),
593        trunk_ddi_idents: create_cache::<String>(10, 60),
594        proxies: create_cache::<Proxy>(10, 60 * 60),
595    };
596
597    let local_cache_cloned = local_cache.clone();
598
599    // Spawn event listener task
600    tokio::spawn(async move {
601        while let Some(push_info) = rx.recv().await {
602            if push_info.kind != PushKind::Message {
603                continue;
604            }
605
606            if let Some(Value::BulkString(bytes)) = push_info.data.get(1) {
607                if let Ok(event_str) = String::from_utf8(bytes.clone()) {
608                    if let Ok(event) = serde_json::from_str::<RedisEvent>(&event_str) {
609                        handle_redis_event(event, &local_cache_cloned);
610                    }
611                }
612            }
613        }
614    });
615
616    (RedisCache { connection: con }, local_cache)
617}
618
619/// Handles Redis pubsub events by updating the local cache appropriately.
620///
621/// # Arguments
622/// * `event` - The Redis event to handle
623/// * `cache` - The local cache to update
624fn handle_redis_event(event: RedisEvent, cache: &LocalCache) {
625    match event {
626        RedisEvent::AccountUpdate(update) => {
627            build_account_identifiers_locally(cache.clone(), &update.payload.into());
628        }
629        RedisEvent::AccountDelete(delete) => {
630            cache.accounts.invalidate(&delete.id);
631        }
632        RedisEvent::AccountCreate(create) => {
633            build_account_identifiers_locally(cache.clone(), &create.payload.into());
634        }
635        RedisEvent::AccountSync => {
636            cache.accounts.invalidate_all();
637            cache.account_idents.invalidate_all();
638        }
639        RedisEvent::RegionUpdate(update) => {
640            cache
641                .regions
642                .insert(update.payload.id.to_string(), update.payload);
643        }
644        RedisEvent::RegionDelete(delete) => {
645            cache.regions.invalidate(&delete.id);
646        }
647        RedisEvent::RegionCreate(create) => {
648            build_region_identifiers_locally(cache.clone(), &create.payload);
649        }
650        RedisEvent::RegionSync => {
651            cache.regions.invalidate_all();
652            cache.region_idents.invalidate_all();
653        }
654    }
655}
656
657/// Retrieves a string value directly from Redis.
658///
659/// # Arguments
660/// * `con` - Redis connection
661/// * `key` - Redis key
662///
663/// # Returns
664/// * `Result<Option<String>, RedisError>` - String value if found, None if not found, or a Redis error
665pub async fn get_str(
666    mut con: MultiplexedConnection,
667    key: &str,
668) -> Result<Option<String>, RedisError> {
669    con.get(key).await
670}
671
672/// Retrieves a field from a Redis hash directly.
673///
674/// # Arguments
675/// * `con` - Redis connection
676/// * `key` - Redis hash key
677/// * `field` - Field to retrieve
678///
679/// # Returns
680/// * `Result<Option<String>, RedisError>` - Field value if found, None if not found, or a Redis error
681pub async fn get_hash(
682    mut con: MultiplexedConnection,
683    key: &str,
684    field: &str,
685) -> Result<Option<String>, RedisError> {
686    con.hget(key, field).await
687}
688
689/// Retrieves all values from a Redis hash.
690///
691/// # Arguments
692/// * `con` - Redis connection
693/// * `key` - Redis hash key
694///
695/// # Returns
696/// * `Result<HashMap<String, String>, RedisError>` - Map of field names to values, or a Redis error
697pub async fn get_hash_all_values(
698    mut con: MultiplexedConnection,
699    key: &str,
700) -> Result<HashMap<String, String>, RedisError> {
701    con.hvals(key).await
702}