Skip to content

fix(notification): fix memory leak in NotifyServerApplet destructor#1598

Open
heysion wants to merge 1 commit into
linuxdeepin:masterfrom
heysion:heysion-dev
Open

fix(notification): fix memory leak in NotifyServerApplet destructor#1598
heysion wants to merge 1 commit into
linuxdeepin:masterfrom
heysion:heysion-dev

Conversation

@heysion
Copy link
Copy Markdown
Member

@heysion heysion commented May 15, 2026

Fixed the issue where NotificationManager was not properly destroyed when the worker thread exited. The deleteLater() call was scheduled but never executed because the thread's event loop had already stopped.

Added comprehensive unit tests to verify the fix covers all destruction scenarios including worker-only, manager-only, and both present cases.

Log: Fix memory leak in NotifyServerApplet destructor

fix(notification): 修复 NotifyServerApplet 析构函数中的内存泄漏

修复了当工作线程退出时 NotificationManager 未被正确销毁的问题。
deleteLater() 被调度但从未执行,因为线程的事件循环已经停止。

添加了全面的单元测试来验证修复涵盖了所有销毁场景,
包括仅 worker、仅 manager 和两者都存在的情况。

Log: 修复 NotifyServerApplet 析构函数中的内存泄漏

Summary by Sourcery

Ensure NotifyServerApplet correctly cleans up NotificationManager and its worker thread to prevent memory leaks on destruction.

Bug Fixes:

  • Fix memory leak where NotificationManager was not destroyed if its worker thread event loop had already stopped when the applet was destructed.

Enhancements:

  • Improve NotifyServerApplet destructor logic to explicitly shut down and delete the worker thread and manager in all ownership scenarios.

Tests:

  • Add unit tests covering NotifyServerApplet destruction when only the manager exists, only the worker exists, and when both are present, including verification that NotificationManager is actually destroyed.

Fixed the issue where NotificationManager was not properly destroyed
when the worker thread exited. The deleteLater() call was scheduled
but never executed because the thread's event loop had already stopped.

Added comprehensive unit tests to verify the fix covers all destruction
scenarios including worker-only, manager-only, and both present cases.

Log: Fix memory leak in NotifyServerApplet destructor

fix(notification): 修复 NotifyServerApplet 析构函数中的内存泄漏

修复了当工作线程退出时 NotificationManager 未被正确销毁的问题。
deleteLater() 被调度但从未执行,因为线程的事件循环已经停止。

添加了全面的单元测试来验证修复涵盖了所有销毁场景,
包括仅 worker、仅 manager 和两者都存在的情况。

Log: 修复 NotifyServerApplet 析构函数中的内存泄漏
@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: heysion

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
Copy link
Copy Markdown

sourcery-ai Bot commented May 15, 2026

Reviewer's Guide

Refactors NotifyServerApplet’s destructor to ensure NotificationManager is destroyed correctly in all worker-thread and main-thread cases, and adds targeted unit tests that validate the new destruction behavior and prevent regressions around the previous memory leak.

Sequence diagram for updated NotifyServerApplet destruction logic

sequenceDiagram
    participant NotifyServerApplet
    participant QThread_worker as m_worker
    participant NotificationManager_manager as m_manager

    NotifyServerApplet->>NotifyServerApplet: ~NotifyServerApplet()
    alt m_worker != nullptr
        opt m_manager != nullptr
            NotifyServerApplet->>QThread_worker: connect(m_worker.finished, m_manager.deleteLater)
        end
        NotifyServerApplet->>QThread_worker: exit()
        NotifyServerApplet->>QThread_worker: wait()
        NotifyServerApplet->>NotifyServerApplet: delete m_worker
        NotifyServerApplet->>NotifyServerApplet: m_worker = nullptr
        NotifyServerApplet->>NotifyServerApplet: m_manager = nullptr
    else m_worker == nullptr and m_manager != nullptr
        NotifyServerApplet->>NotifyServerApplet: delete m_manager
        NotifyServerApplet->>NotifyServerApplet: m_manager = nullptr
    end
Loading

File-Level Changes

