Goroutine Lifecycle Management#
KubeVirt uses several recurring patterns to control goroutine lifetimes, prevent leaks, and ensure safe concurrent shutdown. These patterns appear across production code (WebSocket streaming, virt-handler) and test infrastructure (event watchers, VNC helpers).
Pattern 1: WebSocket Coordination via Done Channel#
The client-go streaming layer uses a shared done chan struct{} to keep a WebSocket connection alive for exactly as long as the stream is running.
How it works:
AsyncSubresourceHelpercreates thedonechannel and embeds it in both theAsyncWSRoundTripperand the returnedwsStreamer.- The round-tripper goroutine blocks on
<-aws.Doneafter handing the live*websocket.Connto the caller . This keeps the underlying HTTP/2 or WebSocket transport alive. - When
wsStreamer.Stream()returns (either copy direction finishes),defer ws.streamDone()fires, which callsclose(ws.done). This unblocks the round-tripper goroutine and tears down the transport.
The channel is allocated in AsyncSubresourceHelper and ownership flows through NewWebsocketStreamer β callers never need to close it manually; defer inside Stream() handles it.
Pattern 2: Idempotent Shutdown with sync.Once#
When multiple code paths can concurrently trigger the same cleanup β for example, migration finalization, migration execution, and VM controller cleanup all calling CloseLauncherClient β a raw close(ch) will panic on the second call.
The fix in PR #18146: add a closeOnce sync.Once field to LauncherClientInfo and route all cleanup through a Close() method:
l.closeOnce.Do(func() {
l.Client.Close()
close(l.DomainPipeStopChan)
})
The method is also nil-safe , so callers don't need to guard against a nil LauncherClientInfo. See pkg/virt-handler/cache/maps.go for the full definition.
Pattern 3: Explicit Goroutine Joining via Done Channel#
The test event watcher (tests/watcher/watcher.go) demonstrates a clean join pattern: the spawned goroutine closes a local done chan struct{} when it exits, and the caller selects across done, ctx.Done(), and an optional timeout .
Key implementation details:
defer eventWatcher.Stop()is registered before the goroutine is launched , ensuring theResultChan()is closed which unblocks the goroutine'srangeloop.- The select statement at lines 215β229 merges three termination signals β completion, cancellation, and timeout β without any additional synchronization primitives .
This pattern avoids the fire-and-forget anti-pattern where a goroutine outlives its calling scope and later triggers test failures via Ginkgo's global failer. The problem and fix are described in PR #17293.
Pattern 4: Defer-Based Cleanup in Goroutine Bodies#
Inside goroutines that own resources, defer ensures cleanup runs on every exit path, including panics:
- VNC test helper (
tests/vnc_test.go, refactored in PR #17101): the goroutine holds a WebSocket connection and registersdefer vnc.AsConn().Close()immediately after acquiring it. After signaling success via the returnedchan bool, it blocks on<-ctx.Done()β keeping the session alive for the test β and exits cleanly when the test's context is cancelled. - Event watcher goroutines use
defer GinkgoRecover()to catch panics without crashing the process, paired with the join pattern above so leaked-goroutine panics can't corrupt later tests.
Pattern 5: Context-Driven Lifecycle for Conditional Goroutines#
watchVMIForPhase in tests/libwait/wait.go spawns a warning-monitor goroutine conditionally (only when FailOnWarnings is enabled) . The goroutine's lifetime is bounded by w.ctx, which is derived from the test's context β ensuring it terminates when the EventuallyWithOffset block exits.
This avoids the previous bug where an unconditional goroutine continued running after the test completed, fixed in PR #17293.
Key Files#
| File | Purpose |
|---|---|
staging/.../async.go | AsyncSubresourceHelper β done-channel allocation and WebSocket lifetime |
staging/.../streamer.go | wsStreamer.Stream() β defer-based done-channel close |
pkg/virt-handler/cache/maps.go | LauncherClientInfo.Close() β sync.Once for idempotent shutdown |
tests/watcher/watcher.go | Watch() β explicit goroutine join via done channel |
tests/libwait/wait.go | watchVMIForPhase() β context-driven conditional goroutine |