Fix publish queue lost wake-up - #4017
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes a lost wake-up race in SessionPublishQueue that could leave Publish requests stranded and stall monitored-item notifications.
Changes:
- Makes the “ready subscription” check and Publish-request enqueue atomic under
m_lock(removing the split publish lock). - Synchronizes Publish completion/requeue/timer assignment on
m_lockand movesSessionClosed()callbacks outside the queue lock. - Adds a regression test intended to deterministically reproduce the lost wake-up window.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| Tests/Opc.Ua.Server.Tests/SessionPublishQueueRaceTests.cs | Adds a race/regression test using private-lock coordination to reproduce the lost wake-up. |
| Libraries/Opc.Ua.Server/Subscription/SessionPublishQueue.cs | Consolidates locking on m_lock, removes m_subscriptionPublishLock, and adjusts close/completion paths accordingly. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master378 #4017 +/- ##
=============================================
- Coverage 60.15% 60.14% -0.02%
=============================================
Files 378 378
Lines 79069 79071 +2
Branches 13836 13838 +2
=============================================
- Hits 47567 47555 -12
- Misses 27084 27093 +9
- Partials 4418 4423 +5
🚀 New features to boost your workflow:
|
|
@marcschier I see the Issue here not so much in the locking stategy, but more in that area. UA-.NETStandard/Libraries/Opc.Ua.Server/Subscription/SessionPublishQueue.cs Lines 460 to 500 in 52294d2 The subscription is assigned ReadyToPublish = True unconditionally, even if no request is available. The Publish cycle skip subscriptions that have ReadyToPublish in the timed PublishTimerExpired. UA-.NETStandard/Libraries/Opc.Ua.Server/Subscription/SessionPublishQueue.cs Lines 410 to 450 in 52294d2 I think to remove that skipping of Subscriptions with Status ReadyToPublish would resolve the Issue without any change to the locking behaviour. Also the described behaviour would be only causing a single publish Request to time out (which is not good, but way less severe than a subscription that is not publishing at all). All later publish requests would be able to publish that subscription again. |
| // do nothing if subscription has already been flagged as available. | ||
| if (subscription.ReadyToPublish) | ||
| { | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
This needs to be re-added
# Description Fixes publish request assignment ordering in `SessionPublishQueue`, following up on the review of the proposed #3997 follow-up. Two related problems were identified during review: 1. Retrying an already-ready Subscription from `PublishTimerExpired()` reset its `Timestamp` when no Publish request was available, which broke oldest-first selection among equal-priority Subscriptions. Master already uses a single lock for the ready check and the Publish-request enqueue, so the original lost-wakeup window does not require removing the `ReadyToPublish` guard. 2. `PublishTimerExpired()` bypassed the selection policy used by `PublishAsync()`. It iterated `m_queuedSubscriptions` — a `ConcurrentDictionary` with no ordering guarantee — and handed each newly notifying Subscription straight to the first waiting request, so `Priority` and `Timestamp` were ignored whenever several Subscriptions became ready in the same timer tick. This PR now: - Keeps the `ReadyToPublish` timer early exit so already-ready Subscriptions retain their timestamps. - Flags all notifying Subscriptions as available first and then drains the waiting requests through `GetSubscriptionToPublish()`, so requests are served highest priority and longest waiting first regardless of dictionary iteration order. - Routes `PublishCompleted(..., moreNotifications: true)` through the same path instead of assigning the Subscription directly. - Replaces `AssignSubscriptionToRequest()` with `AssignSubscriptionsToRequests()` / `TryAssignSubscriptionToRequest()`. The latter also skips a request whose task completed (cancelled or timed out) between the `IsCompleted` check and `TrySetResult`, instead of losing the Subscription. - Adds deterministic regression coverage for both behaviours. ## Related Issues - Follow-up to #3997 - Review correction to #4017 - Tracks the equivalent `master378` correction in #4119 ## Testing - All 26 `SessionPublishQueueTests` pass on net10.0 and net48. - `PublishTimerAssignsWaitingRequestToHighestPrioritySubscriptionAsync` and `PublishTimerPreservesReadySubscriptionTimestampOrderAsync` both fail without the corresponding source change. - The full `Opc.Ua.Server.Tests` project shows no new failures on net10.0; the single `ServerFluentApiHostingTests.ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync` failure reproduces unchanged on the branch without these edits. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 181ba7ea-c72f-42d7-a737-00a3faadaa17
Description
Fixes a server-side lost wake-up in
SessionPublishQueuethat can permanently stop monitored-item notifications while the Session and SecureChannel remain healthy.In 1.5.378,
PublishAsyncchecked for a ready Subscription underm_subscriptionPublishLockand then queued the Publish request underm_lock. A Subscription could become ready between those operations, be markedReadyToPublishbecause no request was queued yet, and then leave the newly queued request stranded. Later timer ticks skipped assignment because the Subscription was already marked ready.This change backports the relevant
SessionPublishQueuecorrection from #3611 and incorporates the review follow-up:m_lock.PublishCompleted,Requeue, and timer-driven assignment with the same lock.m_subscriptionPublishLock.SessionClosedcallbacks outside the queue lock.Related Issues
Testing
SessionPublishQueueRaceTestsfixture passes on net472, net48, net8.0, net9.0, and net10.0.UA.slnxon net10.0 and net48 with zero failures.