Change Details Files
Make NotifyServerApplet destructor correctly handle lifetime of NotificationManager and worker thread depending on which objects are present.
  • Remove unconditional use of deleteLater() on m_manager at the beginning of the destructor.
  • When a worker thread exists, connect QThread::finished to the manager’s deleteLater() with Qt::UniqueConnection so the deletion happens while the worker event loop is still active.
  • Exit and wait for the worker thread, then delete the worker directly, nulling out m_worker and m_manager and returning early.
  • When no worker exists but a manager does, delete the manager directly and null out m_manager.
panels/notification/server/notifyserverapplet.cpp
Add tests to cover all destructor scenarios and verify that NotificationManager is destroyed exactly once in each case, preventing the prior leak.
  • Introduce TrackableNotificationManager subclass that counts destructor invocations via a static std::atomic_int.
  • Use QPointer to track manager object lifetime in tests.
  • Add tests for: manager-only case, worker-only case, and worker+manager case with the manager moved to the worker thread, verifying correct destruction and no crashes.
  • Include QTest utilities and temporarily expose NotifyServerApplet private members via #define private public to set up internal state for tests.
tests/panels/notification/server/notifyserverapplet_test.cpp

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

这份代码变更主要重构了 NotifyServerApplet 的析构函数,以修复 Qt 多线程环境下对象生命周期管理的潜在问题,并增加了相应的单元测试。

整体来看,这次修改的方向是正确的,解决了一个典型的 Qt 线程对象销毁顺序问题。但在代码逻辑、安全性和测试设计上还有一些可以改进和注意的地方。以下是详细的审查意见:

一、 语法与逻辑

1. 析构函数中的 return 语句导致逻辑分支冗余
NotifyServerApplet::~NotifyServerApplet() 中,当 m_worker 存在时,处理完后直接 return,然后在函数末尾又处理了无 m_workerm_manager 的删除。虽然逻辑正确,但 return 使得代码流不够清晰,且两个分支都对 m_manager 进行了处理,增加了维护成本。

改进意见: 去掉 return,使用更清晰的 if-else 结构。

NotifyServerApplet::~NotifyServerApplet()
{
    qDebug(notifyLog) << "Exit notification server.";
    
    if (m_worker) {
        if (m_manager) {
            // Schedule manager deletion while the worker event loop is still able to process it.
            connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);
        }
        m_worker->exit();
        m_worker->wait();
        delete m_worker;
        m_worker = nullptr;
        // m_manager 的生命周期已交给 QThread::finished 信号管理,无需在此置空或删除
    } else if (m_manager) {
        // 没有 worker 线程,直接在当前线程删除
        delete m_manager;
        m_manager = nullptr;
    }
}

2. m_manager = nullptr; 的位置问题
在原 diff 的 if (m_worker) 分支中,执行了 m_manager = nullptr;。此时 m_manager 实际上还没有被删除(它被放到了线程结束后的事件循环中删除),将其置空是合理的,可以防止析构期间其他地方误用。但结合上面的逻辑简化,如果走 m_worker 分支,m_manager 已经通过信号槽安排了后事,此处不再显式置空也不会有悬挂指针问题,因为对象正在析构,不会再有外部访问。

二、 代码质量

1. 测试代码中破坏封装性的宏定义
测试文件中使用了:

#define private public
#include "notifyserverapplet.h"
#undef private

这是一种非常不推荐的做法。它破坏了类的封装性,使得测试与类的内部实现强耦合。一旦类的成员变量名或结构改变,测试就会崩溃,且可能导致 ODR(One Definition Rule)问题。

改进意见:

  • 推荐做法:为 NotifyServerApplet 添加一个友元类声明,或者提供一个受保护的/私有的构造函数允许注入依赖(Dependency Injection)。
  • 次优做法:如果因为历史代码原因无法修改被测头文件,尽量通过公共接口(如 init() 方法)将 managerworker 注入,而不是直接 testApplet->m_manager = manager;

2. 测试类 TrackableNotificationManager 的设计
使用静态原子变量 destroyedCount 来追踪析构次数在单测中容易产生状态污染。如果某个测试意外失败或中途崩溃,这个计数器可能不会被重置,影响后续测试。

改进意见:
可以利用 Qt 自身的机制,使用 QSignalSpy 监听 destroyed 信号,这样更符合 Qt 的测试习惯,且无状态污染:

