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.metsaving 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 validatepython -m emule_workspace build app --variant main --config Debug --platform x64 --build-output-mode ErrorsOnlypython -m emule_workspace build app --variant main --config Release --platform x64 --build-output-mode ErrorsOnlypython -m emule_workspace build tests --config Debug --platform x64 --build-output-mode ErrorsOnlypython -m emule_workspace build tests --config Release --platform x64 --build-output-mode ErrorsOnlypython -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 ErrorsOnlypython -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()checkstheApp.IsClosing() && !writeThread->IsRunning()beforeFlushBuffer - [ ] 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
mainis stricter than the reference because it also avoids saving.part.metwhen 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()usesPartFilePersistenceSeams::ShouldFlushPartFileOnDestroy()before the first shutdown flush attempt.- Native regression coverage:
Part-file persistence seam keeps the non-shutdown flush path intactandPart-file persistence seam skips destructor flushes only during shutdown after the write thread is gone. - Validation:
python -m emule_workspace validatepython -m emule_workspace build app --config Debug --platform x64python -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.