Skip to content

fix: notification bubble timeout should start when bubble is shown, not when enqueued - #1739

Closed
mhduiy wants to merge 1 commit into
masterfrom
agent/pms-bug-bot/978b7bb8ad5a
Closed

mhduiy wants to merge 1 commit into
masterfrom
agent/pms-bug-bot/978b7bb8ad5a

Conversation

@mhduiy

@mhduiy mhduiy commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

修复 BUG-372279:通知 bubble 超时计时起点错误

问题描述

以 0.2s 间隔连发 100 条通知时,桌面通知无法自动消失,通知中心暂驻区域无法显示通知。间隔 ≥0.3s 或注销/重启后恢复正常。

根因

通知 5 秒超时计时起点设在服务端入列时刻(notificationmanager.cpp 中 point = now + interval),而非 bubble 实际上屏时刻。Bubble 上屏有 300ms 节流排队机制(bubblemodel.cpp),当以 0.2s 间隔连发通知时,约第 51 条起超时计时在 bubble 上屏前就已到期。超时到期时 removeById 只检查 m_bubbles(已上屏列表),不覆盖 pendingBubbles(待上屏队列),导致 close 信号丢失,bubble 永久卡在桌面。

修复方案

采用产品确认的改法 2(根因层修复):将通知超时计时起点从"服务端入列时刻"改为"bubble 实际上屏时刻",服务端仍为唯一超时持有者。

改动文件(8 个文件):

  • notificationmanager.h/.cpp:Notify() 中不再直接调用 pushPendingEntity,改为将 expireTimeout 暂存到 m_pendingExpireTimeouts;新增 onBubbleShowed(qint64 id) 按上屏回调启动计时;removePendingEntity() 末尾清理暂存
  • notifyserverapplet.h/.cpp:新增 onBubbleShowed(qint64 id) 槽,跨线程转发到 manager
  • bubblemodel.h/.cpp:新增 bubbleShown(qint64 id) 信号,在 insertBubble() 和 replaceBubble() 末尾 emit
  • bubblepanel.h/.cpp:init() 中连接 bubbleShown → onBubbleShown,通过 QMetaObject::invokeMethod 转发到 applet

信号流转链路:

BubbleModel::insertBubble/replaceBubble
  → emit bubbleShown(id)
  → BubblePanel::onBubbleShown(id)
  → NotifyServerApplet::onBubbleShowed(id)
  → NotificationManager::onBubbleShowed(id)
  → pushPendingEntity(entity, expireTimeout)  // 5s 计时从此刻开始

与 #1691 方案对比

存在关联 PR #1691(由 @Ivy233 创建,同样修复 BUG-372279,当前 OPEN)。两个方案解决同一根因,但实现路径不同:

本 PR #1691
超时持有者 服务端 NotificationManager(不变) 前端 ExpireTimer 单例(新增)
实现方式 服务端 onBubbleShowed 回调,bubble 上屏后回传服务端启动计时 前端独立管理超时,将超时机制整体迁移到前端
改动范围 8 文件,新增信号/槽转发链路,复用既有跨线程模式 前端新增 ExpireTimer 单例
设计理念 最小改动,保持服务端为唯一超时持有者 前端自治,减少跨线程通信

两个方案各有优劣,供 reviewer 对比评估后决定合并方向。

PMS Bug

https://pms.uniontech.com/bug-view-372279.html

Summary by Sourcery

Start notification expiration timing when each bubble becomes visible to ensure queued notifications can be displayed and dismissed correctly.

Bug Fixes:

  • Start notification bubble expiration timers when bubbles are displayed instead of when notifications are enqueued, preventing queued bubbles from expiring before they appear.

Enhancements:

  • Add bubble display notifications across the bubble panel and notification server so the server remains the single owner of expiration timing.

…ot when enqueued

