Skip to content

[server] Remove utils.IsClosed[K] helper now unused - #19585

Closed
SarthakB11 wants to merge 8 commits into
meshery:masterfrom
SarthakB11:fix/delete-isclosed-helper
Closed

SarthakB11 wants to merge 8 commits into
meshery:masterfrom
SarthakB11:fix/delete-isclosed-helper

Conversation

@SarthakB11

Copy link
Copy Markdown

Final step in the IsClosed cleanup sequence:

IsClosed was incorrect by construction: on a buffered, still-open channel with a queued value it executed case <-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

  • Yes, I signed my commits.

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]>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +20 to +21
actual, _ := c.clients.LoadOrStore(id, &clients{})
cc := actual.(*clients)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This touches code from #19572, not the deletion in this PR. Worth doing as a follow-up after that lands; not blocking here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, yes... talking to yourself is a sure sigh of insanity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe he got stuck in a coding loop

Comment on lines +41 to +49
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same as above, this is from #19572. Happy to roll into a follow-up perf pass once the chain lands.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, @SarthakB11. Why, thank you, @SarthakB11. No, thank me, @SarthakB11. Who me? Yes, you, @SarthakB11.

@hamza-mohd

Copy link
Copy Markdown
Member

Great final step in the IsClosed cleanup sequence - deleting the helper after all call sites are removed is the right way to complete a refactor. The commit chain referencing #19572 and #19580 makes the sequence easy to follow.

@SarthakB11

Copy link
Copy Markdown
Author

Fair. Those were aimed at @gemini-code-assist but GitHub hides the parent on inline replies. Reading back, it does sound deranged.

@hamza-mohd

Copy link
Copy Markdown
Member

Clean dead-code removal. Please confirm no external packages or tests import this helper before merging.

@SarthakB11

Copy link
Copy Markdown
Author

Confirmed. grep -rn "IsClosed" --include="*.go" . on this branch returns zero hits across the entire repo, including tests and helper packages. Safe to delete once #19572 and #19580 merge.

@SarthakB11

Copy link
Copy Markdown
Author

utils.IsClosed was already removed on master by a03fd9a (#20375), so this is done. Closing as superseded.

@SarthakB11 SarthakB11 closed this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants