Skip to content

Commit 10ef954

Browse files
committed
feat: cache port
1 parent 7b238c0 commit 10ef954

File tree

13 files changed

+328
-9
lines changed

13 files changed

+328
-9
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/adapter/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ version = "1.0.80"
4949

5050
[dependencies.tonic]
5151
version = "0.11.0"
52+
53+
[dependencies.redis-async]
54+
version = "0.17.1"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use std::{
2+
collections::HashMap,
3+
sync::Arc,
4+
time::{Duration, SystemTime},
5+
};
6+
7+
use async_trait::async_trait;
8+
use tokio::sync::RwLock;
9+
10+
use rust_core::common::errors::CoreError;
11+
use rust_core::ports::cache::CachePort;
12+
13+
/// Represents an in-memory cache implementation.
14+
pub struct InMemoryCache {
15+
cache: Arc<RwLock<HashMap<String, (String, SystemTime, Option<Duration>)>>>,
16+
}
17+
18+
impl InMemoryCache {
19+
/// Creates a new empty in-memory cache.
20+
pub fn new() -> Self {
21+
Self {
22+
cache: Arc::new(RwLock::new(HashMap::new())),
23+
}
24+
}
25+
26+
/// Removes expired entries from the cache asynchronously.
27+
///
28+
/// This method is used to remove any expired cache entries based on their expiration time.
29+
///
30+
/// # Returns
31+
///
32+
/// Returns `Ok(())` if the cleanup is successful, otherwise returns a `CoreError`.
33+
async fn cleanup_expired_entries(&self) -> Result<(), CoreError> {
34+
let mut cache = self.cache.write().await;
35+
let now = SystemTime::now();
36+
cache.retain(|_, (_, expiry_time, _)| *expiry_time > now);
37+
Ok(())
38+
}
39+
}
40+
41+
#[async_trait]
42+
impl CachePort for InMemoryCache {
43+
/// Retrieves a value from the cache based on the provided key.
44+
///
45+
/// # Arguments
46+
///
47+
/// * `key`: A string representing the key to retrieve from the cache.
48+
///
49+
/// # Returns
50+
///
51+
/// Returns a `Result` containing `Some(value)` if the key is found and not expired,
52+
/// or `None` if the key is not found or expired. Returns a `CoreError` on failure.
53+
async fn get(&self, key: &str) -> Result<Option<String>, CoreError> {
54+
let cache = self.cache.read().await;
55+
Ok(cache
56+
.get(key)
57+
.filter(|(_, expiry_time, _)| *expiry_time > SystemTime::now())
58+
.map(|(value, _, _)| value.clone()))
59+
}
60+
61+
/// Sets a key-value pair in the cache with an optional expiration duration.
62+
///
63+
/// # Arguments
64+
///
65+
/// * `key`: A string representing the key to set in the cache.
66+
/// * `value`: A string representing the value to associate with the key.
67+
/// * `expiration`: An optional `Duration` specifying the expiration time for the key-value pair.
68+
///
69+
/// # Returns
70+
///
71+
/// Returns `Ok(())` if the set operation is successful, otherwise returns a `CoreError`.
72+
async fn set(
73+
&mut self,
74+
key: String,
75+
value: String,
76+
expiration: Option<Duration>,
77+
) -> Result<bool, CoreError> {
78+
self.cleanup_expired_entries().await?;
79+
let expiry_time = expiration.map(|exp| SystemTime::now() + exp);
80+
let mut cache = self.cache.write().await;
81+
cache.insert(
82+
key,
83+
(
84+
value,
85+
expiry_time.unwrap_or_else(|| SystemTime::UNIX_EPOCH),
86+
expiration,
87+
),
88+
);
89+
Ok(true)
90+
}
91+
92+
/// Removes a key-value pair from the cache based on the provided key.
93+
///
94+
/// # Arguments
95+
///
96+
/// * `key`: A string representing the key to remove from the cache.
97+
///
98+
/// # Returns
99+
///
100+
/// Returns `Ok(())` if the key is found and removed, otherwise returns a `CoreError`.
101+
async fn remove(&mut self, key: &str) -> Result<bool, CoreError> {
102+
let mut cache = self.cache.write().await;
103+
if cache.remove(key).is_some() {
104+
Ok(true)
105+
} else {
106+
Err(CoreError::NotFound)
107+
}
108+
}
109+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod cache;
12
pub mod question;

src/adapter/src/repositories/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod grpc;
22
pub mod in_memory;
33
pub mod postgres;
4+
pub mod redis;
45
pub mod repository_test;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use async_trait::async_trait;
2+
use redis_async::{client, resp::RespValue, resp_array};
3+
4+
use rust_core::{common::errors::CoreError, ports::cache::CachePort};
5+
6+
use std::time::Duration;
7+
8+
/// Represents a Redis cache implementation.
9+
pub struct RedisCache {
10+
client: client::PairedConnection,
11+
}
12+
13+
impl RedisCache {
14+
/// Creates a new Redis cache instance.
15+
///
16+
/// # Arguments
17+
///
18+
/// * `redis_host`: Hostname or IP address of the Redis server.
19+
/// * `redis_port`: Port number of the Redis server.
20+
///
21+
/// # Returns
22+
///
23+
/// Returns a `Result` containing the initialized `RedisCache` instance.
24+
pub async fn new(redis_host: &str, redis_port: u16) -> Result<Self, CoreError> {
25+
let client = client::paired_connect(redis_host, redis_port)
26+
.await
27+
.map_err(|err| CoreError::InternalError(err.into()))?;
28+
Ok(Self { client })
29+
}
30+
}
31+
32+
#[async_trait]
33+
impl CachePort for RedisCache {
34+
/// Retrieves a value from the Redis cache.
35+
///
36+
/// # Arguments
37+
///
38+
/// * `key`: The key of the value to retrieve.
39+
///
40+
/// # Returns
41+
///
42+
/// Returns a `Result` containing the value associated with the key if found, or `None`.
43+
async fn get(&self, key: &str) -> Result<Option<String>, CoreError> {
44+
let result: Result<RespValue, _> = self.client.send(resp_array!["GET", key]).await;
45+
match result {
46+
Ok(RespValue::BulkString(data)) => Ok(Some(String::from_utf8_lossy(&data).to_string())),
47+
_ => Ok(None),
48+
}
49+
}
50+
51+
/// Sets a key-value pair in the Redis cache with an optional expiration time.
52+
///
53+
/// # Arguments
54+
///
55+
/// * `key`: The key to set.
56+
/// * `value`: The value to associate with the key.
57+
/// * `expiration`: Optional expiration duration for the key-value pair.
58+
///
59+
/// # Returns
60+
///
61+
/// Returns a `Result` indicating success (`true`) or failure (`false`).
62+
async fn set(
63+
&mut self,
64+
key: String,
65+
value: String,
66+
expiration: Option<Duration>,
67+
) -> Result<bool, CoreError> {
68+
let mut args = vec!["SET", &key, &value];
69+
70+
let exp_secs = if let Some(exp) = expiration {
71+
args.push("EX");
72+
Some(exp.as_secs().to_string())
73+
} else {
74+
None
75+
};
76+
77+
if let Some(ref secs) = exp_secs {
78+
args.push(secs);
79+
}
80+
81+
let result: Result<RespValue, _> = self.client.send(resp_array![].append(args)).await;
82+
match result {
83+
Ok(RespValue::SimpleString(s)) if s == "OK" => Ok(true),
84+
_ => Ok(false),
85+
}
86+
}
87+
88+
/// Removes a key-value pair from the Redis cache.
89+
///
90+
/// # Arguments
91+
///
92+
/// * `key`: The key to remove.
93+
///
94+
/// # Returns
95+
///
96+
/// Returns a `Result` indicating success (`true`) or failure (`false`).
97+
async fn remove(&mut self, key: &str) -> Result<bool, CoreError> {
98+
let result: Result<RespValue, _> = self.client.send(resp_array!["DEL", key]).await;
99+
match result {
100+
Ok(RespValue::Integer(1)) => Ok(true),
101+
_ => Ok(false),
102+
}
103+
}
104+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod cache;

src/core/src/ports/cache.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use async_trait::async_trait;
2+
3+
use std::time::Duration;
4+
5+
use crate::common::errors::CoreError;
6+
7+
/// Represents a cache port for storing key-value pairs.
8+
#[async_trait]
9+
pub trait CachePort {
10+
/// Retrieves a value from the cache based on the given key.
11+
///
12+
/// # Arguments
13+
///
14+
/// * `key`: The key to look up in the cache.
15+
///
16+
/// # Returns
17+
///
18+
/// Returns an optional value associated with the key, or `None` if the key is not present in the cache.
19+
async fn get(&self, key: &str) -> Result<Option<String>, CoreError>;
20+
21+
/// Sets a key-value pair in the cache with an optional expiration duration.
22+
///
23+
/// # Arguments
24+
///
25+
/// * `key`: The key to set in the cache.
26+
/// * `value`: The value to associate with the key.
27+
/// * `expiration`: Optional expiration duration for the key-value pair.
28+
///
29+
/// # Returns
30+
///
31+
/// Returns `true` if the key-value pair is successfully set in the cache, `false` otherwise.
32+
async fn set(
33+
&mut self,
34+
key: String,
35+
value: String,
36+
expiration: Option<Duration>,
37+
) -> Result<bool, CoreError>;
38+
39+
/// Removes a key-value pair from the cache based on the given key.
40+
///
41+
/// # Arguments
42+
///
43+
/// * `key`: The key to remove from the cache.
44+
///
45+
/// # Returns
46+
///
47+
/// Returns `true` if the key-value pair is successfully removed from the cache, `false` otherwise.
48+
async fn remove(&mut self, key: &str) -> Result<bool, CoreError>;
49+
}

src/core/src/ports/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod cache;
12
pub mod gpt_answer;
23
pub mod question;

src/gpt_answer_server/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ path = "../core"
1818
[dependencies.common]
1919
path = "../common"
2020

21+
[dependencies.adapter]
22+
path = "../adapter"
23+
2124
[dependencies.tonic]
2225
version = "0.11.0"
2326

0 commit comments

Comments
 (0)