Two related, pre-existing concurrency/lifecycle issues in the exec session code (meshsync/exec.go, meshsync/handlers.go), surfaced by review on #573. Deferred from that PR because a correct fix is a broader refactor that risks the exec request/reply protocol, which #573 was scoped to preserve.
1. h.channelPool is a plain map shared across goroutines
channelPool holds both fixed system channels (Stop/ReSync/OS, set once at init) and dynamic per-session exec channels. Exec goroutines mutate it (h.channelPool[id] = ...; delete(h.channelPool, id) in streamSession/terminate; execCleanup) while other goroutines read or range it (getActiveChannels ranges the whole map; <-h.channelPool[channels.Stop] in select loops across handlers.go/exec.go). Concurrent map read+write can panic at runtime.
Fix direction: move the mutable exec-session channels into their own sync.Mutex-guarded map so the system channelPool is read-only after init; or add a Handler-wide lock and read system-channel refs into locals before selecting on them. Also fix getActiveChannels, which currently ranges the whole pool and returns system-channel keys as "active sessions".
2. Exec input subscription and its drain goroutine can't be torn down cleanly
streamSession subscribes to input.<id> via SubscribeWithChannel and, on session end, parks a drain goroutine (<-done; for range subCh {}) that never exits because subCh is never closed. Root cause: MeshKit's broker.Handler interface has no Unsubscribe. Once MeshKit exposes it, streamSession should unsubscribe on teardown and drop the drain goroutine.
Depends on: MeshKit broker.Handler.Unsubscribe (designed in #580).
Two related, pre-existing concurrency/lifecycle issues in the exec session code (
meshsync/exec.go,meshsync/handlers.go), surfaced by review on #573. Deferred from that PR because a correct fix is a broader refactor that risks the exec request/reply protocol, which #573 was scoped to preserve.1.
h.channelPoolis a plain map shared across goroutineschannelPoolholds both fixed system channels (Stop/ReSync/OS, set once at init) and dynamic per-session exec channels. Exec goroutines mutate it (h.channelPool[id] = ...;delete(h.channelPool, id)instreamSession/terminate;execCleanup) while other goroutines read or range it (getActiveChannelsranges the whole map;<-h.channelPool[channels.Stop]in select loops acrosshandlers.go/exec.go). Concurrent map read+write can panic at runtime.Fix direction: move the mutable exec-session channels into their own
sync.Mutex-guarded map so the systemchannelPoolis read-only after init; or add a Handler-wide lock and read system-channel refs into locals before selecting on them. Also fixgetActiveChannels, which currently ranges the whole pool and returns system-channel keys as "active sessions".2. Exec input subscription and its drain goroutine can't be torn down cleanly
streamSessionsubscribes toinput.<id>viaSubscribeWithChanneland, on session end, parks a drain goroutine (<-done; for range subCh {}) that never exits becausesubChis never closed. Root cause: MeshKit'sbroker.Handlerinterface has noUnsubscribe. Once MeshKit exposes it,streamSessionshould unsubscribe on teardown and drop the drain goroutine.Depends on: MeshKit
broker.Handler.Unsubscribe(designed in #580).