fix: 修复 _email_poller 线程静默死亡问题

- IMAP 连接加 30s socket timeout,防止 _send_id_command 等操作永久阻塞
- _email_poller 加顶层 try/except,异常后 sleep 5s 继续循环
- @retry() 加指数退避延迟(1s,2s,4s...),避免重试风暴
This commit is contained in:
2026-07-06 19:54:23 +08:00
committed by Zichao Lin
parent c684090b9c
commit 0c1ccdfc79
3 changed files with 21 additions and 14 deletions

View File

@@ -45,6 +45,7 @@ def _signal_handler(signum, frame):
def _email_poller(cfg): def _email_poller(cfg):
logger.info("邮件轮询线程已启动") logger.info("邮件轮询线程已启动")
while _running: while _running:
try:
for acct_idx, mail in poll_accounts(cfg): for acct_idx, mail in poll_accounts(cfg):
if not _running: if not _running:
return return
@@ -52,6 +53,9 @@ def _email_poller(cfg):
if mail.uid not in _pending_uids: if mail.uid not in _pending_uids:
_pending_uids.add(mail.uid) _pending_uids.add(mail.uid)
_email_queue.put((acct_idx, mail)) _email_queue.put((acct_idx, mail))
except Exception as e:
logger.error(f"邮件轮询线程异常: {e}", exc_info=True)
time.sleep(5)
if _running: if _running:
for _ in range(cfg.polling.interval_seconds): for _ in range(cfg.polling.interval_seconds):
if not _running: if not _running:

View File

@@ -87,12 +87,12 @@ def _select_mailbox(conn, mailbox: str = "INBOX"):
raise RuntimeError(f"无法选择邮箱 {mailbox}: {data}") raise RuntimeError(f"无法选择邮箱 {mailbox}: {data}")
def _login_and_prepare(account: EmailAccount): def _login_and_prepare(account: EmailAccount, timeout: int = 30):
logger.info("连接 %s:%d", account.imap_server, account.imap_port) logger.info("连接 %s:%d (超时%ds)", account.imap_server, account.imap_port, timeout)
if account.use_ssl: if account.use_ssl:
conn = imaplib.IMAP4_SSL(account.imap_server, account.imap_port) conn = imaplib.IMAP4_SSL(account.imap_server, account.imap_port, timeout=timeout)
else: else:
conn = imaplib.IMAP4(account.imap_server, account.imap_port) conn = imaplib.IMAP4(account.imap_server, account.imap_port, timeout=timeout)
if account.use_starttls: if account.use_starttls:
conn.starttls() conn.starttls()
conn.login(account.username, account.password) conn.login(account.username, account.password)

View File

@@ -1,10 +1,11 @@
import logging import logging
import time
from functools import wraps from functools import wraps
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def retry(max_tries=10): def retry(max_tries=10, base_delay=1, max_delay=60):
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@@ -15,10 +16,12 @@ def retry(max_tries=10):
except Exception as e: except Exception as e:
last_exc = e last_exc = e
if attempt < max_tries: if attempt < max_tries:
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
logger.warning( logger.warning(
"%s 失败(第%d次)正在重试: %s", "%s 失败(第%d次)%d秒后重试: %s",
func.__name__, attempt, e, func.__name__, attempt, delay, e,
) )
time.sleep(delay)
else: else:
logger.error( logger.error(
"%s 失败 %d 次,放弃: %s", "%s 失败 %d 次,放弃: %s",