Skip to content

Commit 100fa6e

Browse files
committed
wip: Rework the cache layer
1 parent 0fe0b55 commit 100fa6e

File tree

6 files changed

+689
-11
lines changed

6 files changed

+689
-11
lines changed

dht-cache/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ openssl-sys = "*"
3030
libsqlite3-sys = "*"
3131
thiserror = "1.0.43"
3232
anyhow = "1.0.72"
33+
libp2p-swarm-test = "0.2.0"
34+
tokio-stream = "0.1.14"
35+
36+
[dev-dependencies]
37+
env_logger = "0.10.0"
3338

3439

3540
[package.metadata.cargo-udeps.ignore]

dht-cache/src/cache.rs

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
//! Cached access to the DHT
2+
3+
mod local;
4+
5+
use std::sync::Arc;
6+
use std::{collections::BTreeMap, time::Duration};
7+
8+
use futures_util::{Stream, StreamExt};
9+
use libp2p::Swarm;
10+
use serde_json::Value;
11+
use tokio::sync::mpsc::UnboundedSender;
12+
use tokio::sync::RwLock;
13+
use tokio::time;
14+
use tokio_stream::wrappers::UnboundedReceiverStream;
15+
16+
use crate::{
17+
cache::local::DomoCacheStateMessage,
18+
data::DomoEvent,
19+
dht::{dht_channel, Command, Event},
20+
domolibp2p::DomoBehaviour,
21+
utils, Error,
22+
};
23+
24+
use self::local::{DomoCacheElement, LocalCache, Query};
25+
26+
/// Cached DHT
27+
///
28+
/// It keeps a local cache of the dht state and allow to query the persistent topics
29+
pub struct Cache {
30+
peer_id: String,
31+
local: LocalCache,
32+
cmd: UnboundedSender<Command>,
33+
}
34+
35+
impl Cache {
36+
/// Send a volatile message
37+
///
38+
/// Volatile messages are unstructured and do not persist in the DHT.
39+
pub fn send(&self, value: &Value) -> Result<(), Error> {
40+
self.cmd
41+
.send(Command::Broadcast(value.to_owned()))
42+
.map_err(|_| Error::Channel)?;
43+
44+
Ok(())
45+
}
46+
47+
/// Persist a value within the DHT
48+
///
49+
/// It is identified by the topic and uuid value
50+
pub async fn put(&self, topic: &str, uuid: &str, value: &Value) -> Result<(), Error> {
51+
let elem = DomoCacheElement {
52+
topic_name: topic.to_string(),
53+
topic_uuid: uuid.to_string(),
54+
value: value.to_owned(),
55+
publication_timestamp: utils::get_epoch_ms(),
56+
publisher_peer_id: self.peer_id.clone(),
57+
..Default::default()
58+
};
59+
60+
self.local.put(&elem).await;
61+
62+
self.cmd
63+
.send(Command::Publish(serde_json::to_value(&elem)?))
64+
.map_err(|_| Error::Channel)?;
65+
66+
Ok(())
67+
}
68+
69+
/// Delete a value within the DHT
70+
///
71+
/// It inserts the deletion entry and the entry value will be marked as deleted and removed
72+
/// from the stored cache.
73+
pub async fn del(&self, topic: &str, uuid: &str) -> Result<(), Error> {
74+
let elem = DomoCacheElement {
75+
topic_name: topic.to_string(),
76+
topic_uuid: uuid.to_string(),
77+
publication_timestamp: utils::get_epoch_ms(),
78+
publisher_peer_id: self.peer_id.clone(),
79+
deleted: true,
80+
..Default::default()
81+
};
82+
83+
self.local.put(&elem).await;
84+
85+
self.cmd
86+
.send(Command::Publish(serde_json::to_value(&elem)?))
87+
.map_err(|_| Error::Channel)?;
88+
89+
Ok(())
90+
}
91+
92+
/// Query the local cache
93+
pub fn query(&self, topic: &str) -> Query {
94+
self.local.query(topic)
95+
}
96+
}
97+
98+
#[derive(Default, Debug, Clone)]
99+
pub(crate) struct PeersState {
100+
list: BTreeMap<String, DomoCacheStateMessage>,
101+
last_repub_timestamp: u128,
102+
repub_interval: u128,
103+
}
104+
105+
#[derive(Debug)]
106+
enum CacheState {
107+
Synced,
108+
Desynced { is_leader: bool },
109+
}
110+
111+
impl PeersState {
112+
fn with_interval(repub_interval: u128) -> Self {
113+
Self {
114+
repub_interval,
115+
..Default::default()
116+
}
117+
}
118+
119+
fn insert(&mut self, state: DomoCacheStateMessage) {
120+
self.list.insert(state.peer_id.to_string(), state);
121+
}
122+
123+
async fn is_synchronized(&self, peer_id: &str, hash: u64) -> CacheState {
124+
let cur_ts = utils::get_epoch_ms() - self.repub_interval;
125+
let desync = self
126+
.list
127+
.values()
128+
.find(|data| data.cache_hash != hash && data.publication_timestamp > cur_ts)
129+
.is_some();
130+
131+
if desync {
132+
CacheState::Desynced {
133+
is_leader: self
134+
.list
135+
.values()
136+
.find(|data| {
137+
data.cache_hash == hash
138+
&& data.peer_id.as_str() < peer_id
139+
&& data.publication_timestamp > cur_ts
140+
})
141+
.is_none(),
142+
}
143+
} else {
144+
CacheState::Synced
145+
}
146+
}
147+
}
148+
149+
/// Join the dht and keep a local cache up to date
150+
///
151+
/// the resend interval is expressed in milliseconds
152+
pub fn cache_channel(
153+
local: LocalCache,
154+
swarm: Swarm<DomoBehaviour>,
155+
resend_interval: u64,
156+
) -> (Cache, impl Stream<Item = DomoEvent>) {
157+
let local_peer_id = swarm.local_peer_id().to_string();
158+
159+
let (cmd, r, _j) = dht_channel(swarm);
160+
161+
let cache = Cache {
162+
local: local.clone(),
163+
cmd: cmd.clone(),
164+
peer_id: local_peer_id.clone(),
165+
};
166+
167+
let stream = UnboundedReceiverStream::new(r);
168+
169+
let peers_state = Arc::new(RwLock::new(PeersState::with_interval(
170+
resend_interval as u128,
171+
)));
172+
173+
let local_read = local.clone();
174+
let cmd_update = cmd.clone();
175+
let peer_id = local_peer_id.clone();
176+
177+
tokio::task::spawn(async move {
178+
let mut interval = time::interval(Duration::from_millis(resend_interval.max(100)));
179+
while !cmd_update.is_closed() {
180+
interval.tick().await;
181+
let hash = local_read.get_hash().await;
182+
let m = DomoCacheStateMessage {
183+
peer_id: peer_id.clone(),
184+
cache_hash: hash,
185+
publication_timestamp: utils::get_epoch_ms(),
186+
};
187+
188+
if cmd_update
189+
.send(Command::Config(serde_json::to_value(&m).unwrap()))
190+
.is_err()
191+
{
192+
break;
193+
}
194+
}
195+
});
196+
197+
// TODO: refactor once async closures are stable
198+
let events = stream.filter_map(move |ev| {
199+
let local_write = local.clone();
200+
let peers_state = peers_state.clone();
201+
let peer_id = local_peer_id.clone();
202+
let cmd = cmd.clone();
203+
async move {
204+
match ev {
205+
Event::Config(cfg) => {
206+
let m: DomoCacheStateMessage = serde_json::from_str(&cfg).unwrap();
207+
208+
let hash = local_write.get_hash().await;
209+
210+
// SAFETY: only user
211+
let mut peers_state = peers_state.write().await;
212+
213+
// update the peers_caches_state
214+
peers_state.insert(m);
215+
216+
let sync_info = peers_state.is_synchronized(&peer_id, hash).await;
217+
218+
log::debug!("local {peer_id:?} {sync_info:?} -> {peers_state:#?}");
219+
220+
if let CacheState::Desynced { is_leader } = sync_info {
221+
if is_leader
222+
&& utils::get_epoch_ms() - peers_state.last_repub_timestamp
223+
>= peers_state.repub_interval
224+
{
225+
local_write
226+
.read_owned()
227+
.await
228+
.values()
229+
.flat_map(|topic| topic.values())
230+
.for_each(|elem| {
231+
let mut elem = elem.to_owned();
232+
log::debug!("resending {}", elem.topic_uuid);
233+
elem.republication_timestamp = utils::get_epoch_ms();
234+
cmd.send(Command::Publish(
235+
serde_json::to_value(&elem).unwrap(),
236+
))
237+
.unwrap();
238+
});
239+
peers_state.last_repub_timestamp = utils::get_epoch_ms();
240+
}
241+
}
242+
243+
// check for desync
244+
// republish the local cache if needed
245+
None
246+
}
247+
Event::Discovered(who) => Some(DomoEvent::NewPeers(
248+
who.into_iter().map(|w| w.to_string()).collect(),
249+
)),
250+
Event::VolatileData(data) => {
251+
// TODO we swallow errors quietly here
252+
serde_json::from_str(&data)
253+
.ok()
254+
.map(DomoEvent::VolatileData)
255+
}
256+
Event::PersistentData(data) => {
257+
if let Ok(mut elem) = serde_json::from_str::<DomoCacheElement>(&data) {
258+
if elem.republication_timestamp != 0 {
259+
log::debug!("Retransmission");
260+
}
261+
// TODO: do something with this value instead
262+
elem.republication_timestamp = 0;
263+
local_write
264+
.try_put(&elem)
265+
.await
266+
.ok()
267+
.map(|_| DomoEvent::PersistentData(elem))
268+
} else {
269+
None
270+
}
271+
}
272+
}
273+
}
274+
});
275+
276+
(cache, events)
277+
}
278+
279+
#[cfg(test)]
280+
mod test {
281+
use super::*;
282+
use crate::dht::test::*;
283+
use std::{collections::HashSet, pin::pin};
284+
285+
#[tokio::test(flavor = "multi_thread")]
286+
async fn syncronization() {
287+
let [mut a, mut b, mut c] = make_peers().await;
288+
let mut d = make_peer().await;
289+
290+
connect_peer(&mut a, &mut d).await;
291+
connect_peer(&mut b, &mut d).await;
292+
connect_peer(&mut c, &mut d).await;
293+
294+
let a_local_cache = LocalCache::new();
295+
let b_local_cache = LocalCache::new();
296+
let c_local_cache = LocalCache::new();
297+
let d_local_cache = LocalCache::new();
298+
299+
let mut expected: HashSet<_> = (0..10)
300+
.into_iter()
301+
.map(|uuid| format!("uuid-{uuid}"))
302+
.collect();
303+
304+
tokio::task::spawn(async move {
305+
let (a_c, a_ev) = cache_channel(a_local_cache, a, 1000);
306+
let (_b_c, b_ev) = cache_channel(b_local_cache, b, 1000);
307+
let (_c_c, c_ev) = cache_channel(c_local_cache, c, 1000);
308+
309+
let mut a_ev = pin!(a_ev);
310+
let mut b_ev = pin!(b_ev);
311+
let mut c_ev = pin!(c_ev);
312+
for uuid in 0..10 {
313+
let _ = a_c
314+
.put(
315+
"Topic",
316+
&format!("uuid-{uuid}"),
317+
&serde_json::json!({"key": uuid}),
318+
)
319+
.await;
320+
}
321+
322+
loop {
323+
let (node, ev) = tokio::select! {
324+
v = a_ev.next() => ("a", v.unwrap()),
325+
v = b_ev.next() => ("b", v.unwrap()),
326+
v = c_ev.next() => ("c", v.unwrap()),
327+
};
328+
329+
match ev {
330+
DomoEvent::PersistentData(data) => {
331+
log::debug!("{node}: Got data {data:?}");
332+
}
333+
_ => {
334+
log::debug!("{node}: Other {ev:?}");
335+
}
336+
}
337+
}
338+
});
339+
340+
log::info!("Adding D");
341+
342+
let (_d_c, d_ev) = cache_channel(d_local_cache, d, 1000);
343+
344+
let mut d_ev = pin!(d_ev);
345+
while !expected.is_empty() {
346+
let ev = d_ev.next().await.unwrap();
347+
match ev {
348+
DomoEvent::PersistentData(data) => {
349+
assert!(expected.remove(&data.topic_uuid));
350+
log::warn!("d: Got data {data:?}");
351+
}
352+
_ => {
353+
log::warn!("d: Other {ev:?}");
354+
}
355+
}
356+
}
357+
}
358+
}

0 commit comments

Comments
 (0)