Skip to content

Commit 622ad7b

Browse files
committed
feat: add memory cacher to idempotent-proxy-server
1 parent 0aae62a commit 622ad7b

File tree

9 files changed

+327
-44
lines changed

9 files changed

+327
-44
lines changed

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
SERVER_ADDR=127.0.0.1:8080
2-
REDIS_URL=127.0.0.1:6379
2+
# if not set, use in-memory cache
3+
# REDIS_URL=127.0.0.1:6379
34
POLL_INTERVAL=100 # in milliseconds
45
REQUEST_TIMEOUT=10000 # in milliseconds
56
LOG_LEVEL=info # debug, info, warn, error

Cargo.lock

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

src/idempotent-proxy-server/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ name = "idempotent-proxy-server"
1717
axum = { workspace = true }
1818
axum-server = { workspace = true }
1919
tokio = { workspace = true }
20+
futures = { workspace = true }
2021
reqwest = { workspace = true }
2122
dotenvy = { workspace = true }
2223
log = { workspace = true }
@@ -26,9 +27,17 @@ rustis = { workspace = true }
2627
bb8 = { workspace = true }
2728
async-trait = { workspace = true }
2829
serde = { workspace = true }
30+
serde_bytes = { workspace = true }
31+
serde_json = { workspace = true }
2932
ciborium = { workspace = true }
3033
anyhow = { workspace = true }
3134
k256 = { workspace = true }
3235
ed25519-dalek = { workspace = true }
3336
base64 = { workspace = true }
3437
idempotent-proxy-types = { path = "../idempotent-proxy-types", version = "1" }
38+
39+
[dev-dependencies]
40+
rand_core = "0.6"
41+
hex = { package = "hex-conservative", version = "0.2", default-features = false, features = [
42+
"alloc",
43+
] }
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use async_trait::async_trait;
2+
use std::{
3+
collections::{
4+
hash_map::{Entry, HashMap},
5+
BTreeSet,
6+
},
7+
sync::Arc,
8+
};
9+
use structured_logger::unix_ms;
10+
use tokio::{
11+
sync::RwLock,
12+
time::{sleep, Duration},
13+
};
14+
15+
use super::Cacher;
16+
17+
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
18+
struct PriorityKey(u64, String);
19+
20+
#[derive(Clone, Default)]
21+
pub struct MemoryCacher {
22+
priority_queue: Arc<RwLock<BTreeSet<PriorityKey>>>,
23+
kv: Arc<RwLock<HashMap<String, (u64, Vec<u8>)>>>,
24+
}
25+
26+
impl MemoryCacher {
27+
fn clean_expired_values(&self) -> tokio::task::JoinHandle<()> {
28+
let kv = self.kv.clone();
29+
let priority_queue = self.priority_queue.clone();
30+
tokio::spawn(async move {
31+
let now = unix_ms();
32+
let mut pq = priority_queue.write().await;
33+
let mut kv = kv.write().await;
34+
while let Some(PriorityKey(expire_at, key)) = pq.pop_first() {
35+
if expire_at > now {
36+
pq.insert(PriorityKey(expire_at, key));
37+
break;
38+
}
39+
40+
kv.remove(&key);
41+
}
42+
()
43+
})
44+
}
45+
}
46+
47+
#[async_trait]
48+
impl Cacher for MemoryCacher {
49+
async fn obtain(&self, key: &str, ttl: u64) -> Result<bool, String> {
50+
let mut kv = self.kv.write().await;
51+
let now = unix_ms();
52+
match kv.entry(key.to_string()) {
53+
Entry::Occupied(mut entry) => {
54+
let (expire_at, value) = entry.get_mut();
55+
if *expire_at > now {
56+
return Ok(false);
57+
}
58+
59+
let mut pq = self.priority_queue.write().await;
60+
pq.remove(&PriorityKey(*expire_at, key.to_string()));
61+
62+
*expire_at = now + ttl;
63+
*value = vec![0];
64+
pq.insert(PriorityKey(*expire_at, key.to_string()));
65+
Ok(true)
66+
}
67+
Entry::Vacant(entry) => {
68+
let expire_at = now + ttl;
69+
entry.insert((expire_at, vec![0]));
70+
self.priority_queue
71+
.write()
72+
.await
73+
.insert(PriorityKey(expire_at, key.to_string()));
74+
Ok(true)
75+
}
76+
}
77+
}
78+
79+
async fn polling_get(
80+
&self,
81+
key: &str,
82+
poll_interval: u64,
83+
mut counter: u64,
84+
) -> Result<Vec<u8>, String> {
85+
while counter > 0 {
86+
let kv = self.kv.read().await;
87+
let res = kv.get(key);
88+
match res {
89+
None => return Err("not obtained".to_string()),
90+
Some((expire_at, value)) => {
91+
if *expire_at <= unix_ms() {
92+
drop(kv);
93+
self.kv.write().await.remove(key);
94+
self.clean_expired_values();
95+
return Err("value expired".to_string());
96+
}
97+
98+
if value.len() > 1 {
99+
return Ok(value.clone());
100+
}
101+
}
102+
}
103+
104+
counter -= 1;
105+
sleep(Duration::from_millis(poll_interval)).await;
106+
}
107+
108+
Err(("polling get cache timeout").to_string())
109+
}
110+
111+
async fn set(&self, key: &str, val: Vec<u8>, ttl: u64) -> Result<bool, String> {
112+
let mut kv = self.kv.write().await;
113+
match kv.get_mut(key) {
114+
Some((expire_at, value)) => {
115+
let now = unix_ms();
116+
if *expire_at <= now {
117+
kv.remove(key);
118+
self.clean_expired_values();
119+
return Err("value expired".to_string());
120+
}
121+
122+
let mut pq = self.priority_queue.write().await;
123+
pq.remove(&PriorityKey(*expire_at, key.to_string()));
124+
125+
*expire_at = now + ttl;
126+
*value = val;
127+
pq.insert(PriorityKey(*expire_at, key.to_string()));
128+
Ok(true)
129+
}
130+
None => Err("not obtained".to_string()),
131+
}
132+
}
133+
134+
async fn del(&self, key: &str) -> Result<(), String> {
135+
let mut kv = self.kv.write().await;
136+
if let Some(val) = kv.remove(key) {
137+
let mut pq = self.priority_queue.write().await;
138+
pq.remove(&PriorityKey(val.0, key.to_string()));
139+
}
140+
self.clean_expired_values();
141+
Ok(())
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod test {
147+
use super::*;
148+
149+
#[tokio::test]
150+
async fn memory_cacher() {
151+
let mc = MemoryCacher::default();
152+
153+
assert!(mc.obtain("key1", 100).await.unwrap());
154+
assert!(!mc.obtain("key1", 100).await.unwrap());
155+
assert!(mc.polling_get("key1", 10, 2).await.is_err());
156+
assert!(mc.set("key", vec![1, 2, 3, 4], 100).await.is_err());
157+
assert!(mc.set("key1", vec![1, 2, 3, 4], 100).await.is_ok());
158+
assert!(!mc.obtain("key1", 100).await.unwrap());
159+
assert_eq!(
160+
mc.polling_get("key1", 10, 2).await.unwrap(),
161+
vec![1, 2, 3, 4]
162+
);
163+
assert_eq!(
164+
mc.polling_get("key1", 10, 2).await.unwrap(),
165+
vec![1, 2, 3, 4]
166+
);
167+
168+
assert!(mc.del("key").await.is_ok());
169+
assert!(mc.del("key1").await.is_ok());
170+
assert!(mc.polling_get("key1", 10, 2).await.is_err());
171+
assert!(mc.set("key1", vec![1, 2, 3, 4], 100).await.is_err());
172+
assert!(mc.obtain("key1", 100).await.unwrap());
173+
assert!(mc.set("key1", vec![1, 2, 3, 4], 100).await.is_ok());
174+
assert_eq!(
175+
mc.polling_get("key1", 10, 2).await.unwrap(),
176+
vec![1, 2, 3, 4]
177+
);
178+
179+
sleep(Duration::from_millis(200)).await;
180+
assert!(mc.polling_get("key1", 10, 2).await.is_err());
181+
assert!(mc.set("key1", vec![1, 2, 3, 4], 100).await.is_err());
182+
assert!(mc.del("key1").await.is_ok());
183+
184+
assert!(mc.obtain("key1", 100).await.unwrap());
185+
sleep(Duration::from_millis(200)).await;
186+
let _ = mc.clean_expired_values().await;
187+
println!("{:?}", mc.priority_queue.read().await);
188+
189+
let res = futures::try_join!(
190+
mc.obtain("key1", 100),
191+
mc.obtain("key1", 100),
192+
mc.obtain("key1", 100),
193+
)
194+
.unwrap();
195+
match res {
196+
(true, false, false) | (false, true, false) | (false, false, true) => {}
197+
_ => panic!("unexpected result"),
198+
}
199+
200+
assert_eq!(mc.kv.read().await.len(), 1);
201+
assert_eq!(mc.priority_queue.read().await.len(), 1);
202+
203+
sleep(Duration::from_millis(200)).await;
204+
assert_eq!(mc.kv.read().await.len(), 1);
205+
assert_eq!(mc.priority_queue.read().await.len(), 1);
206+
let _ = mc.clean_expired_values().await;
207+
208+
assert!(mc.kv.read().await.is_empty());
209+
assert!(mc.priority_queue.read().await.is_empty());
210+
}
211+
}

src/idempotent-proxy-types/src/cache.rs renamed to src/idempotent-proxy-server/src/cache/mod.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,36 @@ use http::{
88
header::{HeaderMap, HeaderName, HeaderValue},
99
StatusCode,
1010
};
11+
use idempotent_proxy_types::err_string;
1112
use serde::{Deserialize, Serialize};
1213
use serde_bytes::ByteBuf;
1314

14-
use crate::err_string;
15+
mod memory;
16+
mod redis;
17+
18+
pub use memory::*;
19+
pub use redis::*;
20+
21+
pub struct HybridCacher {
22+
pub poll_interval: u64,
23+
pub cache_ttl: u64,
24+
cache: CacherEntry,
25+
}
26+
27+
impl HybridCacher {
28+
pub fn new(poll_interval: u64, cache_ttl: u64, cache: CacherEntry) -> Self {
29+
Self {
30+
poll_interval,
31+
cache_ttl,
32+
cache,
33+
}
34+
}
35+
}
36+
37+
pub enum CacherEntry {
38+
Memory(MemoryCacher),
39+
Redis(RedisClient),
40+
}
1541

1642
#[async_trait]
1743
pub trait Cacher {
@@ -26,6 +52,42 @@ pub trait Cacher {
2652
async fn del(&self, key: &str) -> Result<(), String>;
2753
}
2854

55+
#[async_trait]
56+
impl Cacher for HybridCacher {
57+
async fn obtain(&self, key: &str, ttl: u64) -> Result<bool, String> {
58+
match &self.cache {
59+
CacherEntry::Memory(cacher) => cacher.obtain(key, ttl).await,
60+
CacherEntry::Redis(cacher) => cacher.obtain(key, ttl).await,
61+
}
62+
}
63+
64+
async fn polling_get(
65+
&self,
66+
key: &str,
67+
poll_interval: u64,
68+
counter: u64,
69+
) -> Result<Vec<u8>, String> {
70+
match &self.cache {
71+
CacherEntry::Memory(cacher) => cacher.polling_get(key, poll_interval, counter).await,
72+
CacherEntry::Redis(cacher) => cacher.polling_get(key, poll_interval, counter).await,
73+
}
74+
}
75+
76+
async fn set(&self, key: &str, val: Vec<u8>, ttl: u64) -> Result<bool, String> {
77+
match &self.cache {
78+
CacherEntry::Memory(cacher) => cacher.set(key, val, ttl).await,
79+
CacherEntry::Redis(cacher) => cacher.set(key, val, ttl).await,
80+
}
81+
}
82+
83+
async fn del(&self, key: &str) -> Result<(), String> {
84+
match &self.cache {
85+
CacherEntry::Memory(cacher) => cacher.del(key).await,
86+
CacherEntry::Redis(cacher) => cacher.del(key).await,
87+
}
88+
}
89+
}
90+
2991
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
3092
pub struct ResponseData {
3193
pub status: u16,

0 commit comments

Comments
 (0)