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.

@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

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.

Comment thread tests/panels/notification/server/notifyserverapplet_test.cpp
@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented May 20, 2026

TAG Bot

TAG: 2.0.42
EXISTED: yes
DISTRIBUTION: unstable

m_worker->deleteLater();
delete m_worker;
m_worker = nullptr;
m_manager = nullptr;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这里有问题,此时无法确保 m_manager 的确被销毁了。

@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented May 28, 2026

TAG Bot

New tag: 2.0.43
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1611

@heysion heysion force-pushed the heysion-dev branch 8 times, most recently from 825bbf7 to e606777 Compare May 29, 2026 02:52
m_worker = nullptr;
if (m_manager) {
m_manager->deleteLater();
QCoreApplication::sendPostedEvents(m_manager, QEvent::DeferredDelete);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

释放需要这么复杂么?这里的场景很简单,将m_manager移动到worker线程中执行,释放NotifyServerApplet后需要停止这个worker线程,再释放m_manager,和worker,

Comment thread panels/notification/server/notifyserverapplet.cpp Outdated
@heysion heysion force-pushed the heysion-dev branch 4 times, most recently from b1a79a6 to bd77fe9 Compare May 29, 2026 05:40
@heysion heysion requested review from 18202781743 and zccrs May 29, 2026 05:40
Comment thread panels/notification/server/notifyserverapplet.cpp
@heysion heysion force-pushed the heysion-dev branch 4 times, most recently from 67fa2f8 to fd07706 Compare May 29, 2026 06:21
@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented Jun 4, 2026

TAG Bot

New tag: 2.0.44
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1618

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

deepin pr auto review

这份代码变更对通知服务插件进行了重构,主要改进了线程和对象的生命周期管理,增加了空指针保护和重入保护,并补充了详尽的单元测试。整体改进方向非常正确,修复了原有的内存泄漏和潜在的崩溃问题。

以下是对代码的详细审查意见,分为语法逻辑、代码质量、代码性能和代码安全四个方面:

一、 语法与逻辑

  1. 宏定义的末尾分号与 do-while(0) 缩进问题

    • 问题CHECK_MANAGER_RET 宏的 while(0) 缩进不一致,且宏定义末尾缺少分号(虽然使用时可能加了分号,但不符合最佳实践)。
    • 建议:统一缩进,并在宏定义末尾不加 ;,确保调用时必须加分号(像函数调用一样)。
    #define CHECK_MANAGER_RET(val) \
        do { \
            if (!m_manager) { \
                qWarning(notifyLog) << "NotificationManager is null."; \
                return val; \
            } \
        } while(0)
  2. 析构函数中的线程终止逻辑风险

    • 问题:在 ~NotifyServerApplet() 中,如果线程未在 kWaitTimeoutMs 内退出,代码调用了 m_worker->terminate()terminate() 是极其危险的,它会在任意点终止线程,不释放内存、不解锁互斥量,极易导致死锁或内存损坏。此外,如果 terminate() 成功,随后的 delete m_manager不安全的,因为 m_manager 仍属于该 worker 线程,跨线程强制 delete 可能导致其内部锁状态异常。
    • 建议
      • 尽量避免使用 terminate()。如果必须使用,必须在 terminate() 后调用 m_worker->wait() 确保线程真正停止,然后再清理。
      • 对于 m_manager,如果线程被 terminate(),最安全的做法是调用 m_manager->deleteLater() 并将清理推给主线程事件循环(前提是主事件循环还在跑),或者接受这种极端情况下的内存泄漏以换取不崩溃。
      if (!m_worker->wait(kWaitTimeoutMs)) {
          qWarning(notifyLog) << "Worker thread did not exit in time, terminating.";
          m_worker->terminate();
          if (!m_worker->wait(kTerminateWaitMs)) {
              qCritical(notifyLog) << "Worker thread terminate timeout.";
          }
          // 线程被强行终止,直接 delete m_manager 可能不安全
          m_manager->deleteLater(); 
          m_manager = nullptr;
          delete m_worker;
          m_worker = nullptr;
          return;
      }
  3. init() 中的 D-Bus 注册失败逻辑

    • 问题:如果 registerDbusService() 失败,代码 delete m_manager; m_manager = nullptr; 是正确的。但此时 m_worker 仍为 nullptr,如果后续有代码依赖 m_worker 进行清理,逻辑是通的。不过,原有的设计似乎期望 m_manager 在 worker 线程中析构,这里直接在主线程 delete,需确保 NotificationManager 的析构函数是线程安全的(不依赖其所在的线程事件循环)。
    • 建议:确认 NotificationManager 的析构是否依赖线程局部存储或事件循环。如果不依赖,当前逻辑没问题;如果依赖,需要先 move 到 worker 线程再清理(但此时 worker 没启动,比较尴尬,当前做法已是较优解)。

二、 代码质量

  1. 使用 QPointer 替代裸指针

    • 优点:使用 QPointer<NotificationManager>QPointer<QThread> 是本次重构的亮点,有效防止了悬空指针导致的崩溃。
    • 隐患QPointer 在对象销毁时会自动置 nullptr,这导致析构函数中的 else if (m_manager) 逻辑变得不可预测。如果在析构时 m_worker 已经因为某种原因被外部删除(QPointer 自动置空),则 m_manager 可能会漏删。
    • 建议:对象生命周期应由 NotifyServerApplet 严格管理,不建议外部随意 delete 这些成员。如果确实需要防护,析构函数的逻辑应改为分别判断:
    NotifyServerApplet::~NotifyServerApplet() {
        if (m_worker && m_worker->isRunning()) {
            // ... 退出线程逻辑 ...
        }
        
        if (m_worker) {
            delete m_worker;
            m_worker = nullptr;
        }
        
        if (m_manager) {
            delete m_manager; // 或者 m_manager->deleteLater()
            m_manager = nullptr;
        }
    }
  2. 被注释掉的 connect 与实际逻辑不一致

    • 问题:在 init() 中,注释掉了 connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater);。但在测试代码 DestructorDeletesManagerAfterWorkerThreadExit 等用例中,又手动加上了这个 connect。这说明设计者对对象的销毁时机存在犹豫。
    • 建议:明确销毁策略。如果由 finished 信号触发 deleteLater,那么析构函数中就不应该再 delete m_manager,否则会 double free(虽然 QPointer 会置空,但 delete 一个已经销毁的对象是未定义行为)。推荐策略:完全由析构函数控制同步销毁,不使用 deleteLater,这样资源释放最确定、最及时。
  3. 测试代码中的 Hack 手法

    • 问题:测试中使用了 #define private public 来访问私有成员,这破坏了封装,且在某些编译器/标准库下可能导致编译错误或未定义行为(因为 ABI 可能不同)。
    • 建议:在 Qt 中,更好的测试私有成员的方法是将测试类声明为被测类的 friend,或者提供受保护的访问函数给测试子类。如果项目限制无法修改原类,保留当前 Hack 也是常见做法,但需注意跨平台风险。