QSignalSpy destroySpy(manager, &QObject::destroyed);
// ... 触发析构 ...
EXPECT_EQ(destroySpy.count(), 1);

三、 代码性能

1. Qt::UniqueConnection 带来的微小开销
在析构函数中连接信号时使用了 Qt::UniqueConnection

connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);

从性能角度看,Qt::UniqueConnection 在底层会遍历已有的连接列表以检查是否重复,带来极微小的性能开销。而在析构函数中,m_workerm_manager 即将被销毁,不可能存在重复连接的情况。

改进意见:
移除 Qt::UniqueConnection 标志,直接使用 Qt::AutoConnection(默认)即可:

connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater);

四、 代码安全

1. 线程安全:m_manager 的跨线程访问隐患
代码在析构函数中判断 if (m_manager),并调用 connect。如果 m_manager 是一个 QObject 且已经通过 moveToThread 移动到了 m_worker 线程中(从测试用例 manager->moveToThread(worker); 可以看出这是预期用法),那么在主线程的析构函数中直接访问 m_manager 的指针本身是安全的(只要不调用非线程安全的函数),但调用 connect 时,由于跨线程,内部会向 m_worker 线程的事件循环 post 事件。

潜在风险:如果在调用 connect 之前,m_worker 的事件循环已经因为某些异常原因停止或 m_manager 已经被其他机制删除,这里就会产生崩溃或无效连接。不过,由于紧接着就调用了 m_worker->exit()m_worker->wait(),这段逻辑在时序上是受控的。

改进建议:
确保在对象析构或销毁前,停止所有向 m_manager 发送信号的源头,防止在 m_worker->wait() 期间,其他线程向已经安排销毁的 m_manager 发送事件导致崩溃。

2. m_worker->wait() 的死锁风险
m_worker->wait() 是阻塞调用。如果 m_worker 线程中正在执行一个耗时操作或持有互斥锁,而主线程又在等待它结束,可能会造成死锁。

改进意见:
虽然这是业务逻辑层面的问题,但建议在 wait() 之前增加超时机制,或者在文档中明确说明 m_worker 必须能够及时响应 exit() 退出事件循环。

if (!m_worker->wait(5000)) { // 例如等待5秒
    qWarning(notifyLog) << "Worker thread failed to exit in time, forcing termination.";
    m_worker->terminate();
    m_worker->wait();
}

总结

这次修改的核心逻辑(利用 QThread::finished 触发 deleteLater 来安全销毁跨线程对象)是非常正确和专业的做法。建议:

  1. 简化析构函数的 return 分支,使其更易读。
  2. 移除不必要的 Qt::UniqueConnection
  3. 重构测试代码,尽量避免 #define private public,改用 QSignalSpy 代替静态计数器,提升测试的健壮性和规范性。

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The destructor logic assumes the worker thread is still running when the connection to finished is made; if the thread has already finished (but m_worker is non-null), the finished signal will never fire and m_manager will leak, so consider checking m_worker->isRunning() and deleting m_manager directly in the non-running case.
  • The tests use #define private public to access NotifyServerApplet internals; it would be more robust to expose the needed state via test-only helpers or friend declarations rather than redefining access specifiers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The destructor logic assumes the worker thread is still running when the connection to `finished` is made; if the thread has already finished (but `m_worker` is non-null), the `finished` signal will never fire and `m_manager` will leak, so consider checking `m_worker->isRunning()` and deleting `m_manager` directly in the non-running case.
- The tests use `#define private public` to access `NotifyServerApplet` internals; it would be more robust to expose the needed state via test-only helpers or friend declarations rather than redefining access specifiers.

## Individual Comments

