Run FwDataFactory shutdown synchronously in StopAsync - #2327
Conversation
Wrapping Dispose() in Task.Run inside StopAsync served no purpose: IHostedService.StopAsync is the host's contract to do shutdown work synchronously-as-needed, and the host awaits the returned Task before proceeding to terminate the process. The Task.Run only added a threadpool hop and made the CancellationToken's meaning ambiguous (the token cancels the Task scheduling, not the in-flight Dispose, so the caches could end up undisposed if shutdown is already past its timeout). Running Dispose synchronously on the StopAsync caller's thread keeps shutdown deterministic and tightens the threading model that .NET Mutex (in libpalaso's GlobalMutex) is sensitive to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesSynchronous disposal on shutdown
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~3 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Companion to sillsdev/libpalaso#1504, which catches the
AbandonedMutexExceptionitself. This PR removes a small but real abandonment-risk amplifier on the FwLite side: the unnecessaryTask.RuninsideFwDataFactory.StopAsync().What
public Task StopAsync(CancellationToken cancellationToken) { _shuttingDown = true; - return Task.Run(Dispose, cancellationToken); + Dispose(); + return Task.CompletedTask; }Why
IHostedService.StopAsyncis the host's contract to do shutdown work, and the host awaits the returnedTaskbefore terminating the process. OffloadingDispose()to a threadpool task gains nothing —StopAsyncis not on a hot path.cancellationTokentoTask.Run, which only cancels the scheduling ofDispose, not an in-flightDispose. IfStopAsyncwas called after the host's shutdown timeout had already elapsed (cancellationToken pre-signaled), the caches would never be disposed at all — the worst possible state for any file-locks or libpalasoGlobalMutexholds inside them.Dispose()synchronously on the StopAsync caller's thread keeps shutdown deterministic. Exceptions fromDispose()now propagate synchronously to the host instead of being captured in aTaskthat nobody awaits the result of.LcmCache.Dispose()(viaWritingSystemManager.Save→ libpalasoGlobalMutex.Lock) is sensitive to the threading model —System.Threading.Mutexhas thread affinity. Reducing the number of thread hops on shutdown reduces the surface area where a process exit can race against an in-flight Dispose.What this does NOT fix on its own
Task.Run(lcmCache.Dispose)inCloseProjectAsyncis kept intentionally. That path is reachable from MAUI UI-thread callers (the "open in FieldWorks" handoff); removing theTask.Runthere would freeze the UI during Dispose. The threadpool offload is correct in that location.OnLcmProjectCacheEvictioncallback fires on theMemoryCachescanner thread and disposes the cache out from under whatever request is still using it. That's a use-after-dispose risk separate from mutex abandonment and needs a leasing/refcount scheme (the comment at lines 77–79 already flags it). Not in scope for this PR.Risk
StopAsyncwere already awaiting the returned Task; they now observe immediate completion afterDispose()returns. No public API change.cancellationTokenparameter is unused after this change — same as the existingStartAsyncabove it. Kept forIHostedServiceinterface compatibility.Tests
FwDataMiniLcmBridge.Testsruns 761/762 green (1 unrelated skip). No new test added: the change is a removal of a wrapper around an already-tested method.🤖 Generated with Claude Code