When notifications are sent rapidly (e.g. 0.2s interval), the 5-second
timeout was calculated from the enqueue time rather than when the bubble
actually appears on screen. Due to the 300ms throttle queue in BubbleModel,
bubbles could still be pending when their timeout expired, causing the
server close signal to be lost (removeById only checks shown bubbles, not
the pending queue). This left bubbles permanently stuck on screen and broke
the notification center's temporary display area.

Fix by deferring pushPendingEntity until the bubble is actually shown:
- Store expireTimeout in m_pendingExpireTimeouts at Notify() time
- Add onBubbleShowed() to start the timeout when bubble appears
- Emit bubbleShown signal from BubbleModel on insert/replace
- Forward through BubblePanel → NotifyServerApplet → NotificationManager

Closes: BUG-372279

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @mhduiy, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 hours and 30 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: mhduiy

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes premature notification expiration by deferring the server-managed timeout until the bubble is actually inserted or replaced on screen, using a new bubbleShown signal and callback chain from the bubble model through the panel and applet to NotificationManager.

Sequence diagram for starting notification timeout after bubble is shown

sequenceDiagram
    participant BubbleModel
    participant BubblePanel
    participant NotifyServerApplet
    participant NotificationManager
    participant Persistence

    BubbleModel->>BubblePanel: bubbleShown(id)
    BubblePanel->>NotifyServerApplet: onBubbleShowed(id)
    NotifyServerApplet->>NotificationManager: onBubbleShowed(id)
    NotificationManager->>Persistence: fetchEntity(id)
    Persistence-->>NotificationManager: NotifyEntity
    NotificationManager->>NotificationManager: pushPendingEntity(entity, expireTimeout)
    Note over NotificationManager: Timeout begins when bubble is shown
Loading

File-Level Changes

Change Details Files
Move notification expiration start from enqueue time to the moment the bubble is displayed.
  • Store per-notification expiration durations while bubbles are pending.
  • Start the existing server-side timeout when the display callback is received.
  • Remove deferred timeout state when a notification is removed or cannot be resolved.
panels/notification/server/notificationmanager.cpp
panels/notification/server/notificationmanager.h
Add a cross-component callback chain to report bubble display events to the notification manager.
  • Emit a bubbleShown signal after insertion and replacement.
  • Connect the signal through the panel and applet using direct method invocation.
  • Expose the display callback as an applet and manager slot.
panels/notification/bubble/bubblemodel.cpp
panels/notification/bubble/bubblemodel.h
panels/notification/bubble/bubblepanel.cpp
panels/notification/bubble/bubblepanel.h
panels/notification/server/notifyserverapplet.cpp
panels/notification/server/notifyserverapplet.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

AI 代码审查报告

PR: linuxdeepin/dde-shell#1739
标题: fix: notification bubble timeout should start when bubble is shown, not when enqueued
作者: mhduiy
分支: agent/pms-bug-bot/978b7bb8ad5a → master
关联Bug: BUG-372279
审查时间: 2026-09-14
修改文件数: 8 (新增 44 行, 删除 1 行)


总体评分

维度 评分 状态
语法逻辑 (25%) 25/25 ✓
代码质量 (25%) 22/25 ✓
代码性能 (20%) 19/20 ✓
代码安全 (30%) 30/30 ✓
总分 96/100 优秀

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个


审查结论

代码审查通过。本次 PR 修复了 BUG-372279(通知 bubble 超时计时起点错误),将超时计时从"服务端入列时刻"改为"bubble 实际上屏时刻",通过新增信号/槽转发链路实现。代码实现与 commit message 目的完全一致,信号流转链路清晰,错误处理完善,无安全漏洞。存在少量代码质量改进空间(缺少注释、命名不一致),但不影响功能正确性。


维度1:语法逻辑 (25/25) ✓

语法正确,逻辑清晰,边界处理完善

分析内容:

本次修改涉及 8 个文件,核心逻辑是将通知超时计时起点从 Notify() 入列时刻延迟到 bubble 实际上屏时刻。信号流转链路如下:

