Skip to content

CPartFile destructor calls FlushBuffer after write thread has already exited

Closure

Closed on 2026-05-24 after refreshed shutdown-flush evidence landed:

  • app commit d743bbd (BUG-012 BUG-119: harden part-file shutdown progress) keeps .part.met saving behind the successful shutdown flush decision.
  • test commit 9dcb7d7 (BUG-012 BUG-031 BUG-114 BUG-118 BUG-119: cover hardening seams) covers the shutdown path that skips destructor flushes when the write thread is gone and verifies that metadata saving is skipped after a failed shutdown flush.

Validation:

  • python -m emule_workspace validate
  • python -m emule_workspace build app --variant main --config Debug --platform x64 --build-output-mode ErrorsOnly
  • python -m emule_workspace build app --variant main --config Release --platform x64 --build-output-mode ErrorsOnly
  • python -m emule_workspace build tests --config Debug --platform x64 --build-output-mode ErrorsOnly
  • python -m emule_workspace build tests --config Release --platform x64 --build-output-mode ErrorsOnly
  • python -m emule_workspace test native --config Debug --platform x64 --suite-name packets --suite-name known_file_hash_open --suite-name standby_prevention --suite-name parity --build-output-mode ErrorsOnly
  • python -m emule_workspace test native --config Release --platform x64 --suite-name packets --suite-name known_file_hash_open --suite-name standby_prevention --suite-name parity --build-output-mode ErrorsOnly

Implementation

Landed in emulebb-main commit 4b4087d (Harden part-file durability and disk-space handling): - PartFilePersistenceSeams::ShouldFlushPartFileOnDestroy(bIsClosing, bHasThread, bThreadRunning) added - CPartFile::~CPartFile() now checks seam before calling FlushBuffer: skips flush when app is closing and write thread is already stopped - PartFilePersistenceSeams.h added for testability

Follow-up hardening in emulebb-main commit 75a92c2 restored that guard at the first shutdown flush decision in CPartFile::FlushBufferedDataForShutdown(). The function now returns before the first FlushBuffer(false, true) attempt when the app is closing and the part-file write thread is missing or stopped.

Summary

CPartFile::~CPartFile() unconditionally calls FlushBuffer(false, true) even during app shutdown when the part-file write thread (CPartFileWriteThread) may have already been stopped. Flushing after the write thread exits can hang (waiting for the thread to process items it never will) or access freed state. eMuleAI v1.3 added a shutdown guard to skip the flush in this case.

Location

srchybrid/PartFile.cpp:287-298:

CPartFile::~CPartFile()
{
    // Barry - Ensure all buffered data is written
    if ((HANDLE)m_hpartfile != INVALID_HANDLE_VALUE) {
        // commit file and directory entry
        FlushBuffer(false, true);    // ← unconditional, even after write thread exits
        CPartFileWriteThread::RemFile(this);
        m_hpartfile.Close();
        SavePartFile();
    } else
        CPartFileWriteThread::RemFile(this);

During normal eMule shutdown, destruction order is: 1. CPartFileWriteThread is stopped (thread exits) 2. CDownloadQueue is destroyed, which destroys all CPartFile objects 3. Each CPartFile::~CPartFile() calls FlushBuffer — but the write thread is gone

Problem

If FlushBuffer posts work to the write thread queue and waits for it to be processed, but the write thread is no longer running, the wait can block forever (deadlock) or the thread handle is invalid (crash). Even without blocking, accessing the write thread's state after it has exited is unsafe.

Fix

Check whether the write thread is still running before flushing:

CPartFile::~CPartFile()
{
    if ((HANDLE)m_hpartfile != INVALID_HANDLE_VALUE) {
        // Skip flush during shutdown if the write thread is already gone
        const CPartFileWriteThread* pThread = theApp.m_pPartFileWriteThread;
        const bool bShutdownWithoutWriteThread =
            theApp.IsClosing() && (!pThread || !pThread->IsRunning());
        if (!bShutdownWithoutWriteThread)
            FlushBuffer(false, true);
        CPartFileWriteThread::RemFile(this);
        m_hpartfile.Close();
        SavePartFile();
    } else
        CPartFileWriteThread::RemFile(this);

theApp.IsClosing() is already available. CPartFileWriteThread::IsRunning() returns whether the thread is alive. This makes the flush conditional: always flushes during normal operation, skips during shutdown after the write thread has stopped.

Acceptance Criteria

  • [x] CPartFile::~CPartFile() checks theApp.IsClosing() && !writeThread->IsRunning() before FlushBuffer
  • [ ] Clean shutdown with large download queues does not hang in destructor (evidence pending while testing remains paused)
  • [x] Normal (non-shutdown) destruction still flushes correctly

Current Evidence

  • 2026-05-13 decision: keep this item open as code fixed, evidence pending. The eMuleAI shutdown guard has already been ported, and current main is stricter than the reference because it also avoids saving .part.met when dirty buffered part data could not be flushed during shutdown. Do not run the remaining large-queue shutdown proof while the operator testing pause remains active.
  • CPartFile::FlushBufferedDataForShutdown() uses PartFilePersistenceSeams::ShouldFlushPartFileOnDestroy() before the first shutdown flush attempt.
  • Native regression coverage: Part-file persistence seam keeps the non-shutdown flush path intact and Part-file persistence seam skips destructor flushes only during shutdown after the write thread is gone.
  • Validation:
  • python -m emule_workspace validate
  • python -m emule_workspace build app --config Debug --platform x64
  • python -m emule_workspace test all --config Debug --platform x64
  • Latest Debug x64 evidence: app build passed across main, CFG, community, broadband, and tracing-harness; native parity passed 489/489 and web API passed 64/64. Live-diff completed with expected main-only seam mismatch warnings.
  • No fresh large-queue live shutdown stress run has been captured after 75a92c2; keep that manual acceptance item open until an isolated shutdown probe records the behavior.