### Comment 1
<location path="panels/notification/server/notifyserverapplet.cpp" line_range="28-31" />
<code_context>
 NotifyServerApplet::~NotifyServerApplet()
 {
     qDebug(notifyLog) << "Exit notification server.";
-    if (m_manager) {
-        m_manager->deleteLater();
-    }
     if (m_worker) {
+        if (m_manager) {
+            // Schedule manager deletion while the worker event loop is still able to process it.
+            connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);
+        }
         m_worker->exit();
</code_context>
<issue_to_address>
**issue (bug_risk):** QThread::finished → deleteLater on an object in the worker thread is likely never processed after calling exit(), leading to a leak.

Because `QThread::finished` is emitted in the owning (likely main/UI) thread, the `deleteLater` call on `m_manager` becomes a queued event to the worker’s event loop. But `exit()` stops that event loop and `finished` is emitted only after it has already exited, so the queued `deleteLater` may never run, leaking `m_manager`.

Instead, delete `m_manager` before shutting down the worker, for example by:
- Calling `QMetaObject::invokeMethod(m_manager, "deleteLater", Qt::BlockingQueuedConnection)` before `m_worker->exit()`/`wait()`, or
- Deleting it directly if it already lives in the main thread.

The key point is to clean up `m_manager` synchronously in the correct thread, rather than relying on `finished` after the event loop is gone.
</issue_to_address>

### Comment 2
<location path="tests/panels/notification/server/notifyserverapplet_test.cpp" line_range="127-110" />
<code_context>
+    EXPECT_NO_THROW(delete testApplet);
+}
+
+TEST_F(NotifyServerAppletTest, DestructorDeletesManagerAfterWorkerThreadExit) {
+    TrackableNotificationManager::destroyedCount = 0;
+
+    auto *testApplet = new NotifyServerApplet();
+    auto *worker = new QThread();
+    auto *manager = new TrackableNotificationManager();
+    QPointer<TrackableNotificationManager> managerGuard(manager);
+
+    testApplet->m_manager = manager;
+    testApplet->m_worker = worker;
+
+    manager->moveToThread(worker);
+    worker->start();
+
+    QTRY_VERIFY_WITH_TIMEOUT(worker->isRunning(), 1000);
+
+    delete testApplet;
+
+    EXPECT_TRUE(managerGuard.isNull());
+    EXPECT_EQ(TrackableNotificationManager::destroyedCount.load(), 1);
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** The null QPointer and destroyedCount checks may race with an asynchronous deleteLater, making this test potentially flaky.

Because the manager is deleted via a signal/`deleteLater`, its actual destruction depends on event loop processing. Immediately checking `managerGuard.isNull()` and `destroyedCount == 1` after `delete testApplet;` can therefore be timing‑dependent. To avoid flakiness, either wait for the event loop until the guard becomes null (e.g. `QTRY_VERIFY_WITH_TIMEOUT(managerGuard.isNull(), 1000);`) before asserting the count, or make the manager deletion synchronous in this path so the assertions can be deterministic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 28 to +31
if (m_worker) {
if (m_manager) {
// Schedule manager deletion while the worker event loop is still able to process it.
connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): QThread::finished → deleteLater on an object in the worker thread is likely never processed after calling exit(), leading to a leak.

Because QThread::finished is emitted in the owning (likely main/UI) thread, the deleteLater call on m_manager becomes a queued event to the worker’s event loop. But exit() stops that event loop and finished is emitted only after it has already exited, so the queued deleteLater may never run, leaking m_manager.

Instead, delete m_manager before shutting down the worker, for example by:

  • Calling QMetaObject::invokeMethod(m_manager, "deleteLater", Qt::BlockingQueuedConnection) before m_worker->exit()/wait(), or
  • Deleting it directly if it already lives in the main thread.

The key point is to clean up m_manager synchronously in the correct thread, rather than relying on finished after the event loop is gone.

delete testApplet;

EXPECT_TRUE(managerGuard.isNull());
EXPECT_EQ(TrackableNotificationManager::destroyedCount.load(), 1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): The null QPointer and destroyedCount checks may race with an asynchronous deleteLater, making this test potentially flaky.

Because the manager is deleted via a signal/deleteLater, its actual destruction depends on event loop processing. Immediately checking managerGuard.isNull() and destroyedCount == 1 after delete testApplet; can therefore be timing‑dependent. To avoid flakiness, either wait for the event loop until the guard becomes null (e.g. QTRY_VERIFY_WITH_TIMEOUT(managerGuard.isNull(), 1000);) before asserting the count, or make the manager deletion synchronous in this path so the assertions can be deterministic.

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