mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
feat: connectivity view: quota for all transports (#7630)
- **show quota of all relays** - **remove `DC_STR_STORAGE_ON_DOMAIN` stock string** - renames the quota section to "Relay Capacity" until we come up with a better name in https://github.com/chatmail/core/issues/7580#issuecomment-3633803432 closes #7591 <img width="300" alt="image" src="https://github.com/user-attachments/assets/1909dccd-e6b3-42e6-963f-004b2b464db7" /> <img width="300" alt="image" src="https://github.com/user-attachments/assets/1e97e67b-e0ed-492b-95a0-6ef12595abe4" />
This commit is contained in:
@@ -7314,12 +7314,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a headline in the connectivity view.
|
||||
#define DC_STR_OUTGOING_MESSAGES 104
|
||||
|
||||
/// "Storage on %1$s"
|
||||
///
|
||||
/// Used as a headline in the connectivity view.
|
||||
///
|
||||
/// `%1$s` will be replaced by the domain of the configured e-mail address.
|
||||
#define DC_STR_STORAGE_ON_DOMAIN 105
|
||||
|
||||
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
|
||||
#define DC_STR_ONE_MOMENT 106
|
||||
|
||||
@@ -944,7 +944,7 @@ impl Context {
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.take();
|
||||
self.quota.write().await.clear();
|
||||
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||
|
||||
@@ -210,7 +210,8 @@ impl Context {
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
self.sql
|
||||
let removed_transport_id = self
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let primary_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
@@ -251,10 +252,11 @@ impl Context {
|
||||
(addr, remove_timestamp),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
Ok(transport_id)
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.quota.write().await.remove(&removed_transport_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -243,9 +243,9 @@ pub struct InnerContext {
|
||||
pub(crate) scheduler: SchedulerState,
|
||||
pub(crate) ratelimit: RwLock<Ratelimit>,
|
||||
|
||||
/// Recently loaded quota information, if any.
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
/// Recently loaded quota information for each trasnport, if any.
|
||||
/// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap.
|
||||
pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
@@ -479,7 +479,7 @@ impl Context {
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
|
||||
quota: RwLock::new(None),
|
||||
quota: RwLock::new(BTreeMap::new()),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(None),
|
||||
@@ -614,8 +614,13 @@ impl Context {
|
||||
}
|
||||
|
||||
// Update quota (to send warning if full) - but only check it once in a while.
|
||||
// note: For now this only checks quota of primary transport,
|
||||
// because background check only checks primary transport at the moment
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.quota_needs_update(
|
||||
session.transport_id(),
|
||||
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
|
||||
)
|
||||
.await
|
||||
&& let Err(err) = self.update_recent_quota(&mut session).await
|
||||
{
|
||||
|
||||
56
src/quota.rs
56
src/quota.rs
@@ -107,10 +107,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
|
||||
impl Context {
|
||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||
/// called.
|
||||
pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
|
||||
pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
|
||||
let quota = self.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.get(&transport_id)
|
||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
||||
.is_none()
|
||||
}
|
||||
@@ -155,10 +155,13 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
*self.quota.write().await = Some(QuotaInfo {
|
||||
recent: quota,
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
self.quota.write().await.insert(
|
||||
session.transport_id(),
|
||||
QuotaInfo {
|
||||
recent: quota,
|
||||
modified: tools::Time::now(),
|
||||
},
|
||||
);
|
||||
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
@@ -203,27 +206,42 @@ mod tests {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
const TIMEOUT: u64 = 60;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
||||
});
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
*t.quota.write().await = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
0,
|
||||
QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
||||
},
|
||||
);
|
||||
map
|
||||
};
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
assert!(!t.quota_needs_update(TIMEOUT).await);
|
||||
*t.quota.write().await = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
0,
|
||||
QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now(),
|
||||
},
|
||||
);
|
||||
map
|
||||
};
|
||||
assert!(!t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
t.evtracker.clear_events();
|
||||
t.set_primary_self_addr("new@addr").await?;
|
||||
assert!(t.quota.read().await.is_none());
|
||||
assert!(t.quota.read().await.is_empty());
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
||||
.await;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +481,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(60).await
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session).await
|
||||
{
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
|
||||
@@ -462,21 +462,41 @@ impl Context {
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain =
|
||||
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
|
||||
.domain;
|
||||
let storage_on_domain =
|
||||
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
|
||||
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
|
||||
ret += "<h3>Message Buffers</h3>";
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
|
||||
let transport_id: u32 = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((transport_id, addr))
|
||||
})
|
||||
.await?;
|
||||
let quota = self.quota.read().await;
|
||||
if let Some(quota) = &*quota {
|
||||
ret += "<ul>";
|
||||
for (transport_id, transport_addr) in transports {
|
||||
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
|
||||
.map_or(transport_addr, |email| email.domain);
|
||||
let domain_escaped = escaper::encode_minimal(domain);
|
||||
let Some(quota) = quota.get(&transport_id) else {
|
||||
let not_connected = stock_str::not_connected(self).await;
|
||||
ret += &format!("<li>{domain_escaped} · {not_connected}</li>");
|
||||
continue;
|
||||
};
|
||||
match "a.recent {
|
||||
Err(e) => {
|
||||
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||
ret += &format!("<li>{domain_escaped} · {error_escaped}</li>");
|
||||
}
|
||||
Ok(quota) => {
|
||||
if !quota.is_empty() {
|
||||
if quota.is_empty() {
|
||||
ret += &format!(
|
||||
"<li>{domain_escaped} · Warning: {domain_escaped} claims to support quota but gives no information</li>"
|
||||
);
|
||||
} else {
|
||||
for (root_name, resources) in quota {
|
||||
use async_imap::types::QuotaResourceName::*;
|
||||
for resource in resources {
|
||||
ret += "<li>";
|
||||
ret += &format!("<li>{domain_escaped} · ");
|
||||
|
||||
// root name is empty eg. for gmail and redundant eg. for riseup.
|
||||
// therefore, use it only if there are really several roots.
|
||||
@@ -539,21 +559,9 @@ impl Context {
|
||||
ret += "</li>";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let domain_escaped = escaper::encode_minimal(domain);
|
||||
ret += &format!(
|
||||
"<li>Warning: {domain_escaped} claims to support quota but gives no information</li>"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||
ret += &format!("<li>{error_escaped}</li>");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let not_connected = stock_str::not_connected(self).await;
|
||||
ret += &format!("<li>{not_connected}</li>");
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
|
||||
@@ -1144,14 +1144,6 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::OutgoingMessages).await
|
||||
}
|
||||
|
||||
/// Stock string: `Storage on %1$s`.
|
||||
/// `%1$s` will be replaced by the domain of the configured email-address.
|
||||
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
|
||||
translated(context, StockMessage::StorageOnDomain)
|
||||
.await
|
||||
.replace1(domain)
|
||||
}
|
||||
|
||||
/// Stock string: `Not connected`.
|
||||
pub(crate) async fn not_connected(context: &Context) -> String {
|
||||
translated(context, StockMessage::NotConnected).await
|
||||
|
||||
Reference in New Issue
Block a user