Skip to content

Thread-safety fixes (#53, #26) and .NET 10 build-warning cleanup (#46)#54

Merged
rwmcintosh merged 6 commits into
mainfrom
thread-safety-and-build-warning-fixes
Jun 17, 2026
Merged

Thread-safety fixes (#53, #26) and .NET 10 build-warning cleanup (#46)#54
rwmcintosh merged 6 commits into
mainfrom
thread-safety-and-build-warning-fixes

Conversation

@rwmcintosh

@rwmcintosh rwmcintosh commented Jun 16, 2026

Copy link
Copy Markdown
Member

Bundles three independent OSPSuite.Utility fixes. Each is a separate commit.

#53EventPublisher.PublishEvent thread-safety

PublishEvent pruned _listeners under a lock but snapshotted it (.ToList()) outside the lock, so a concurrent Add/Remove/prune could corrupt the enumeration (two concurrent publishers are enough). The fix takes the snapshot inside the lock and dispatches via _context.Send outside the lock, so handlers never run while the lock is held. _context.Send is intentionally left as-is — the blocking-marshal concern noted in the issue is explicitly not the core bug.

#26BuilderRepository thread-safety

BuilderFor did a check-then-act across two cache calls (ContainsAdd); two threads could both miss and both Add the same key, throwing An item with the key '...' has already been added (root cause of PK-Sim #1260 under parallel qualification export). The fix resolves-and-caches under a private lock so the sequence is atomic, and guards Start() with double-checked locking (_isInitialized is now volatile). No change to Cache is needed — the repository's cache is private and now only ever touched single-threaded.

#46 — .NET 10 build warnings

  • SYSLIB0045: HashAlgorithm.Create("MD5")MD5.Create().
  • SYSLIB0051: removed the dead legacy serialization constructor from OSPSuiteException. Verified by a tree-wide search that nothing in PK-Sim/MoBi/OSPSuite.Core (all on .NET 10) derives from it, so removal is safe — no suppression needed.
  • Deprecated PackageIconUrl: replaced with PackageIcon and the canonical OSP logo.png is now shipped in the package (dotnet pack emits <icon> with no NU5048).
  • Targeting .NET 10.0 … in Visual Studio 2022 17.14 is not supported: CI now builds with dotnet build instead of msbuild (dropped the microsoft/setup-msbuild step) in both workflows.

Testing

  • Full suite green: 313/313.
  • Both concurrency tests were confirmed to fail on the pre-fix code (with the exact production exceptions) and pass after the fix.
  • dotnet build of the library reports 0 warnings; dotnet pack produces a package carrying logo.png with no packaging warnings.

Fixes #53
Fixes #26
Fixes #46

Summary by CodeRabbit

  • Improvements

    • Enhanced thread-safety for concurrent event publishing and component resolution to prevent race conditions
  • Chores

    • Updated build workflows to use .NET SDK instead of MSBuild
    • Updated NuGet package metadata with new icon asset
    • Removed deprecated serialization support from core exception handling

Snapshot the listener list inside the lock before dispatching, so a concurrent Add/Remove/prune can no longer corrupt the enumeration. Adds a multi-threaded publish/add/remove stress test.
Resolve and cache under a lock so the Contains-then-Add in BuilderFor is atomic, and guard Start with double-checked locking. Fixes the 'An item with the key ... has already been added' crash under concurrent BuilderFor calls. Adds a multi-threaded reproduction test.
- SYSLIB0045: use MD5.Create() instead of HashAlgorithm.Create("MD5")
- SYSLIB0051: remove the dead legacy serialization constructor from OSPSuiteException (nothing in the suite derives from it)
- Replace deprecated PackageIconUrl with PackageIcon and ship logo.png in the package
- Build with 'dotnet build' instead of 'msbuild' in CI to clear the VS 2022 17.14 net10 targeting warning
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rwmcintosh, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 29 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2229d4e-e3b3-4948-8a17-48fa890f32cf

📥 Commits

Reviewing files that changed from the base of the PR and between c22b69d and f8d1e4c.

📒 Files selected for processing (3)
  • src/OSPSuite.Utility/BuilderRepository.cs
  • src/OSPSuite.Utility/Events/EventPublisher.cs
  • src/OSPSuite.Utility/FileHelper.cs
📝 Walkthrough

Walkthrough

Two thread-safety bugs are fixed: BuilderRepository wraps cache lookup and initialization in lock(_locker) with a volatile flag, and EventPublisher.PublishEvent now prunes and snapshots the listener list within a single lock acquisition before dispatching outside it. Separately, obsolete serialization and MD5 APIs are removed, NuGet icon metadata is modernized, and CI workflows switch from MSBuild to dotnet build.

Changes

Thread-safety fixes for BuilderRepository and EventPublisher

Layer / File(s) Summary
BuilderRepository lock-based initialization and resolution
src/OSPSuite.Utility/BuilderRepository.cs, tests/OSPSuite.Utility.Tests/BuilderRepositorySpecs.cs
Introduces _locker and volatile _isInitialized; wraps BuilderFor cache access and Start initialization in lock(_locker). Concurrency spec spawns synchronized threads calling BuilderFor for the same type and asserts no exceptions.
EventPublisher snapshot-under-lock dispatch
src/OSPSuite.Utility/Events/EventPublisher.cs, tests/OSPSuite.Utility.Tests/EventPublisherSpecs.cs
PublishEvent now prunes and snapshots _listeners inside a single lock, then dispatches outside it. Stress test runs concurrent publisher and mutator threads, asserting zero exceptions and full operation count.

Build warning fixes and CI migration

Layer / File(s) Summary
Obsolete API removals and NuGet icon update
src/OSPSuite.Utility/Exceptions/OSPSuiteException.cs, src/OSPSuite.Utility/FileHelper.cs, src/OSPSuite.Utility/OSPSuite.Utility.csproj
Removes the protected serialization constructor and its using directive from OSPSuiteException. Changes HashAlgorithm.Create("MD5") to MD5.Create(). Switches PackageIconUrl to PackageIcon with a packed logo.png file.
CI workflows migrated from MSBuild to dotnet build
.github/workflows/build-and-publish.yml, .github/workflows/build-pr.yml
Removes microsoft/setup-msbuild@v3 setup steps from both workflows and replaces the msbuild invocation with dotnet build, keeping the same version property and Debug configuration.

Sequence Diagram(s)

sequenceDiagram
  participant T1 as Publisher Thread
  participant T2 as Mutator Thread
  participant EP as EventPublisher
  participant LL as _listeners (List)

  T1->>EP: PublishEvent(event)
  T2->>EP: AddListener(newListener)
  EP->>LL: lock(_listeners): pruneReferences()
  EP->>LL: ToList() → snapshot
  note over EP,LL: lock released
  T2->>LL: lock(_listeners): _listeners.Add(newListener)
  T2->>EP: RemoveListener(newListener)
  T2->>LL: lock(_listeners): _listeners.Remove(newListener)
  loop over snapshot (no lock held)
    EP->>EP: cast Target as IListener<T> → Handle(event)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • msevestre
  • Yuri05

Poem

🐇 Hop hop, the locks are set in place,
No race condition can keep up my pace!
Dead references pruned before the snap,
MSBuild gone — closed that old gap.
The rabbit ships thread-safe code with glee,
Concurrent hops, exception-free! 🎉

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: thread-safety fixes for EventPublisher and BuilderRepository (issues #53 and #26) and .NET 10 build-warning cleanup (issue #46).
Linked Issues check ✅ Passed All linked issue requirements are met: EventPublisher snapshot is now inside the lock with concurrent testing; BuilderRepository uses atomic locking for resolve-and-cache with double-checked locking and concurrent testing; all four .NET 10 warnings are addressed.
Out of Scope Changes check ✅ Passed All changes directly address the three linked issues; no out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch thread-safety-and-build-warning-fixes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/OSPSuite.Utility/FileHelper.cs (1)

190-201: ⚠️ Potential issue | 🟡 Minor

Dispose the MD5 instance after hashing.

At Line 190, MD5.Create() returns a disposable object that is not disposed. With FileStreams properly wrapped in using statements in the same method, the hashAlgorithm should follow the same pattern to release unmanaged crypto resources. This becomes especially relevant when the method is called repeatedly.

Suggested fix
-         var hashAlgorithm = MD5.Create();
+         using var hashAlgorithm = MD5.Create();
          byte[] hashFile1;
          byte[] hashFile2;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/OSPSuite.Utility/FileHelper.cs` around lines 190 - 201, The MD5.Create()
call at the beginning returns a disposable object that is not being disposed,
which wastes unmanaged crypto resources, especially when the method is called
repeatedly. Wrap the hashAlgorithm variable in a using statement similar to how
the FileStream instances are already wrapped in the method. Move the hashFile1
and hashFile2 variable declarations inside or before the using block so they
remain accessible after the using statement closes, then place both the
ComputeHash calls for both files within the using block to ensure the MD5
instance is properly disposed after both hashes are computed.
🧹 Nitpick comments (1)
src/OSPSuite.Utility/Events/EventPublisher.cs (1)

25-34: 💤 Low value

Minor: Redundant reentrant lock acquisition.

pruneReferences() internally calls doWithinLock(), which acquires lock (_listeners) again while it's already held at line 26. C# locks are reentrant so this is safe, but it adds unnecessary overhead on each publish.

Consider extracting the inner pruning logic to a pruneReferencesCore() method that doesn't acquire the lock:

♻️ Optional refactor
 lock (_listeners)
 {
-    pruneReferences();
+    pruneReferencesCore();
     listeners = _listeners.ToList();
 }
 ...

-private void pruneReferences()
-{
-    doWithinLock(() =>
+private void pruneReferencesCore()
+{
+    for (var i = _listeners.Count - 1; i >= 0; i--)
     {
-        for (var i = _listeners.Count - 1; i >= 0; i--)
+        if (_listeners[i].Target == null)
         {
-            if (_listeners[i].Target == null)
-            {
-                _listeners.RemoveAt(i);
-            }
+            _listeners.RemoveAt(i);
         }
-    });
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/OSPSuite.Utility/Events/EventPublisher.cs` around lines 25 - 34, The
pruneReferences() method is being called while already holding the lock on
_listeners, but pruneReferences() internally calls doWithinLock() which acquires
the same lock again, causing redundant reentrant lock acquisition. Extract the
actual pruning logic from the pruneReferences() method into a new
pruneReferencesCore() method that does not acquire the lock, update
pruneReferences() to call doWithinLock() with pruneReferencesCore() inside, and
then change the call site in the lock block (around line 30) to call
pruneReferencesCore() directly instead of pruneReferences() since the lock is
already held.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/OSPSuite.Utility/FileHelper.cs`:
- Around line 190-201: The MD5.Create() call at the beginning returns a
disposable object that is not being disposed, which wastes unmanaged crypto
resources, especially when the method is called repeatedly. Wrap the
hashAlgorithm variable in a using statement similar to how the FileStream
instances are already wrapped in the method. Move the hashFile1 and hashFile2
variable declarations inside or before the using block so they remain accessible
after the using statement closes, then place both the ComputeHash calls for both
files within the using block to ensure the MD5 instance is properly disposed
after both hashes are computed.

---

Nitpick comments:
In `@src/OSPSuite.Utility/Events/EventPublisher.cs`:
- Around line 25-34: The pruneReferences() method is being called while already
holding the lock on _listeners, but pruneReferences() internally calls
doWithinLock() which acquires the same lock again, causing redundant reentrant
lock acquisition. Extract the actual pruning logic from the pruneReferences()
method into a new pruneReferencesCore() method that does not acquire the lock,
update pruneReferences() to call doWithinLock() with pruneReferencesCore()
inside, and then change the call site in the lock block (around line 30) to call
pruneReferencesCore() directly instead of pruneReferences() since the lock is
already held.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a00d20d6-3ab3-4293-9c4f-1b6c7cfe4118

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8463e and c22b69d.

⛔ Files ignored due to path filters (1)
  • logo.png is excluded by !**/*.png
📒 Files selected for processing (9)
  • .github/workflows/build-and-publish.yml
  • .github/workflows/build-pr.yml
  • src/OSPSuite.Utility/BuilderRepository.cs
  • src/OSPSuite.Utility/Events/EventPublisher.cs
  • src/OSPSuite.Utility/Exceptions/OSPSuiteException.cs
  • src/OSPSuite.Utility/FileHelper.cs
  • src/OSPSuite.Utility/OSPSuite.Utility.csproj
  • tests/OSPSuite.Utility.Tests/BuilderRepositorySpecs.cs
  • tests/OSPSuite.Utility.Tests/EventPublisherSpecs.cs
💤 Files with no reviewable changes (1)
  • src/OSPSuite.Utility/Exceptions/OSPSuiteException.cs

Wrap MD5.Create() in a using block so the crypto object is released after both file hashes are computed (addresses PR review).
@rwmcintosh rwmcintosh self-assigned this Jun 16, 2026
@rwmcintosh
rwmcintosh requested review from Yuri05 and msevestre June 16, 2026 18:36
Comment thread src/OSPSuite.Utility/Events/EventPublisher.cs
Comment thread src/OSPSuite.Utility/BuilderRepository.cs Outdated
pruneReferences is only called from PublishEvent, which now holds the lock on _listeners, so its own doWithinLock was a redundant reentrant acquire (addresses PR review).
@rwmcintosh
rwmcintosh requested a review from Yuri05 June 17, 2026 11:17
@rwmcintosh
rwmcintosh merged commit 7313f77 into main Jun 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EventPublisher.PublishEvent is not thread-safe: listener list enumerated outside the lock Build warnings BuilderRepository is not thread safe

2 participants