Fix runtime data races, memory leak, and shutdown safety - #1429
Fix runtime data races, memory leak, and shutdown safety#1429bmehta001 wants to merge 86 commits into
Conversation
- HttpClient_Apple: scope Cancel() to m_dataTask only instead of blanket-cancelling every task on the shared session. Fix torn read on m_requests.empty() in CancelAllRequests spin loop. - HttpClientManager: fix torn read on m_httpCallbacks.empty() in cancelAllRequests spin loop — read under lock. - HttpResponseDecoder: add missing delete ctx->httpResponse before nullptr in Abort and RetryNetwork paths (memory leak). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Only delete queued tasks after successful join (not after detach, where the thread may still access them — undefined behavior) - Replace catch(...) with std::system_error and std::exception handlers that log error code and message - Log pending queue sizes in both join and detach paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both variables are read and written from different threads during normal upload scheduling. Declare as std::atomic to eliminate data races per the C++ memory model. Add .load() for variadic LOG_TRACE calls. Add comment explaining why unlocked stores in uploadAsync are safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove LOG_TRACE from Logger destructor — it triggers a crash on iOS simulator when the recursive_mutex used by logging has already been destroyed during static destruction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3f0289c to
de46cb2
Compare
Reject new worker-thread tasks once shutdown starts so queue cleanup cannot race with late producers, and move the TPM scheduled-upload state back under a single mutex so latency/next-upload decisions stay consistent without mixed atomic and mutex access. Files changed: - lib/pal/WorkerThread.cpp - lib/tpm/TransmissionPolicyManager.cpp - lib/tpm/TransmissionPolicyManager.hpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR targets runtime correctness in the SDK by addressing thread-safety issues, shutdown safety, and a per-request memory leak in HTTP response handling.
Changes:
- Tighten HTTP request cancellation scoping and fix torn reads in request/callback tracking loops.
- Fix a
SimpleHttpResponseleak on aborted/network-failure decode paths. - Rework worker-thread shutdown behavior to avoid unsafe queue cleanup after
detach()and improve error logging. - Refactor
TransmissionPolicyManagerscheduling state synchronization (mutex + newcancelUploadTaskLocked()helper).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/tpm/TransmissionPolicyManager.hpp | Changes upload-scheduling state fields and adds a locked cancellation helper declaration. |
| lib/tpm/TransmissionPolicyManager.cpp | Moves upload-scheduling state access under a mutex and adjusts cancellation/scheduling flow. |
| lib/pal/WorkerThread.cpp | Makes shutdown/join behavior safer and improves exception handling/logging during join/detach. |
| lib/http/HttpResponseDecoder.cpp | Deletes ctx->httpResponse on Abort/RetryNetwork paths to prevent leaks. |
| lib/http/HttpClient_Apple.mm | Limits cancellation to the instance’s task and fixes a torn read in the shutdown wait loop. |
| lib/http/HttpClientManager.cpp | Fixes a torn read in the shutdown wait loop by locking around empty-check. |
| lib/api/Logger.cpp | Removes destructor logging to avoid iOS static-destruction-order crash. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Keep the scheduled-upload state mutex-based, but stop holding m_scheduledUploadMutex across DeferredCallbackHandle::Cancel so shutdown and pause paths do not block uploadAsync behind the same lock. While touching the path, use std::chrono::milliseconds for the bandwidth-controller reschedule call so ENABLE_BW_CONTROLLER builds cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Keep forced upload scheduling atomic around no-wait cancellation and preserve HTTP responses until downstream abort/network-failure handlers finish. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When scheduleUpload is called with force=true (or zero delay) and the previously scheduled upload task is currently executing on the worker, the no-wait cancel returns false and m_isUploadScheduled stays set. The existing m_isUploadScheduled check then skipped scheduling a new task, silently dropping the requested latency for force-scheduled profile changes. Propagate the requested latency to m_runningLatency under the same mutex when this race occurs. uploadAsync re-reads m_runningLatency inside its own LOCKGUARD, so a task that hasn't yet entered that critical section will pick up the new latency. If uploadAsync has already cleared m_isUploadScheduled (past its LOCKGUARD), the existing fallthrough at line 184 schedules a fresh task with the new latency. Add a regression test using a fake dispatcher whose Cancel always returns false, asserting that a force-scheduled call updates m_runningLatency without enqueueing a duplicate task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the existing LOCKGUARD helper because scheduled upload cancellation does not need movable lock ownership. Consolidate the duplicated Issue 388 cancellation note so the PR keeps the remaining limitation documented without repeating the same TODO. Files changed: - lib/tpm/TransmissionPolicyManager.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace tight current-time assertions with a direct comparison against the original delayed schedule time. This keeps coverage for the forced immediate upload race while reducing timing sensitivity in CI. Files changed: - tests/unittests/TransmissionPolicyManagerTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the existing Issue 388 wording in the remaining cancellation comment while keeping the duplicated helper comment removed. Files changed: - lib/tpm/TransmissionPolicyManager.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix printf-style logging arguments for scheduled upload delays and queued worker task pointers. Ensure the blocking cancel test releases the dispatcher before failing so async futures cannot hang the test runner. Files changed: - lib/pal/WorkerThread.cpp - lib/tpm/TransmissionPolicyManager.cpp - tests/unittests/TransmissionPolicyManagerTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4178021 to
d357260
Compare
Keep the pending-flush state update synchronized after the flush lock is unwound by an exception. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Ensure vcpkg-built Apple libraries match the consumer deployment target and avoid linker warnings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5f341bc5-f8ae-4259-b03b-8eeb87c06837
Propagate the resolved iOS sysroot to embedding builds and keep Apple vendored targets compatible with strict warning settings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5f341bc5-f8ae-4257-b03b-8eeb87c06837
Remove legacy Apple architecture, platform, and deployment-target inputs so standalone scripts and embedding consumers share CMAKE_OSX_* configuration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5f341bc5-f8ae-4257-b03b-8eeb87c06837
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
…mehta/runtime-fixes
Ensure flush completion is signaled when record recovery throws, prevent activity cleanup exceptions from terminating teardown, and make worker task state race-free. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Integrate the batched SQLite transaction path and preserve exception-safe flush recovery for PR microsoft#1429. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Prevent PauseGuard and other teardown destructors from terminating the process when activity cleanup encounters a mutex or system error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
tests/unittests/OfflineStorageTests.cpp:13
- This test file now uses std::find and std::vector (NoopTaskDispatcher) but does not include or . Relying on transitive includes is brittle and can break builds when headers change; add the direct includes here.
| /* Can't recursively wait on completion of our own thread */ | ||
| if (m_hThread.get_id() != std::this_thread::get_id()) | ||
| { | ||
| if (waitTime > 0 && m_execution_mutex.try_lock_for(std::chrono::milliseconds(waitTime))) | ||
| { |
Ensure the offline storage unit tests do not rely on transitive includes.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (2)
lib/offline/OfflineStorageHandler.cpp:380
- LOG_INFO uses printf-style varargs; for a "%p" format the argument must be a void*. Passing a MAT::Task* here is undefined behavior. Cast the pointer to void* so the format/type match.
LOG_INFO("Requested Flush (%p)", m_flushHandle.GetTask());
lib/offline/OfflineStorageHandler.cpp:121
- LOG_INFO uses printf-style varargs; for a "%p" format the argument must be a void*. Passing a MAT::Task* here is undefined behavior. Cast the pointer to void* so the format/type match consistently (similar to the WorkerThread logging fixes).
This issue also appears on line 380 of the same file.
LOG_INFO("Waiting for pending Flush (%p) to complete...", m_flushHandle.GetTask());
Prevent the transaction destructor from committing a partial batch after an exception, so Flush can safely recover the entire drained batch without duplicate persisted records.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Adopt main's CMake preset and dependency architecture while preserving the runtime, storage, and teardown fixes on this branch. Resolve the iOS deployment and bundled dependency conflicts in favor of the current main build approach.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
NSURLSession cancellation callbacks may provide no HTTP response. Avoid dereferencing the null response while preserving the aborted result so teardown can complete safely.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Preserve request lifetime until the asynchronous NSURLSession completion callback has finished, preventing teardown use-after-free and callback drain deadlocks. Also pass task pointers safely to variadic logging. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/offline/OfflineStorage_SQLite.cpp:371
- In the batched StoreRecords() path, insertRecordUnsafe() increments m_DbSizeEstimate before the transaction is committed. If an exception occurs during the loop, the transaction is marked for rollback and rethrown, but m_DbSizeEstimate is not reverted, leaving the size estimate permanently inflated (which can trigger incorrect storage-full notifications/resizes).
catch (...)
{
#ifdef ENABLE_LOCKING
// DbTransaction commits on destruction by default for legacy
// callers. An exception during a batch must explicitly roll
Restore the size estimate when a batched insert transaction rolls back, and make the release performance test measure the batched StoreRecords path instead of timing 1,000 individual transactions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2bfc77f4-1a25-439f-8552-16750895413b
Fixes runtime thread-safety, shutdown-safety, response-lifetime, and resource-leak issues that affect normal SDK operation. Split out from #1415 per reviewer request so runtime behavior changes stay separate from CI/build/test fixes.
Fix HTTP handle cleanup
HttpClient_WinInet.cpp
m_hWinInetSessionwas closed only inside theif (m_hWinInetRequest != nullptr)block, so ifHttpOpenRequestAfailed afterInternetConnectAsucceeded (session set, request null) the session handle leaked.Fix HTTP response lifetime on abort/network-failure paths
HttpResponseDecoder.cpp
ctx->httpResponsethroughrequestAborted(ctx)andtemporaryNetworkFailure(ctx)so downstream storage/statistics handlers can still read status and headers.EventsUploadContext, whoseclear()path deletes the response.HttpResponseDecoderTests.cpp
Fix WorkerThread shutdown safety, dropped-task handles, and task leak
TaskDispatcher.hpp
scheduleTask(...)return a no-opDeferredCallbackHandlewhen the dispatcher synchronously drops/deletes the task duringQueue()(for example on a shutdown-drop path), instead of returning a handle that dangles.Queue().WorkerThread.cpp
m_shuttingDown): lateQueue()calls are rejected (and the task deleted) once teardown starts.Join()the owning thread deletes them; on the self-dispose path the worker drains and deletes its own remaining tasks before exiting, so neither path leaks.TaskDispatcherCAPITests.cpp
scheduleTask(...)returns a no-op handle when a dispatcher drops the task synchronously duringQueue().PalTests.cpp
Join()returns a no-op handle, and that releasing the last worker-thread reference from a task on that same worker thread does not use-after-free.Fix Flush teardown deadlock
OfflineStorageHandler.cpp
Flush()could early-return (whenStartActivity()fails during teardown) without posting flush completion, soWaitForFlush()blocked forever. Signal completion on the early-return path so teardown cannot deadlock.Flush()also pairedStartActivity()/EndActivity()manually (StartActivity()at the top,EndActivity()on the last line) with no exception safety in between.StoreRecords(), the optional checkpointFlush(), andIOfflineStorageObserver::OnStorageRecordsSaved()are all real throw surfaces (disk I/O, a full/locked DB, or an observer implementation); if any of them threw,EndActivity()was skipped and the pause-activity count was permanently leaked, deadlocking every laterFlushAndTeardown()'sPauseActivity()+WaitPause(). This reproduced as a live macOS deadlock onmain. AddedActivityGuard, an RAII wrapper matching the existing safe pattern already used byPauseGuard(TransmissionPolicyManager.cpp) andActiveLoggerCall(Logger.cpp): its destructor callsEndActivity()on every exit path, including exception unwinding.BasicFuncTests.cpp
CFG_INT_MAX_TEARDOWN_TIME = 0, large payloads against the slow endpoint) and asserts shutdown completes cleanly; it also asserts the/slow/endpoint rewrite actually happened so the coverage can’t silently lapse.Fix static-destruction-order crashes in the two process-wide singletons
LogManagerFactory.hpp / PAL.cpp
LogManagerFactory::instance()andPAL::GetPAL()were ordinary function-local statics. Their destruction order relative toLogManagerProvider::Release()andPAL::shutdown()(both invoked during process teardown) is unspecified —PALin particular is constructed lazily on first use rather than at a fixed point, so whether it outlives the teardown call that needs it depends on runtime timing, not source order.EXC_BAD_ACCESScrashes on macOS-arm64 at process exit —LogManagerFactory's registries andPAL'sISystemInformationmember were sometimes already destroyed by the time teardown code tried to use them — and worked around it in their vendored copy of this SDK by leaking both singletons.static T& x = *new T();deliberately never destroys the object, so it stays valid for the rest of the process regardless of teardown timing. Both objects are small and process-lifetime singletons (one instance ever), andPAL::shutdown()/Release()already perform the real resource teardown explicitly, so this only removes the destructor-ordering hazard, not a resource leak in the ordinary sense.Make TransmissionPolicyManager scheduling consistently mutex-guarded
TransmissionPolicyManager.cpp / .hpp
m_isUploadScheduled,m_runningLatency, andm_scheduledUploadTimeconsistently withm_scheduledUploadMutex.m_scheduledUploadMutexacross potentially blocking cancellation during stop/shutdown.std::chrono::millisecondsvalue in the bandwidth-controller reschedule path, and caststd::chronocounts tolong longin the%lldLOG_TRACEcalls (the rep islongon LP64, a-Wformatmismatch in logging-enabled builds).TransmissionPolicyManagerTests.cpp
Fix Logger static-destruction-order crash
Logger.cpp
Logger::~Logger()because it can run after logging infrastructure has already been destroyed, causing crashes during static teardown.Known parity gap (separate repo, follow-up):
AIHttpResponseDecoder::handleDecodein thelib/modulessubmodule (lib/modules/azmon/AIHttpResponseDecoder.cpp:105,118) still setsctx->httpResponse = nullptr;on the aborted/failure paths — the same response-lifetime leak fixed here inlib/http/HttpResponseDecoder.cpp. It lives in a different repository (thelib/modulessubmodule), so it must be fixed there and pulled in via a submodule bump; it is out of scope for this PR.