BubbleModel::insertBubble/replaceBubble
  → emit bubbleShown(id)
  → BubblePanel::onBubbleShown(id)
  → NotifyServerApplet::onBubbleShowed(id) [QMetaObject::invokeMethod, DirectConnection]
  → NotificationManager::onBubbleShowed(id) [QMetaObject::invokeMethod, DirectConnection]
  → pushPendingEntity(entity, expireTimeout)  // 5s 计时从此刻开始
  1. BubbleModel::insertBubble() (bubblemodel.cpp:86) 和 replaceBubble() (bubblemodel.cpp:103) 在 bubble 上屏/替换后正确 emit bubbleShown 信号
  2. BubblePanel::init() (bubblepanel.cpp:57) 正确连接 bubbleShown → onBubbleShown 信号槽
  3. BubblePanel::onBubbleShown() (bubblepanel.cpp:173-176) 通过 QMetaObject::invokeMethod 转发到 NotifyServerApplet,与现有 onBubbleClosed、onActionInvoked 方法使用相同的跨线程转发模式
  4. NotifyServerApplet::onBubbleShowed() (notifyserverapplet.cpp:112-115) 转发到 NotificationManager::onBubbleShowed()
  5. NotificationManager::onBubbleShowed() (notificationmanager.cpp:516-533) 从 m_pendingExpireTimeouts 取出暂存的超时值,从持久层获取 entity,调用 pushPendingEntity 启动计时
  6. NotificationManager::Notify() (notificationmanager.cpp:313) 不再直接调用 pushPendingEntity,改为 m_pendingExpireTimeouts.insert(entity.id(), expireTimeout) 暂存
  7. NotificationManager::removePendingEntity() (notificationmanager.cpp:790) 末尾新增 m_pendingExpireTimeouts.remove(entity.id()) 清理暂存

边界处理:

  • onBubbleShowed 中正确处理 id 不在 m_pendingExpireTimeouts 中的情况(提前 return)
  • onBubbleShowed 中正确处理 fetchEntity 返回无效 entity 的情况(日志告警 + 清理暂存 + return)
  • removePendingEntity 正确清理 m_pendingExpireTimeouts,避免内存泄漏

线程安全:

  • NotificationManager 运行在独立 worker 线程(notifyserverapplet.cpp:61-63),m_pendingExpireTimeouts 被多线程访问(Notify() 在 worker 线程写入,onBubbleShowed() 在调用线程读取)。此模式与现有代码中 m_pendingTimeoutEntities 的访问模式一致,属于既有设计约定,本次修改未引入新的线程安全问题。

维度2:代码质量 (22/25) ✓

代码结构清晰,注释基本完整,存在少量改进空间

分析内容:

  1. 注释完整性 (-2分):NotificationManager::Notify() 中将 pushPendingEntity 替换为 m_pendingExpireTimeouts.insert() 的位置缺少注释说明为何要延迟启动超时计时。onBubbleShowed() 方法虽有实现逻辑,但缺少方法级注释说明其设计目的("当 bubble 实际上屏后,从暂存中取出超时值并启动计时")。建议添加注释以提高可维护性。

  2. 命名一致性 (-1分):方法命名存在不一致:

    • BubblePanel::onBubbleShown (使用 "shown")
    • NotificationManager::onBubbleShowed (使用 "showed")
    • NotifyServerApplet::onBubbleShowed (使用 "showed")

    "shown" 是 "show" 更标准的过去分词形式。建议统一命名为 onBubbleShown。

  3. 结构合理性:代码遵循既有的信号/槽转发模式,新增方法与现有方法(onBubbleClosed、onActionInvoked)结构一致,模块耦合度合理。

  4. 调试信息清理:onBubbleShowed 中的 qWarning 日志输出用于记录异常情况(entity 无效),属于合理的运维日志,非残留调试代码。


维度3:代码性能 (19/20) ✓

性能良好,资源使用合理

