Skip to content

perf: optimize email fetching with SSE, caching, and concurrent IMAP - #36

Open
EucalyZ wants to merge 1 commit into
ZeroPointSix:mainfrom
EucalyZ:dev
Open

perf: optimize email fetching with SSE, caching, and concurrent IMAP#36
EucalyZ wants to merge 1 commit into
ZeroPointSix:mainfrom
EucalyZ:dev

Conversation

@EucalyZ

@EucalyZ EucalyZ commented Apr 11, 2026

Copy link
Copy Markdown

Summary

  • Skip Graph API for accounts without Mail.Read permission — check token scope after refresh, avoid wasting ~1.5s on guaranteed-401 requests
  • Channel cache (in-memory, 1h TTL) — remember whether each account uses Graph or IMAP, skip probing on subsequent requests
  • Email list cache (in-memory, 2h TTL) — avoid redundant IMAP/Graph fetches for the same mailbox+folder within 2 hours
  • SSE streaming (/api/emails/<email>/stream) — IMAP emails rendered progressively as each message arrives (~5s to first email instead of waiting 30s for all)
  • Concurrent IMAP — try outlook.live.com and outlook.office365.com simultaneously, use whichever connects first
  • Batch IMAP FETCH — single FETCH command for all messages instead of N sequential round-trips
  • Gunicorn gthread — 8 threads per worker, concurrent request handling without blocking (page load API calls served in parallel)
  • Fix cached method name matching (includes() vs ===) preventing email detail fetch after cache hit

Performance comparison

Scenario Before After
Graph API account 1.5-2s 1.5-2s (unchanged)
IMAP account (cold) 20-60s (blocked UI) ~5s first email visible (SSE)
IMAP account (cached) 20-60s 3ms
Concurrent page load APIs Serial (one blocks all) Parallel (8 threads)
IMAP server failover +20-30s (sequential retry) 0s (concurrent)

Test plan

  • Graph API accounts load emails correctly
  • IMAP-only accounts stream emails progressively via SSE
  • Cached results return instantly (verify (cached) in method tag)
  • Force refresh (获取邮件 button) bypasses cache
  • Email detail click works for both Graph and cached-Graph accounts
  • Multiple concurrent requests don't block each other

🤖 Generated with Claude Code

…concurrent IMAP

- Skip Graph API for accounts without Mail.Read permission (check token scope)
- Cache account channel (graph/imap) in memory with 1h TTL to avoid repeated probing
- Add server-side email list cache with 2h TTL to eliminate redundant fetches
- SSE streaming endpoint (/api/emails/<email>/stream) for progressive email rendering
- Concurrent IMAP: try both outlook.live.com and outlook.office365.com simultaneously
- Batch IMAP FETCH: single request for all messages instead of N sequential fetches
- Gunicorn: switch to gthread worker with 8 threads for concurrent request handling
- Fix cached method name matching (use includes() instead of strict equality)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ZeroPointSix

ZeroPointSix commented Apr 11, 2026

Copy link
Copy Markdown
Owner

审查中

@ZeroPointSix

Copy link
Copy Markdown
Owner

感谢你的贡献!这个 PR 在性能优化方向上的思路很有价值,特别是以下几点我们非常认可:

  • Graph API 权限预检(scope 判断跳过无 Mail.Read 的账号):避免了 ~1.5s 的无效 API 调用,这个思路很好
  • IMAP 并发双服务器尝试:同时连接 outlook.live.comoutlook.office365.com,消除了串行 failover 的等待时间
  • 批量 IMAP FETCH:单次 FETCH 命令替代 N 次串行网络往返
  • SSE 流式渲染理念:首封邮件 ~5s 可见而非等待 30s 全部加载完成,UX 方向正确

不过在代码审查中我们发现了一些需要关注的问题,目前不适合直接合并:

1. account_type 路由分发缺失

当前主分支的 api_get_emails() 已支持通用 IMAP 账号(account_type == "imap"),通过 imap_generic 服务处理。新增的 api_stream_emails() SSE 端点完全忽略了这一分支,会导致通用 IMAP 账号调用 SSE 时走入 Outlook OAuth IMAP 认证路径,必然报错。

2. 缓存缺少失效机制和容量限制

  • invalidate_email_cache()invalidate_channel() 虽然已实现,但在整个代码库中没有任何调用点。删除邮件、标记已读、Token 刷新后缓存不会失效,用户在 2h 内会看到已删除的邮件。
  • 缓存是无限增长的内存 dict,缺少 LRU / 最大条目数限制,大规模邮箱管理场景下存在内存泄漏风险。

3. 原始 API 的缓存与接口语义矛盾

api_get_emails() 入口直接加入缓存检查,但该函数注释为「支持分页,不使用缓存」,且没有 force 参数。外部 API 调用方会始终拿到缓存数据,与预期行为不符。

4. 与当前 dev 分支有大量冲突

我们的 dev 分支近期新增了验证码提取通道路由、AI 增强提取等功能,与此 PR 涉及的 controllers/emails.py 有较大差异,直接合并会产生大量冲突。

5. SSE 流式端点仍用逐条 FETCH

stream_emails_imap() 中仍是逐条 connection.fetch(msg_id, "(RFC822)"),与 PR 描述的 "Batch IMAP FETCH" 优化不一致。流式场景的性能提升主要来自并发连接而非批量 FETCH。


我们的计划

我们会将这个 PR 中有价值的优化思路纳入内部 backlog,在下一个版本中由团队自行实现,并补齐:

  • 完整的 account_type 路由兼容
  • 缓存失效策略(删除/标记/刷新时主动失效)+ LRU 容量限制
  • 与现有 dev 分支的功能集成
  • 测试覆盖

再次感谢你的贡献和优化思路!🙏

Copy link
Copy Markdown
Owner

Migration note: I reviewed this PR during the #109 migration pass, but did not include it in #112.

Reason: #36 is a broader SSE/cache/controller rewrite and also touches the old frontend email surface. Applying it wholesale onto the Ant Design Pro SPA branch would mix a larger architecture change into the backend migration PR. It should be handled as a separate follow-up after #109 stabilizes.

Copy link
Copy Markdown
Owner

发版处置结论

本 PR 不纳入本次发版,现关闭。

原因

  • 该 PR 属于较大的 SSE / 缓存 / IMAP 架构改造,不适合与当前 SPA 发版混入。
  • 仍存在缓存失效策略、容量限制、通用 IMAP 路由兼容等需要单独处理的问题。
  • 后续如继续推进,应基于当前 main 新建性能优化 PR,避免基于旧前端代码继续演进。

后续

保留性能优化方向,后续独立评估 SSE、缓存和 IMAP 并发方案。

Copy link
Copy Markdown
Owner

处置更新

根据当前版本规划,PR #36 不关闭,保留作为后续邮件获取性能优化方向。

当前发版范围

后续计划

  • 保留 PR 作为性能优化候选。
  • 后续单独评估并处理:
    • 通用 IMAP / Outlook IMAP 路由完整兼容。
    • 缓存失效策略和容量控制。
    • 与当前 SPA 邮件页面契约对齐。
    • 完整回归测试。

本 PR 状态调整为:保留,延期到后续版本处理。

Copy link
Copy Markdown
Owner

发布线路归类

本次仅调整发布归类,不关闭该 PR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants