mirror of
https://github.com/chatmail/core.git
synced 2026-05-17 05:46:30 +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.
|
/// Used as a headline in the connectivity view.
|
||||||
#define DC_STR_OUTGOING_MESSAGES 104
|
#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.
|
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
|
||||||
#define DC_STR_ONE_MOMENT 106
|
#define DC_STR_ONE_MOMENT 106
|
||||||
|
|||||||
@@ -944,7 +944,7 @@ impl Context {
|
|||||||
/// This should only be used by test code and during configure.
|
/// This should only be used by test code and during configure.
|
||||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
#[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<()> {
|
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
|
self.sql
|
||||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ impl Context {
|
|||||||
/// (i.e. [EnteredLoginParam::addr]).
|
/// (i.e. [EnteredLoginParam::addr]).
|
||||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||||
let now = time();
|
let now = time();
|
||||||
self.sql
|
let removed_transport_id = self
|
||||||
|
.sql
|
||||||
.transaction(|transaction| {
|
.transaction(|transaction| {
|
||||||
let primary_addr = transaction.query_row(
|
let primary_addr = transaction.query_row(
|
||||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||||
@@ -251,10 +252,11 @@ impl Context {
|
|||||||
(addr, remove_timestamp),
|
(addr, remove_timestamp),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(transport_id)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
send_sync_transports(self).await?;
|
send_sync_transports(self).await?;
|
||||||
|
self.quota.write().await.remove(&removed_transport_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,9 +243,9 @@ pub struct InnerContext {
|
|||||||
pub(crate) scheduler: SchedulerState,
|
pub(crate) scheduler: SchedulerState,
|
||||||
pub(crate) ratelimit: RwLock<Ratelimit>,
|
pub(crate) ratelimit: RwLock<Ratelimit>,
|
||||||
|
|
||||||
/// Recently loaded quota information, if any.
|
/// Recently loaded quota information for each trasnport, if any.
|
||||||
/// Set to `None` if quota was never tried to load.
|
/// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap.
|
||||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>,
|
||||||
|
|
||||||
/// Notify about new messages.
|
/// Notify about new messages.
|
||||||
///
|
///
|
||||||
@@ -479,7 +479,7 @@ impl Context {
|
|||||||
events,
|
events,
|
||||||
scheduler: SchedulerState::new(),
|
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.
|
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,
|
new_msgs_notify,
|
||||||
server_id: RwLock::new(None),
|
server_id: RwLock::new(None),
|
||||||
metadata: 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.
|
// 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
|
if self
|
||||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
.quota_needs_update(
|
||||||
|
session.transport_id(),
|
||||||
|
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
&& let Err(err) = self.update_recent_quota(&mut session).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 {
|
impl Context {
|
||||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||||
/// called.
|
/// 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;
|
let quota = self.quota.read().await;
|
||||||
quota
|
quota
|
||||||
.as_ref()
|
.get(&transport_id)
|
||||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
||||||
.is_none()
|
.is_none()
|
||||||
}
|
}
|
||||||
@@ -155,10 +155,13 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.quota.write().await = Some(QuotaInfo {
|
self.quota.write().await.insert(
|
||||||
recent: quota,
|
session.transport_id(),
|
||||||
modified: tools::Time::now(),
|
QuotaInfo {
|
||||||
});
|
recent: quota,
|
||||||
|
modified: tools::Time::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
self.emit_event(EventType::ConnectivityChanged);
|
self.emit_event(EventType::ConnectivityChanged);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -203,27 +206,42 @@ mod tests {
|
|||||||
let mut tcm = TestContextManager::new();
|
let mut tcm = TestContextManager::new();
|
||||||
let t = &tcm.unconfigured().await;
|
let t = &tcm.unconfigured().await;
|
||||||
const TIMEOUT: u64 = 60;
|
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 {
|
*t.quota.write().await = {
|
||||||
recent: Ok(Default::default()),
|
let mut map = BTreeMap::new();
|
||||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
map.insert(
|
||||||
});
|
0,
|
||||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
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 {
|
*t.quota.write().await = {
|
||||||
recent: Ok(Default::default()),
|
let mut map = BTreeMap::new();
|
||||||
modified: tools::Time::now(),
|
map.insert(
|
||||||
});
|
0,
|
||||||
assert!(!t.quota_needs_update(TIMEOUT).await);
|
QuotaInfo {
|
||||||
|
recent: Ok(Default::default()),
|
||||||
|
modified: tools::Time::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map
|
||||||
|
};
|
||||||
|
assert!(!t.quota_needs_update(0, TIMEOUT).await);
|
||||||
|
|
||||||
t.evtracker.clear_events();
|
t.evtracker.clear_events();
|
||||||
t.set_primary_self_addr("new@addr").await?;
|
t.set_primary_self_addr("new@addr").await?;
|
||||||
assert!(t.quota.read().await.is_none());
|
assert!(t.quota.read().await.is_empty());
|
||||||
t.evtracker
|
t.evtracker
|
||||||
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
||||||
.await;
|
.await;
|
||||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||||
|
|
||||||
Ok(())
|
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.
|
// 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
|
&& let Err(err) = ctx.update_recent_quota(&mut session).await
|
||||||
{
|
{
|
||||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||||
|
|||||||
@@ -462,21 +462,41 @@ impl Context {
|
|||||||
// [======67%===== ]
|
// [======67%===== ]
|
||||||
// =============================================================================================
|
// =============================================================================================
|
||||||
|
|
||||||
let domain =
|
ret += "<h3>Message Buffers</h3>";
|
||||||
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
|
let transports = self
|
||||||
.domain;
|
.sql
|
||||||
let storage_on_domain =
|
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
|
||||||
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
|
let transport_id: u32 = row.get(0)?;
|
||||||
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
|
let addr: String = row.get(1)?;
|
||||||
|
Ok((transport_id, addr))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let quota = self.quota.read().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 {
|
match "a.recent {
|
||||||
|
Err(e) => {
|
||||||
|
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||||
|
ret += &format!("<li>{domain_escaped} · {error_escaped}</li>");
|
||||||
|
}
|
||||||
Ok(quota) => {
|
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 {
|
for (root_name, resources) in quota {
|
||||||
use async_imap::types::QuotaResourceName::*;
|
use async_imap::types::QuotaResourceName::*;
|
||||||
for resource in resources {
|
for resource in resources {
|
||||||
ret += "<li>";
|
ret += &format!("<li>{domain_escaped} · ");
|
||||||
|
|
||||||
// root name is empty eg. for gmail and redundant eg. for riseup.
|
// root name is empty eg. for gmail and redundant eg. for riseup.
|
||||||
// therefore, use it only if there are really several roots.
|
// therefore, use it only if there are really several roots.
|
||||||
@@ -539,21 +559,9 @@ impl Context {
|
|||||||
ret += "</li>";
|
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>";
|
ret += "</ul>";
|
||||||
|
|
||||||
|
|||||||
@@ -1144,14 +1144,6 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
|
|||||||
translated(context, StockMessage::OutgoingMessages).await
|
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`.
|
/// Stock string: `Not connected`.
|
||||||
pub(crate) async fn not_connected(context: &Context) -> String {
|
pub(crate) async fn not_connected(context: &Context) -> String {
|
||||||
translated(context, StockMessage::NotConnected).await
|
translated(context, StockMessage::NotConnected).await
|
||||||
|
|||||||
Reference in New Issue
Block a user