mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-10 06:52:00 +00:00
improvements nov2025 pt2 (#13)
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
This commit is contained in:
+56
-31
@@ -29,7 +29,8 @@ type ForwardWorker struct {
|
||||
cancel context.CancelFunc
|
||||
stopChan chan struct{}
|
||||
doneChan chan struct{}
|
||||
reconnectChan chan string // Channel to trigger reconnection
|
||||
reconnectChan chan string // Channel to trigger reconnection
|
||||
successChan chan struct{} // Channel to signal successful connection (for backoff reset)
|
||||
verbose bool
|
||||
lastPod string // Track the last pod we connected to
|
||||
statusUI StatusUpdater
|
||||
@@ -52,7 +53,8 @@ func NewForwardWorker(fwd config.Forward, portForwarder *k8s.PortForwarder, verb
|
||||
cancel: cancel,
|
||||
stopChan: make(chan struct{}),
|
||||
doneChan: make(chan struct{}),
|
||||
reconnectChan: make(chan string, 1), // Buffered to avoid blocking
|
||||
reconnectChan: make(chan string, 1), // Buffered to avoid blocking
|
||||
successChan: make(chan struct{}, 1), // Buffered to avoid blocking
|
||||
verbose: verbose,
|
||||
statusUI: statusUI,
|
||||
healthChecker: healthChecker,
|
||||
@@ -61,6 +63,16 @@ func NewForwardWorker(fwd config.Forward, portForwarder *k8s.PortForwarder, verb
|
||||
}
|
||||
}
|
||||
|
||||
// signalConnectionSuccess signals that a connection was successfully established.
|
||||
// This is used to reset the backoff timer after a successful connection.
|
||||
func (w *ForwardWorker) signalConnectionSuccess() {
|
||||
select {
|
||||
case w.successChan <- struct{}{}:
|
||||
default:
|
||||
// Channel already has pending signal
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerReconnect triggers a reconnection (e.g., due to stale connection)
|
||||
func (w *ForwardWorker) TriggerReconnect(reason string) {
|
||||
// Cancel current forward if running
|
||||
@@ -100,16 +112,33 @@ func (w *ForwardWorker) Stop() {
|
||||
}
|
||||
}
|
||||
|
||||
// IsAlive implements HeartbeatResponder interface.
|
||||
// Returns true if the worker goroutine is still running and responsive.
|
||||
func (w *ForwardWorker) IsAlive() bool {
|
||||
select {
|
||||
case <-w.doneChan:
|
||||
return false
|
||||
case <-w.ctx.Done():
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// GetForwardID implements HeartbeatResponder interface.
|
||||
func (w *ForwardWorker) GetForwardID() string {
|
||||
return w.forward.ID()
|
||||
}
|
||||
|
||||
// run is the main worker loop that handles retries.
|
||||
func (w *ForwardWorker) run() {
|
||||
defer close(w.doneChan)
|
||||
defer w.stopHTTPProxy() // Ensure proxy is stopped on exit
|
||||
|
||||
// Start heartbeat goroutine to continuously send heartbeats to watchdog
|
||||
// This prevents false "hung worker" detection when connections are long-lived
|
||||
if w.watchdog != nil {
|
||||
go w.heartbeatLoop()
|
||||
}
|
||||
// Note: Heartbeat management is now centralized in the Watchdog.
|
||||
// The watchdog polls workers via the HeartbeatResponder interface (IsAlive method)
|
||||
// instead of each worker spawning its own heartbeat goroutine.
|
||||
// This reduces goroutine count from 2N to N for N workers.
|
||||
|
||||
// Start HTTP logging proxy if enabled
|
||||
if err := w.startHTTPProxy(); err != nil {
|
||||
@@ -123,13 +152,16 @@ func (w *ForwardWorker) run() {
|
||||
backoff := retry.NewBackoff()
|
||||
|
||||
for {
|
||||
// Check if we should stop
|
||||
// Check if we should stop or reset backoff on successful connection
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
if w.verbose {
|
||||
log.Printf("[%s] Worker stopped", w.forward.ID())
|
||||
}
|
||||
return
|
||||
case <-w.successChan:
|
||||
// Reset backoff after successful connection
|
||||
backoff.Reset()
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -225,26 +257,6 @@ func (w *ForwardWorker) run() {
|
||||
}
|
||||
}
|
||||
|
||||
// heartbeatLoop sends periodic heartbeats to the watchdog to prove the worker is alive
|
||||
// This runs in a separate goroutine and continues throughout the worker's lifetime
|
||||
func (w *ForwardWorker) heartbeatLoop() {
|
||||
// Send heartbeats every 15 seconds (well within typical 60s watchdog timeout)
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Send immediate heartbeat
|
||||
w.watchdog.Heartbeat(w.forward.ID())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
w.watchdog.Heartbeat(w.forward.ID())
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// establishForward establishes a port-forward connection.
|
||||
// This blocks until the connection is closed or an error occurs.
|
||||
func (w *ForwardWorker) establishForward(podName string) error {
|
||||
@@ -267,15 +279,26 @@ func (w *ForwardWorker) establishForward(podName string) error {
|
||||
w.forwardCancelMu.Unlock()
|
||||
}()
|
||||
|
||||
// Use sync.Once to ensure stopChan is closed exactly once
|
||||
var closeOnce sync.Once
|
||||
closeStopChan := func() {
|
||||
closeOnce.Do(func() {
|
||||
close(stopChan)
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure stopChan is closed when this function exits (prevents goroutine leak)
|
||||
defer closeStopChan()
|
||||
|
||||
// Start a goroutine to monitor for stop signal and reconnect triggers
|
||||
go func() {
|
||||
select {
|
||||
case <-w.stopChan:
|
||||
close(stopChan)
|
||||
closeStopChan()
|
||||
case <-w.reconnectChan:
|
||||
close(stopChan)
|
||||
closeStopChan()
|
||||
case <-forwardCtx.Done():
|
||||
close(stopChan)
|
||||
closeStopChan()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -326,6 +349,8 @@ func (w *ForwardWorker) establishForward(podName string) error {
|
||||
if w.healthChecker != nil {
|
||||
w.healthChecker.MarkConnected(w.forward.ID())
|
||||
}
|
||||
// Signal success back to caller so backoff can be reset
|
||||
w.signalConnectionSuccess()
|
||||
case err := <-errChan:
|
||||
return fmt.Errorf("failed to establish forward: %w", err)
|
||||
case <-w.ctx.Done():
|
||||
|
||||
Reference in New Issue
Block a user