三、 代码性能

  1. 析构函数中的 wait 超时机制

    • 优点:引入 kWaitTimeoutMs (3000ms) 避免了无限期阻塞主线程,这是非常好的性能保障。
    • 建议:3000ms 对于桌面应用的主线程关闭流程来说可能偏长。如果此析构发生在应用退出时,会导致用户感觉应用“卡死”3秒。建议评估正常退出耗时,将 kWaitTimeoutMs 降至 1000ms - 1500ms。
  2. QMetaObject::invokeMethod 的连接类型

    • 问题:代码中 QMetaObject::invokeMethod(m_manager, ..., Qt::DirectConnection) 强制使用了直接连接。
    • 风险:这意味着这些方法将在调用者线程(通常是主线程)中直接执行,而不是在 m_manager 所在的 worker 线程执行。这与将 m_manager move 到 worker 线程的初衷(避免阻塞主线程)相违背。
    • 建议:如果希望任务在 worker 线程执行,应使用 Qt::QueuedConnection。如果因为需要同步返回值而使用 DirectConnection,那么必须确保这些函数内部都是线程安全的,且不会耗时过长阻塞主线程。如果确实无需多线程执行逻辑,那么 move to thread 的意义就不大了。

四、 代码安全

  1. terminate() 导致的死锁与内存损坏

    • 如前文所述,QThread::terminate() 是重大的安全隐患。如果 worker 线程持有了某个互斥锁,terminate() 后该锁永远不会释放,主线程后续如果请求这把锁就会死锁。
    • 强烈建议:在 terminate() 之后,记录严重的错误日志,并考虑通过退出进程(如 qFatalabort())来避免进程处于不可预测的僵尸状态,或者在设计上保证 worker 线程的退出事件循环一定会响应 quit()
  2. 重入保护的安全性

    • 优点init() 中增加了 if (m_manager || m_worker) 的重入保护,防止了多次调用导致的内存泄漏。
    • 建议:目前是简单的布尔判断。如果该类可能在多线程环境下被调用(虽然 init 通常在主线程),建议使用 std::atomic_flagQMutex 进行严格保护,或者加上 Q_ASSERT 确保不会发生重入。

总结

本次代码重构显著提升了代码的健壮性,特别是引入 QPointer 和析构超时机制。最需要修改的核心问题是:析构函数中 terminate() 后对 m_manager 的处理方式,以及**Qt::DirectConnection 违背了使用 worker 线程的初衷**。建议优先修复这两处逻辑,以确保线上环境的稳定与安全。

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.

4 participants