fix: make background_fetch not wait on or trigger smtp connections

Also adds tests and docs to respective functions,
clarifying background fetching behaviour and the `ACCOUNTS_BACKGROUND_FETCH_DONE` event,
that came up in questions/discussions with UI devs lately.
This commit is contained in:
holger krekel
2026-09-16 17:29:34 +02:00
parent 44c2febbe1
commit 5db55ac4de
10 changed files with 132 additions and 24 deletions

View File

@@ -482,11 +482,15 @@ impl Accounts {
/// return immediately even before the timeout expiration
/// or finishing fetching.
///
/// Pending outgoing messages are not waited for and not triggered.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
/// If another background fetch is already running,
/// nothing is fetched and the event is emitted immediately.
/// The event carries no data identifying the call it belongs to,
/// so it only safely refers to your call if no concurrent background fetch is happening.
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.

View File

@@ -601,9 +601,13 @@ impl Context {
/// Does a single round of fetching messages from all transports and returns.
///
/// Can be used even if I/O is currently stopped.
/// If I/O is stopped, fetches over a dedicated connection per transport
/// and returns as soon as one of them fetched messages.
/// If IO is stopped, pauses the scheduler and fetches over a dedicated connection
/// per transport, returning as soon as one of them fetched messages.
/// If IO is running, interrupts IMAP IDLE on all transports
/// and waits until they are done fetching.
///
/// Does not wait for outgoing messages to be sent out,
/// use [`crate::accounts::Accounts::is_sending_finished`] for that.
pub async fn background_fetch(&self) -> Result<()> {
if !(self.is_configured().await?) {
return Ok(());
@@ -613,8 +617,9 @@ impl Context {
info!(self, "background_fetch started.");
if self.scheduler.is_running().await {
self.scheduler.maybe_network().await;
self.wait_for_all_work_done().await;
self.scheduler.interrupt_inbox_idle().await;
let include_smtp = false;
self.wait_for_work_done(include_smtp).await;
} else {
self.scheduler.background_fetch_any(self).await?;
}

View File

@@ -363,6 +363,9 @@ pub enum EventType {
/// A call made while another background fetch is running gets the event immediately,
/// and the running fetch keeps emitting events until its own marker.
///
/// The event carries no data identifying the call it belongs to,
/// so it is unambiguous only if there are no concurrent background fetch calls.
///
/// This event is only emitted by the account manager.
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.

View File

@@ -215,10 +215,17 @@ impl SchedulerState {
/// Indicate that the network likely has come back.
pub(crate) async fn maybe_network(&self) {
self.interrupt_inbox_idle().await;
self.interrupt_smtp().await;
}
/// Interrupts IDLE on all transports so that they fetch,
/// and marks them as having work to do.
pub(crate) async fn interrupt_inbox_idle(&self) {
let inner = self.inner.read().await;
let inboxes = match *inner {
InnerSchedulerState::Started(ref scheduler) => {
scheduler.maybe_network();
scheduler.interrupt_inbox();
scheduler
.inboxes
.iter()
@@ -799,13 +806,6 @@ impl Scheduler {
self.inboxes.iter()
}
fn maybe_network(&self) {
for b in self.boxes() {
b.conn_state.interrupt();
}
self.interrupt_smtp();
}
fn maybe_network_lost(&self) {
for b in self.boxes() {
b.conn_state.interrupt();

View File

@@ -1,6 +1,6 @@
use core::fmt;
use std::cmp::min;
use std::{iter::once, ops::Deref, sync::Arc};
use std::{ops::Deref, sync::Arc};
use anyhow::Result;
use humansize::{BINARY, format_size};
@@ -531,14 +531,15 @@ impl Context {
Ok(ret)
}
/// Returns true if all background work is done.
async fn all_work_done(&self) -> bool {
/// Returns true if all background work is done,
/// checking the outgoing message queue only if `include_smtp` is set.
async fn work_done(&self, include_smtp: bool) -> bool {
let lock = self.scheduler.inner.read().await;
let stores: Vec<_> = match *lock {
InnerSchedulerState::Started(ref sched) => sched
.boxes()
.map(|b| &b.conn_state.state)
.chain(once(&sched.smtp.state))
.chain(include_smtp.then_some(&sched.smtp.state))
.map(|state| state.connectivity.clone())
.collect(),
_ => return false,
@@ -555,19 +556,26 @@ impl Context {
/// Waits until background work is finished.
pub async fn wait_for_all_work_done(&self) {
let include_smtp = true;
self.wait_for_work_done(include_smtp).await
}
/// Waits until background work is finished,
/// checking the outgoing message queue only if `include_smtp` is set.
pub(crate) async fn wait_for_work_done(&self, include_smtp: bool) {
// Ideally we could wait for connectivity change events,
// but sleep loop is good enough.
// First 100 ms sleep in chunks of 10 ms.
for _ in 0..10 {
if self.all_work_done().await {
if self.work_done(include_smtp).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// If we are not finished in 100 ms, keep waking up every 100 ms.
while !self.all_work_done().await {
while !self.work_done(include_smtp).await {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
@@ -576,6 +584,34 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_background_fetch_leaves_smtp_alone() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.start_io().await;
alice.wait_for_all_work_done().await;
let smtp = match *alice.scheduler.inner.read().await {
InnerSchedulerState::Started(ref scheduler) => {
scheduler.smtp.state.connectivity.clone()
}
_ => panic!("scheduler is not running"),
};
smtp.set_working(&alice);
alice.background_fetch().await?;
assert!(!smtp.get_all_work_done());
assert!(alice.scheduler.is_running().await);
alice
.assert_warns_or_errors(&[
"No IMAP connection candidates provided",
"IMAP got rate limited",
])
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_combine_connectivities() {