Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/backend_task/system_task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ impl AppContext {
self: &Arc<Self>,
theme_mode: ThemeMode,
) -> Result<BackendTaskSuccessResult, String> {
let _guard = self.invalidate_settings_cache();

self.db
.update_theme_preference(theme_mode)
.map_err(|e| e.to_string())?;
Expand Down
72 changes: 71 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ use egui::Context;
use rusqlite::Result;
use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard};

const ANIMATION_REFRESH_TIME: std::time::Duration = std::time::Duration::from_millis(100);

/// A guard that ensures settings cache invalidation happens atomically
///
/// This guard holds a write lock on the cached settings, preventing reads
/// until the database update is complete and the cache is properly invalidated.
type SettingsCacheGuard<'a> = RwLockWriteGuard<'a, Option<Settings>>;

#[derive(Debug)]
pub struct AppContext {
pub(crate) network: Network,
Expand Down Expand Up @@ -70,6 +76,9 @@ pub struct AppContext {
/// This is used to control animations in the UI, such as loading spinners or transitions.
/// Disable for automated tests.
animate: AtomicBool,
/// Cached settings to avoid expensive database reads
/// Use RwLock to allow multiple readers but exclusive writers for cache invalidation
cached_settings: RwLock<Option<Settings>>,
// subtasks started by the app context, used for graceful shutdown
pub(crate) subtasks: Arc<TaskManager>,
}
Expand Down Expand Up @@ -176,6 +185,7 @@ impl AppContext {
transactions_waiting_for_finality: Mutex::new(BTreeMap::new()),
zmq_connection_status: Mutex::new(ZMQConnectionEvent::Disconnected),
animate,
cached_settings: RwLock::new(None),
subtasks,
};

Expand Down Expand Up @@ -472,13 +482,73 @@ impl AppContext {

/// Updates the `start_root_screen` in the settings table
pub fn update_settings(&self, root_screen_type: RootScreenType) -> Result<()> {
let _guard = self.invalidate_settings_cache();

self.db
.insert_or_update_settings(self.network, root_screen_type)
}

/// Updates the main password settings
pub fn update_main_password(
&self,
salt: &[u8],
nonce: &[u8],
password_check: &[u8],
) -> Result<()> {
let _guard = self.invalidate_settings_cache();

self.db.update_main_password(salt, nonce, password_check)
}

/// Updates the Dash Core execution settings
pub fn update_dash_core_execution_settings(
&self,
custom_dash_qt_path: Option<std::path::PathBuf>,
overwrite_dash_conf: bool,
) -> Result<()> {
let _guard = self.invalidate_settings_cache();

self.db
.update_dash_core_execution_settings(custom_dash_qt_path, overwrite_dash_conf)
}

/// Invalidates the settings cache and returns a guard
///
/// The cache is invalidated immediately and the guard prevents concurrent access
/// until the database operation is complete. This ensures atomicity and prevents
/// race conditions regardless of whether the database operation succeeds or fails.
pub fn invalidate_settings_cache(&self) -> SettingsCacheGuard {
let mut guard = self.cached_settings.write().unwrap();
*guard = None;
guard
}

/// Retrieves the current settings
///
/// ## Cached
///
/// This function uses a cache to avoid expensive database operations.
/// The cache is invalidated when settings are updated.
///
/// Use [`AppContext::invalidate_settings_cache`] to invalidate the cache.
pub fn get_settings(&self) -> Result<Option<Settings>> {
// First, try to read from cache
{
let cache = self.cached_settings.read().unwrap();
if let Some(ref settings) = *cache {
return Ok(Some(settings.clone()));
}
}

// Cache miss, read from database
let settings = self.db.get_settings()?.map(Settings::from);

// Update cache with the fresh data
{
let mut cache = self.cached_settings.write().unwrap();
*cache = settings.clone();
}

Ok(settings)
}

Expand Down
15 changes: 13 additions & 2 deletions src/database/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use std::{path::PathBuf, str::FromStr};

impl Database {
/// Inserts or updates the settings in the database. This method ensures that only one row exists.
///
/// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior.
Comment thread
lklimek marked this conversation as resolved.
pub fn insert_or_update_settings(
&self,
network: Network,
Expand All @@ -27,6 +29,9 @@ impl Database {
Ok(())
}

/// Updates the main password information in the settings table.
///
/// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior.
pub fn update_main_password(
&self,
salt: &[u8],
Expand All @@ -45,7 +50,9 @@ impl Database {

Ok(())
}

/// Updates the Dash Core execution settings in the settings table.
///
/// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior.
pub fn update_dash_core_execution_settings(
&self,
custom_dash_qt_path: Option<PathBuf>,
Expand Down Expand Up @@ -112,7 +119,9 @@ impl Database {

Ok(())
}

/// Updates the theme preference in the settings table.
///
/// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior.
pub fn update_theme_preference(&self, theme_preference: ThemeMode) -> Result<()> {
let theme_str = match theme_preference {
ThemeMode::Light => "Light",
Expand Down Expand Up @@ -144,6 +153,8 @@ impl Database {
}

/// Retrieves the settings from the database.
///
/// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior.
#[allow(clippy::type_complexity)]
pub fn get_settings(
&self,
Expand Down
5 changes: 3 additions & 2 deletions src/ui/components/top_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ fn add_connection_indicator(ui: &mut Ui, app_context: &Arc<AppContext>) -> AppAc
let resp = resp.on_hover_text(tip);

if resp.clicked() && !connected {
let settings = app_context.db.get_settings().ok().flatten();
let settings = app_context.get_settings().ok().flatten();

let (custom_path, overwrite) = settings
.map(|(_, _, _, custom_path, overwrite, _)| (custom_path, overwrite))
.map(|s| (s.dash_qt_path, s.overwrite_dash_conf))
.unwrap_or((None, true));
if let Some(dash_qt_path) = custom_path {
action |= AppAction::BackendTask(BackendTask::CoreTask(
Expand Down
1 change: 0 additions & 1 deletion src/ui/network_chooser_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ impl NetworkChooserScreen {
/// TODO: doesn't save local network settings like password yet.
fn save(&self) -> Result<(), String> {
self.current_app_context()
Comment thread
lklimek marked this conversation as resolved.
.db
.update_dash_core_execution_settings(
self.custom_dash_qt_path.clone(),
self.overwrite_dash_conf,
Expand Down
1 change: 0 additions & 1 deletion src/ui/wallets/add_new_wallet_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ impl AddNewWalletScreen {
let (encrypted_message, salt, nonce) =
encrypt_message(DASH_SECRET_MESSAGE, self.password.as_str())?;
self.app_context
.db
.update_main_password(&salt, &nonce, &encrypted_message)
.map_err(|e| e.to_string())?;
Comment thread
lklimek marked this conversation as resolved.
}
Expand Down
1 change: 0 additions & 1 deletion src/ui/wallets/import_wallet_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ impl ImportWalletScreen {
let (encrypted_message, salt, nonce) =
encrypt_message(DASH_SECRET_MESSAGE, self.password.as_str())?;
self.app_context
Comment thread
lklimek marked this conversation as resolved.
.db
.update_main_password(&salt, &nonce, &encrypted_message)
.map_err(|e| e.to_string())?;
}
Expand Down
Loading