Skip to content

Run FwDataFactory shutdown synchronously in StopAsync - #2327

Merged
myieye merged 1 commit into
developfrom
fix/fwdata-factory-stopasync-sync
Jun 3, 2026
Merged

Run FwDataFactory shutdown synchronously in StopAsync#2327
myieye merged 1 commit into
developfrom
fix/fwdata-factory-stopasync-sync

Conversation

@myieye

@myieye myieye commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Companion to sillsdev/libpalaso#1504, which catches the AbandonedMutexException itself. This PR removes a small but real abandonment-risk amplifier on the FwLite side: the unnecessary Task.Run inside FwDataFactory.StopAsync().

What

 public Task StopAsync(CancellationToken cancellationToken)
 {
     _shuttingDown = true;
-    return Task.Run(Dispose, cancellationToken);
+    Dispose();
+    return Task.CompletedTask;
 }

Why

  • IHostedService.StopAsync is the host's contract to do shutdown work, and the host awaits the returned Task before terminating the process. Offloading Dispose() to a threadpool task gains nothing — StopAsync is not on a hot path.
  • The previous form passed cancellationToken to Task.Run, which only cancels the scheduling of Dispose, not an in-flight Dispose. If StopAsync was 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 libpalaso GlobalMutex holds inside them.
  • Running Dispose() synchronously on the StopAsync caller's thread keeps shutdown deterministic. Exceptions from Dispose() now propagate synchronously to the host instead of being captured in a Task that nobody awaits the result of.
  • LcmCache.Dispose() (via WritingSystemManager.Save → libpalaso GlobalMutex.Lock) is sensitive to the threading model — System.Threading.Mutex has 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

  • The Task.Run(lcmCache.Dispose) in CloseProjectAsync is kept intentionally. That path is reachable from MAUI UI-thread callers (the "open in FieldWorks" handoff); removing the Task.Run there would freeze the UI during Dispose. The threadpool offload is correct in that location.
  • The OnLcmProjectCacheEviction callback fires on the MemoryCache scanner 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

  • Low. Behavior change is "wait synchronously" instead of "wait via Task.Run." Callers of StopAsync were already awaiting the returned Task; they now observe immediate completion after Dispose() returns. No public API change.
  • The cancellationToken parameter is unused after this change — same as the existing StartAsync above it. Kept for IHostedService interface compatibility.

Tests

FwDataMiniLcmBridge.Tests runs 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

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>
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d136e6f7-56da-4f57-8b9e-b55a706f4b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 79a9816 and 7f09944.

📒 Files selected for processing (1)
  • backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs

📝 Walkthrough

Walkthrough

FwDataFactory.StopAsync is simplified to synchronously call Dispose() and return a completed task, eliminating asynchronous task scheduling during shutdown.

Changes

Synchronous disposal on shutdown

Layer / File(s) Summary
Synchronous disposal in StopAsync
backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs
StopAsync marks shutdown as started, calls Dispose() directly, and returns Task.CompletedTask instead of queuing disposal via Task.Run(Dispose, cancellationToken).

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

Suggested reviewers

  • hahn-kev
  • imnasnainaec

Poem

🐰 A simpler path to rest,
No more queueing on the task stack—
Dispose now goes direct,
Shutdown's synchronous tack,
Cleaner paths, no race attack! 🎯

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fwdata-factory-stopasync-sync

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label Jun 2, 2026
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

UI unit Tests

  1 files  ±0   59 suites  ±0   30s ⏱️ +3s
177 tests ±0  177 ✅ ±0  0 💤 ±0  0 ❌ ±0 
246 runs  ±0  246 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 7f09944. ± Comparison against base commit 79a9816.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

C# FwHeadless Unit Tests

48 tests  ±0   48 ✅ ±0   18s ⏱️ +3s
 5 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit 7f09944. ± Comparison against base commit 79a9816.

@argos-ci

argos-ci Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 2, 2026, 1:23 PM

@myieye
myieye marked this pull request as ready for review June 3, 2026 11:36
@myieye
myieye merged commit 6305062 into develop Jun 3, 2026
19 of 20 checks passed
@myieye
myieye deleted the fix/fwdata-factory-stopasync-sync branch June 3, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant