Fix #55: dispatch events by thread identity, not context-instance equality#57
Conversation
…ality EventPublisher.PublishEvent chose Send vs Post with `SynchronizationContext.Current == _context`. A single UI thread can own more than one SynchronizationContext instance, so on the UI thread the comparison is false and events are Posted (deferred) instead of Sent (inline). This surfaced as a splash-progress glitch at startup and a NullReferenceException at app close, when a ProjectClosedEvent handler ran after the UI had been torn down. Decide by thread identity instead: Send when publishing from the context's own thread, Post otherwise. Add an explicit-thread constructor for cases where the context targets a different thread than the one constructing the publisher (e.g. a splash screen on its own thread); the existing 2-arg constructor delegates to it with Thread.CurrentThread, so existing callers are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 54 minutes and 45 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 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 review availability. 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, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughEventPublisher now captures a construction thread and uses it to choose synchronous ChangesEventPublisher thread-aware dispatch
Sequence Diagram(s)sequenceDiagram
participant Thread as Thread.CurrentThread
participant EventPublisher
participant SynchronizationContext
participant Listener
Thread->>EventPublisher: PublishEvent<T>(event)
EventPublisher->>EventPublisher: compare current thread with captured thread
alt same thread
EventPublisher->>SynchronizationContext: Send(...)
else different thread
EventPublisher->>SynchronizationContext: Post(...)
end
SynchronizationContext->>Listener: invoke handlers
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
|
@coderabbitai are you able to see my license? |
|
Tip For best results, initiate chat on the files or code changes.
What I can help with is checking whether CodeRabbit appears to be active/configured correctly for this repository or PR. If you want, I can help troubleshoot that side. For the actual license status, you’d need to check your CodeRabbit account/org settings. |
msevestre
left a comment
There was a problem hiding this comment.
I don't get it. What are we doing here now compare to the other version? Too much texts to understandw hat's going on for me
|
You can have many instances of a synchronization context that point to the same thread. The decision to dispatch by send or post is not the captured context, but the captured thread. |
|
And the context instance can be changed on the thread, but its target will always be the UI thread. |
msevestre
left a comment
There was a problem hiding this comment.
I don't understand the thread stuff. EventPublisher is a singletong that will capture the thread once. Are we assuming that the process where event puvlisher is created is always the right one?
| { | ||
| _listeners = new List<WeakRef<IListener>>(); | ||
| _context = context; | ||
| _contextThread = contextThread; |
There was a problem hiding this comment.
event publisher is a singleton. That can't be good. We will capture the thread once. doenst it defeat the purpose?
There was a problem hiding this comment.
No because the thread is the UI thread. There should only be one. It is the same target thread that the SynchronizationContext.Current targets. We always want to know if the publishing thread is the UI thread. Comparing context doesn't tell us that because more than one context can target the UI thread.
The way I found this was that when I ran some sequence-important publishing, the Post was used which I did not expect. That's because in the Forms event handler (eg FormClosing in this case), the SyncrhonizationContext.Current instance had changed on the thread. It still targets the UI thread, just the instance had changed.
So what was happening was
- we capture the SynchronizationContext.Current when EventPublisher is resolved for the first time.
- at some point the SynchronizationContext.Current instance changes, maybe as a result of the event, or async task, or some other life-cycle.
- publish an event on the UI thread, but the SynchronizationContext.Current has changed so Post is used even though we are publishing from the UI thread.
What we really want to know is are we publishing from the UI thread.
There was a problem hiding this comment.
should we rename so that it actually says this ?uiThread vs contextThread
publishingFromContextThread => publishingFromUIThread?
There was a problem hiding this comment.
Done. Cleaned up the comment a bit so it's less confusing.
Yes, we are assuming that the first instantiation of event publisher is on the UI thread. Edit: Singleton - so it should be the only one. Although, I also found that SplashScreen has its own UI thread, and I had to create a special one that targets that thread so the progress bar was using Send instead of Post |
|
yeah good like this. I do think that some of the cokmments are just too much with claude lol. I don't need a book for every method |
I always have to tell him to stop narrating |
Picks up the EventPublisher thread-identity dispatch fix (Open-Systems-Pharmacology/OSPSuite.Utility#57). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
EventPublisher.PublishEventchoosesSend(synchronous, inline) vsPost(asynchronous, deferred) withSynchronizationContext.Current == _context— instance equality, as introduced in #56 (Fixes #55).A single UI thread can own more than one
SynchronizationContextinstance (e.g. a manually-created one and the one the WinForms message loop installs/copies), so on the UI threadCurrent != _contextand every UI-thread publish takes thePost(deferred) branch instead ofSend.Two consumer-side symptoms, both swallowed by
ExceptionManager.Execute(so only visible under a debugger):NullReferenceExceptionat app close: aProjectClosedEventis published and thenApplication.Exit()tears down the UI; the deferred handler runs after teardown.Fix
Decide by thread identity, not context instance:
(SynchronizationContext, Thread contextThread, IExceptionManager). The existing 2-arg constructor delegates to it withThread.CurrentThread— additive and backward-compatible. Use the explicit constructor when the context targets a thread other than the one constructing the publisher (e.g. a splash screen running on its own thread).Sendis chosen only when we are the context thread, so it always runs inline (never blocks or marshals); a different thread always takes the non-blockingPost.Tests
EventPublisherSpecsupdated to thread semantics, plus new cases:SynchronizationContextisCurrent→ stillSend(the regression this fixes);Send, off it →Post.Refs #55.
🤖 Generated with Claude Code
Summary by CodeRabbit