Repository navigation
[server] Remove utils.IsClosed[K] helper now unused - #19585
SarthakB11 wants to merge 8 commits into
Conversation
Three concurrency bugs in server/models/event_broadcast.go: 1. utils.IsClosed drains a queued event from a buffered, still-open channel via 'case <-ch: return true', then falsely reports closed. The Publish gate dropped the send. Events silently lost on bursty subscribers. 2. Subscribe stored clients by value in sync.Map and re-Store'd a local copy after appending to listeners. The *sync.Mutex was shared but the slice header was not, so two racing subscribers for the same id could last-writer-wins on Store, losing one subscriber. 3. Publish iterated listeners with no lock while unsubscribe closed the channel mid-iteration; concurrent unsubscribe + publish could send on a closed channel and panic. Fix: - Store *clients in the sync.Map so the mutex actually protects the slice header. Embed sync.Mutex directly (struct no longer copied). - In Publish, hold cc.mu and use a non-blocking select with default. Drops the IsClosed gate which is wrong for buffered channels. - In unsubscribe, rebuild listeners without ch under mu, release the lock, then close(ch). Publish either runs before the rebuild (sends safely under lock) or after (slice no longer contains ch), never racing the close. Diff keeps Broadcast.Subscribe / Broadcast.Publish public API identical. The five other utils.IsClosed callers carry the same drop-an-event bug; tracked as follow-up. Fixes meshery#17011 Signed-off-by: SarthakB11 <[email protected]>
…rtion - unsubscribe: only close(ch) if ch was actually removed from listeners. Calling unsubscribe twice now no-ops the second call instead of panicking on close-of-closed-channel. - unsubscribe: use ok-idiom on v.(*clients) for consistency with Publish. Both addressed via gemini-code-assist[bot] review on PR meshery#19572. Signed-off-by: SarthakB11 <[email protected]>
…ble-unsub Per @chaitanyamedidar review on PR meshery#19572. Three test functions in server/models/event_broadcast_test.go: - TestBroadcast_MultipleSubscribersSameID asserts two Subscribe calls for the same id both receive the published event. Regresses the value-copy clobber that PR meshery#17013 did not fix. - TestBroadcast_DoubleUnsubscribe asserts calling the returned unsubscribe twice is a no-op rather than a panic on close-of-closed channel. Regresses the gemini-flagged bug from this PR's first revision. - TestBroadcast_ConcurrentPublishUnsubscribe spins 50 subscribers each publishing 10 events plus a background publisher, intended for go test -race to catch any reintroduction of the send-on-closed-channel race or the listener-slice clobber. Signed-off-by: SarthakB11 <[email protected]>
- Switch test uuid import from google/uuid to gofrs/uuid (core.Uuid is an alias for gofrs/uuid.UUID, which is what the broadcaster Subscribe and Publish APIs accept). Fixes the golangci-lint-server typecheck failures introduced in 466533c. - Add doc comment on Publish noting best-effort delivery and the buffer-of-one drop semantics. - Add TestBroadcast_BestEffortDeliveryWhenBufferFull that locks the buffer-full drop behavior in place. Addresses @chaitanyamedidar inline comment on event_broadcast.go:72. Signed-off-by: SarthakB11 <[email protected]>
…ing sends
The IsClosed[K] helper at server/helpers/utils/utils.go:151 does
'case <-ch: return true' on the passed channel. For a buffered channel
that is open but has a queued value, the receive branch fires and the
value is dropped. The 'Publish*' helpers that use it as a gate then
report the channel closed and skip the send, so events are silently
lost.
Replaced the gate with a non-blocking 'select { case ch <- struct{}{}:
default: }' in:
- server/models/configuration_helper.go: PublishApplications,
PublishPatterns, PublishFilters
- server/models/dashboard_k8s_resources.go: PublishDashboardK8sResources
- server/models/meshmodel/helper.go: SummaryChannel.Publish
- server/models/k8scontext_helper.go: PublishContext (the inner select
was already non-blocking; only the redundant IsClosed gate is
removed)
Same fix pattern as PR meshery#19572 for server/models/event_broadcast.go.
None of these helpers expose an unsubscribe path that closes the
channel, so the send-on-closed-channel risk is bounded to callers that
close their own channels directly, and the non-blocking select handles
the slow-subscriber case identically to the broadcaster.
IsClosed itself is left in place because event_broadcast.go still
references it on master. Removal will be a follow-up once meshery#19572
merges and no callers remain.
Signed-off-by: SarthakB11 <[email protected]>
Per @Amr-Shams and gemini-code-assist review on PR meshery#19580. The Publish* methods iterated their subscriber slices without holding mx, while the corresponding Subscribe* methods modified them under mx.Lock(). A reallocating append concurrent with the publish loop would race on the slice header and risk reading from a stale backing array. Apply the copy-under-lock pattern already used by PublishContext in server/models/k8scontext_helper.go: c.mx.Lock() subscribers := make([]chan struct{}, len(c.channel)) copy(subscribers, c.channel) c.mx.Unlock() for _, ch := range subscribers { select { case ch <- struct{}{}: default: } } Sites updated: - server/models/configuration_helper.go: PublishApplications, PublishPatterns, PublishFilters - server/models/dashboard_k8s_resources.go: PublishDashboardK8sResources - server/models/meshmodel/helper.go: SummaryChannel.Publish PublishContext in k8scontext_helper.go was already correct and is unchanged. Signed-off-by: SarthakB11 <[email protected]>
Final step in the IsClosed cleanup sequence after meshery#19572 (event_broadcast.go) and meshery#19580 (configuration_helper.go, dashboard_k8s_resources.go, meshmodel/helper.go, k8scontext_helper.go) remove the last callers. IsClosed was incorrect by construction: on a buffered, still-open channel with a queued value it did 'case <-ch: return true', draining the value and falsely reporting closed. With no callers remaining, keeping it invites the same bug to be reintroduced. 12 LOC deletion. No other changes. Signed-off-by: SarthakB11 <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request refactors the event broadcasting and channel publishing logic to improve thread safety and reliability. It replaces the IsClosed utility with non-blocking select operations and mutex-protected subscriber list copying. Key changes include updating event_broadcast.go to use pointer-based storage for clients and adding a new test suite. Review feedback suggests optimizing the Subscribe method with a double-check pattern to avoid unnecessary allocations and using in-place slice filtering in the unsubscribe function for better performance.
| actual, _ := c.clients.LoadOrStore(id, &clients{}) | ||
| cc := actual.(*clients) |
There was a problem hiding this comment.
To avoid unnecessary allocations of the clients struct on every subscription call, consider using a double-check pattern with Load before LoadOrStore. This ensures that the pointer allocation only happens when the client ID is not already present in the map.
| actual, _ := c.clients.LoadOrStore(id, &clients{}) | |
| cc := actual.(*clients) | |
| actual, ok := c.clients.Load(id) | |
| if !ok { | |
| actual, _ = c.clients.LoadOrStore(id, &clients{}) | |
| } | |
| cc := actual.(*clients) |
There was a problem hiding this comment.
This touches code from #19572, not the deletion in this PR. Worth doing as a follow-up after that lands; not blocking here.
There was a problem hiding this comment.
Ah, yes... talking to yourself is a sure sigh of insanity.
There was a problem hiding this comment.
maybe he got stuck in a coding loop
| updated := make([]chan interface{}, 0, len(cc.listeners)) | ||
| for _, l := range cc.listeners { | ||
| if l == ch { | ||
| found = true | ||
| continue | ||
| } | ||
| updated = append(updated, l) | ||
| } | ||
| cc.listeners = updated |
There was a problem hiding this comment.
The current implementation of unsubscribe allocates a new slice and copies elements on every call. This can be optimized by performing the filtering in-place, reusing the existing slice's underlying array and avoiding extra allocations.
| updated := make([]chan interface{}, 0, len(cc.listeners)) | |
| for _, l := range cc.listeners { | |
| if l == ch { | |
| found = true | |
| continue | |
| } | |
| updated = append(updated, l) | |
| } | |
| cc.listeners = updated | |
| newListeners := cc.listeners[:0] | |
| for _, l := range cc.listeners { | |
| if l == ch { | |
| found = true | |
| continue | |
| } | |
| newListeners = append(newListeners, l) | |
| } | |
| cc.listeners = newListeners |
There was a problem hiding this comment.
Same as above, this is from #19572. Happy to roll into a follow-up perf pass once the chain lands.
There was a problem hiding this comment.
Yes, @SarthakB11. Why, thank you, @SarthakB11. No, thank me, @SarthakB11. Who me? Yes, you, @SarthakB11.
|
Fair. Those were aimed at @gemini-code-assist but GitHub hides the parent on inline replies. Reading back, it does sound deranged. |
|
Clean dead-code removal. Please confirm no external packages or tests import this helper before merging. |
Final step in the IsClosed cleanup sequence:
server/models/event_broadcast.go.configuration_helper.go,dashboard_k8s_resources.go,meshmodel/helper.go,k8scontext_helper.go.IsClosed[K]helper itself.IsClosedwas incorrect by construction: on a buffered, still-open channel with a queued value it executedcase <-ch: return true, drained the value, and falsely reported closed. With no callers remaining, keeping it invites the same bug to be reintroduced.Diff
12 LOC deletion in
server/helpers/utils/utils.go. Nothing else changes.Sequencing
This PR is opened as a draft and should land after both #19572 and #19580 are merged. The branch includes the diffs of both predecessor PRs so the code compiles in isolation; once those merge, please rebase this branch onto master and the final diff will be exactly the 12-line deletion shown.
If a maintainer prefers to skip the draft and squash this with one of the parent PRs, happy to fold it in.
Signed commits