From 71258e3736baba33889d529db875053cc56d3244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 5 Aug 2024 18:01:52 -0700 Subject: [PATCH] Add Nostr event cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a simple in-memory Nostr Event cache, to reduce the amount of bandwidth used as well as to improve performance. Testing ------- Setup: - Two iPhone simulators running Damus and on different accounts - Damus version: 774da239b92ed630fbf91fce42d9e233661c0d7f - Notepush: This commit - Push notifications turned on, setup to connect to localhost, and configured to receive DM notifications - Run Notepush with `RUST_LOG=DEBUG` env variable for debug logging Steps: 1. Send a DM from one account to another. - Push notification should arrive with a few seconds delay - Push notification logs should mention that the event was cached 2. Send a DM again. - Push notification should arrive immediately 3. Wait for more than a minute 4. Send a DM again. - Push notification should take a few seconds again - Push notification logs should mention that the cache item expired and was deleted Signed-off-by: Daniel D’Aquino Closes: https://github.com/damus-io/notepush/issues/3 --- src/notification_manager/mod.rs | 1 + src/notification_manager/nostr_event_cache.rs | 144 ++++++++++++++++++ .../nostr_network_helper.rs | 40 +++-- .../notification_manager.rs | 5 +- 4 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 src/notification_manager/nostr_event_cache.rs diff --git a/src/notification_manager/mod.rs b/src/notification_manager/mod.rs index a3311a0..10b6ab8 100644 --- a/src/notification_manager/mod.rs +++ b/src/notification_manager/mod.rs @@ -1,5 +1,6 @@ pub mod nostr_network_helper; mod nostr_event_extensions; +mod nostr_event_cache; pub mod notification_manager; pub use nostr_network_helper::NostrNetworkHelper; diff --git a/src/notification_manager/nostr_event_cache.rs b/src/notification_manager/nostr_event_cache.rs new file mode 100644 index 0000000..b2b9840 --- /dev/null +++ b/src/notification_manager/nostr_event_cache.rs @@ -0,0 +1,144 @@ +use crate::utils::time_delta::TimeDelta; +use tokio::time::Duration; +use nostr_sdk::prelude::*; +use std::collections::HashMap; +use std::sync::Arc; +use log; + +use super::nostr_event_extensions::MaybeConvertibleToMuteList; + +struct CacheEntry { + event: Option, // `None` means the event does not exist as far as we know (It does NOT mean expired) + added_at: nostr::Timestamp, +} + +impl CacheEntry { + fn is_expired(&self, max_age: Duration) -> bool { + let time_delta = TimeDelta::subtracting(nostr::Timestamp::now(), self.added_at); + time_delta.negative || (time_delta.delta_abs_seconds > max_age.as_secs()) + } +} + +pub struct Cache { + entries: HashMap>, + mute_lists: HashMap>, + contact_lists: HashMap>, + max_age: Duration, +} + +impl Cache { + // MARK: - Initialization + + pub fn new(max_age: Duration) -> Self { + Cache { + entries: HashMap::new(), + mute_lists: HashMap::new(), + contact_lists: HashMap::new(), + max_age, + } + } + + // MARK: - Adding items to the cache + + pub fn add_optional_mute_list_with_author(&mut self, author: &PublicKey, mute_list: Option) { + if let Some(mute_list) = mute_list { + self.add_event(mute_list); + } else { + self.mute_lists.insert( + author.clone(), + Arc::new(CacheEntry { + event: None, + added_at: nostr::Timestamp::now(), + }), + ); + } + } + + pub fn add_optional_contact_list_with_author(&mut self, author: &PublicKey, contact_list: Option) { + if let Some(contact_list) = contact_list { + self.add_event(contact_list); + } else { + self.contact_lists.insert( + author.clone(), + Arc::new(CacheEntry { + event: None, + added_at: nostr::Timestamp::now(), + }), + ); + } + } + + pub fn add_event(&mut self, event: Event) { + let entry = Arc::new(CacheEntry { + event: Some(event.clone()), + added_at: nostr::Timestamp::now(), + }); + self.entries.insert(event.id.clone(), entry.clone()); + + match event.kind { + Kind::MuteList => { + self.mute_lists.insert(event.pubkey.clone(), entry.clone()); + log::debug!("Added mute list to the cache. Event ID: {}", event.id.to_hex()); + } + Kind::ContactList => { + self.contact_lists + .insert(event.pubkey.clone(), entry.clone()); + log::debug!("Added contact list to the cache. Event ID: {}", event.id.to_hex()); + } + _ => { + log::debug!("Added event to the cache. Event ID: {}", event.id.to_hex()); + } + } + } + + // MARK: - Fetching items from the cache + + pub fn get_mute_list(&mut self, pubkey: &PublicKey) -> Result, CacheError> { + if let Some(entry) = self.mute_lists.get(pubkey) { + let entry = entry.clone(); // Clone the Arc to avoid borrowing issues + if !entry.is_expired(self.max_age) { + if let Some(event) = entry.event.clone() { + return Ok(event.to_mute_list()); + } + } else { + log::debug!("Mute list for pubkey {} is expired, removing it from the cache", pubkey.to_hex()); + self.mute_lists.remove(pubkey); + self.remove_event_from_all_maps(&entry.event); + } + } + Err(CacheError::NotFound) + } + + pub fn get_contact_list(&mut self, pubkey: &PublicKey) -> Result, CacheError> { + if let Some(entry) = self.contact_lists.get(pubkey) { + let entry = entry.clone(); // Clone the Arc to avoid borrowing issues + if !entry.is_expired(self.max_age) { + return Ok(entry.event.clone()); + } else { + log::debug!("Contact list for pubkey {} is expired, removing it from the cache", pubkey.to_hex()); + self.contact_lists.remove(pubkey); + self.remove_event_from_all_maps(&entry.event); + } + } + Err(CacheError::NotFound) + } + + // MARK: - Removing items from the cache + + fn remove_event_from_all_maps(&mut self, event: &Option) { + if let Some(event) = event { + let event_id = event.id.clone(); + let pubkey = event.pubkey.clone(); + self.entries.remove(&event_id); + self.mute_lists.remove(&pubkey); + self.contact_lists.remove(&pubkey); + } + // We can't remove an event from all maps if the event does not exist + } +} + +// Error type +#[derive(Debug)] +pub enum CacheError { + NotFound, +} diff --git a/src/notification_manager/nostr_network_helper.rs b/src/notification_manager/nostr_network_helper.rs index 1e77c34..fbd64d1 100644 --- a/src/notification_manager/nostr_network_helper.rs +++ b/src/notification_manager/nostr_network_helper.rs @@ -1,12 +1,15 @@ use super::nostr_event_extensions::MaybeConvertibleToMuteList; use super::ExtendedEvent; use nostr_sdk::prelude::*; +use super::nostr_event_cache::Cache; use tokio::time::{timeout, Duration}; const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5); +const CACHE_MAX_AGE: Duration = Duration::from_secs(60); pub struct NostrNetworkHelper { client: Client, + cache: Cache, } impl NostrNetworkHelper { @@ -16,13 +19,14 @@ impl NostrNetworkHelper { let client = Client::new(&Keys::generate()); client.add_relay(relay_url.clone()).await?; client.connect().await; - Ok(NostrNetworkHelper { client }) + + Ok(NostrNetworkHelper { client, cache: Cache::new(CACHE_MAX_AGE) }) } // MARK: - Answering questions about a user pub async fn should_mute_notification_for_pubkey( - &self, + &mut self, event: &Event, pubkey: &PublicKey, ) -> bool { @@ -67,7 +71,7 @@ impl NostrNetworkHelper { } pub async fn does_pubkey_follow_pubkey( - &self, + &mut self, source_pubkey: &PublicKey, target_pubkey: &PublicKey, ) -> bool { @@ -82,16 +86,32 @@ impl NostrNetworkHelper { false } - // MARK: - Fetching specific event types + // MARK: - Getting specific event types with caching - pub async fn get_public_mute_list(&self, pubkey: &PublicKey) -> Option { - self.fetch_single_event(pubkey, Kind::MuteList) - .await? - .to_mute_list() + pub async fn get_public_mute_list(&mut self, pubkey: &PublicKey) -> Option { + match self.cache.get_mute_list(pubkey) { + Ok(optional_mute_list) => optional_mute_list, + Err(_) => { + // We don't have an answer from the cache, so we need to fetch it + let mute_list_event = self.fetch_single_event(pubkey, Kind::MuteList) + .await; + self.cache.add_optional_mute_list_with_author(pubkey, mute_list_event.clone()); + mute_list_event?.to_mute_list() + } + } } - pub async fn get_contact_list(&self, pubkey: &PublicKey) -> Option { - self.fetch_single_event(pubkey, Kind::ContactList).await + pub async fn get_contact_list(&mut self, pubkey: &PublicKey) -> Option { + match self.cache.get_contact_list(pubkey) { + Ok(optional_contact_list) => optional_contact_list, + Err(_) => { + // We don't have an answer from the cache, so we need to fetch it + let contact_list_event = self.fetch_single_event(pubkey, Kind::ContactList) + .await; + self.cache.add_optional_contact_list_with_author(pubkey, contact_list_event.clone()); + contact_list_event + } + } } // MARK: - Lower level fetching functions diff --git a/src/notification_manager/notification_manager.rs b/src/notification_manager/notification_manager.rs index f6f1112..5427c68 100644 --- a/src/notification_manager/notification_manager.rs +++ b/src/notification_manager/notification_manager.rs @@ -27,7 +27,6 @@ pub struct NotificationManager { db: Mutex>, apns_topic: String, apns_client: Mutex, - nostr_network_helper: Mutex, } @@ -221,7 +220,7 @@ impl NotificationManager { let mut pubkeys_to_notify = HashSet::new(); for pubkey in relevant_pubkeys_yet_to_receive { let should_mute: bool = { - let mute_manager_mutex_guard = self.nostr_network_helper.lock().await; + let mut mute_manager_mutex_guard = self.nostr_network_helper.lock().await; mute_manager_mutex_guard .should_mute_notification_for_pubkey(event, &pubkey) .await @@ -286,7 +285,7 @@ impl NotificationManager { ) -> Result> { let notification_preferences = self.get_user_notification_settings(pubkey, device_token).await?; if notification_preferences.only_notifications_from_following_enabled { - let nostr_network_helper_mutex_guard = self.nostr_network_helper.lock().await; + let mut nostr_network_helper_mutex_guard = self.nostr_network_helper.lock().await; if !nostr_network_helper_mutex_guard.does_pubkey_follow_pubkey(pubkey, &event.author()).await { return Ok(false); }