分析内容:

  1. 数据结构选择合理:m_pendingExpireTimeouts 使用 QHash<qint64, int>,插入、查找、删除均为 O(1) 平均时间复杂度,适合高频通知场景。

  2. 额外持久层查询 (-1分):onBubbleShowed() 中调用 m_persistence->fetchEntity(id) 从持久层重新获取 entity。在原始实现中,Notify() 中已有 entity 对象可直接传给 pushPendingEntity。此额外查询是延迟计时机制的必要代价(因为 entity 对象在 Notify() 作用域结束后不可保留),但增加了每次 bubble 上屏时的一次持久层 I/O。对于通知场景(非高频),性能影响可接受。

  3. 资源管理:m_pendingExpireTimeouts 的清理路径完善:

    • 正常路径:onBubbleShowed 中 erase
    • 异常路径:removePendingEntity 中 remove
    • 无资源泄漏风险

维度4:代码安全 (30/30) ✓

存在0个安全漏洞

分析内容:

  1. 无命令注入风险:所有参数传递通过 Qt 类型安全的信号/槽机制,qint64 类型的 id 参数不可被注入恶意内容。

  2. 无敏感信息泄露:代码中无硬编码密钥、密码或 Token。qWarning 日志仅输出 id 数值,无敏感信息。

  3. 无路径遍历风险:不涉及文件路径操作。

  4. 无缓冲区溢出风险:使用 Qt 容器类(QHash),自动管理内存。

  5. 无权限绕过风险:超时计时机制的改变不影响通知系统的权限控制逻辑。

  6. 输入校验充分:onBubbleShowed 中对 fetchEntity 返回值进行有效性校验,防止使用无效 entity。


改进建议

建议1:添加方法注释

在 NotificationManager::onBubbleShowed() 和 Notify() 中的暂存位置添加注释:

// notificationmanager.cpp - Notify() 方法中
// 0: never expire. -1: DefaultTimeOutMSecs
if (expireTimeout != 0 && !critical) {
    // 暂存超时值,待 bubble 实际上屏后再启动计时
    // 避免 bubble 在排队等待上屏期间超时已到期
    m_pendingExpireTimeouts.insert(entity.id(), expireTimeout);
}
// notificationmanager.cpp - onBubbleShowed() 方法
/**
 * @brief bubble 实际上屏后的回调,启动超时计时
 * @param id 通知 entity ID
 * 
 * 从 m_pendingExpireTimeouts 取出暂存的超时值,
 * 从持久层获取 entity 并启动超时计时。
 * 此方法确保超时计时从 bubble 上屏时刻开始,而非入列时刻。
 */
void NotificationManager::onBubbleShowed(qint64 id)
{
    // ... implementation
}

建议2:统一命名

将 onBubbleShowed 统一重命名为 onBubbleShown,与 BubblePanel::onBubbleShown 保持一致:

// notificationmanager.h
Q_INVOKABLE void onBubbleShown(qint64 id);  // 改为 onBubbleShown

// notifyserverapplet.h
void onBubbleShown(qint64 id);  // 改为 onBubbleShown

修改文件清单

文件 修改内容
panels/notification/bubble/bubblemodel.h 新增 bubbleShown(qint64 id) 信号声明
panels/notification/bubble/bubblemodel.cpp insertBubble() 和 replaceBubble() 末尾 emit bubbleShown
panels/notification/bubble/bubblepanel.h 新增 onBubbleShown(qint64 id) 槽声明
panels/notification/bubble/bubblepanel.cpp init() 中连接信号;新增 onBubbleShown() 方法转发到 server
panels/notification/server/notificationmanager.h 新增 Q_INVOKABLE onBubbleShowed;新增 m_pendingExpireTimeouts 成员
panels/notification/server/notificationmanager.cpp Notify() 改为暂存超时;新增 onBubbleShowed() 方法;removePendingEntity() 清理暂存
panels/notification/server/notifyserverapplet.h 新增 onBubbleShowed(qint64 id) 槽声明
panels/notification/server/notifyserverapplet.cpp 新增 onBubbleShowed() 方法转发到 manager

@mhduiy

mhduiy commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

#1691 再此跟踪

@mhduiy mhduiy closed this Sep 14, 2026
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