mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-14 07:51: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:
+105
-19
@@ -5,18 +5,29 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nvm/kportal/internal/events"
|
||||
"github.com/nvm/kportal/internal/logger"
|
||||
)
|
||||
|
||||
// Watchdog monitors worker goroutines to detect hung workers
|
||||
const (
|
||||
// defaultHeartbeatInterval is how often the watchdog sends heartbeats to workers
|
||||
defaultHeartbeatInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
// Watchdog monitors worker goroutines to detect hung workers.
|
||||
// It centralizes heartbeat management - instead of each worker sending heartbeats,
|
||||
// the watchdog polls workers periodically. This reduces goroutine count and
|
||||
// simplifies worker implementation.
|
||||
type Watchdog struct {
|
||||
mu sync.RWMutex
|
||||
workers map[string]*workerState // key: forward ID
|
||||
checkInterval time.Duration
|
||||
hangThreshold time.Duration // How long without heartbeat before considered hung
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
mu sync.RWMutex
|
||||
workers map[string]*workerState // key: forward ID
|
||||
checkInterval time.Duration
|
||||
hangThreshold time.Duration // How long without heartbeat before considered hung
|
||||
heartbeatInterval time.Duration // How often to poll workers for heartbeat
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
eventBus *events.Bus // Optional event bus for decoupled communication
|
||||
}
|
||||
|
||||
// workerState tracks the health of a single worker
|
||||
@@ -26,20 +37,37 @@ type workerState struct {
|
||||
heartbeatCount uint64
|
||||
isHung bool
|
||||
onHungCallback func(forwardID string)
|
||||
worker HeartbeatResponder // Reference to worker for heartbeat polling
|
||||
}
|
||||
|
||||
// HeartbeatResponder is an interface for workers that can respond to heartbeat checks
|
||||
type HeartbeatResponder interface {
|
||||
// IsAlive returns true if the worker is still responsive
|
||||
IsAlive() bool
|
||||
// GetForwardID returns the forward ID this worker manages
|
||||
GetForwardID() string
|
||||
}
|
||||
|
||||
// NewWatchdog creates a new goroutine watchdog
|
||||
func NewWatchdog(checkInterval, hangThreshold time.Duration) *Watchdog {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Watchdog{
|
||||
workers: make(map[string]*workerState),
|
||||
checkInterval: checkInterval,
|
||||
hangThreshold: hangThreshold,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
workers: make(map[string]*workerState),
|
||||
checkInterval: checkInterval,
|
||||
hangThreshold: hangThreshold,
|
||||
heartbeatInterval: defaultHeartbeatInterval,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventBus sets the event bus for publishing watchdog events
|
||||
func (w *Watchdog) SetEventBus(bus *events.Bus) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.eventBus = bus
|
||||
}
|
||||
|
||||
// Start begins the watchdog monitoring loop
|
||||
func (w *Watchdog) Start() {
|
||||
w.wg.Add(1)
|
||||
@@ -70,6 +98,25 @@ func (w *Watchdog) RegisterWorker(forwardID string, onHungCallback func(string))
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterWorkerWithResponder adds a worker to monitor with heartbeat polling support
|
||||
func (w *Watchdog) RegisterWorkerWithResponder(forwardID string, responder HeartbeatResponder, onHungCallback func(string)) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.workers[forwardID] = &workerState{
|
||||
forwardID: forwardID,
|
||||
lastHeartbeat: time.Now(),
|
||||
heartbeatCount: 0,
|
||||
isHung: false,
|
||||
onHungCallback: onHungCallback,
|
||||
worker: responder,
|
||||
}
|
||||
|
||||
logger.Debug("Watchdog registered worker with responder", map[string]interface{}{
|
||||
"forward_id": forwardID,
|
||||
})
|
||||
}
|
||||
|
||||
// UnregisterWorker removes a worker from monitoring
|
||||
func (w *Watchdog) UnregisterWorker(forwardID string) {
|
||||
w.mu.Lock()
|
||||
@@ -82,8 +129,9 @@ func (w *Watchdog) UnregisterWorker(forwardID string) {
|
||||
})
|
||||
}
|
||||
|
||||
// Heartbeat records that a worker is alive and processing
|
||||
// Workers should call this periodically (e.g., in their main loop)
|
||||
// Heartbeat records that a worker is alive and processing.
|
||||
// This can be called by workers directly (legacy) or the watchdog can poll
|
||||
// workers via HeartbeatResponder interface.
|
||||
func (w *Watchdog) Heartbeat(forwardID string) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
@@ -106,23 +154,54 @@ func (w *Watchdog) GetWorkerState(forwardID string) (lastHeartbeat time.Time, co
|
||||
return time.Time{}, 0, false
|
||||
}
|
||||
|
||||
// monitorLoop periodically checks all workers
|
||||
// monitorLoop periodically checks all workers and polls for heartbeats
|
||||
func (w *Watchdog) monitorLoop() {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(w.checkInterval)
|
||||
defer ticker.Stop()
|
||||
checkTicker := time.NewTicker(w.checkInterval)
|
||||
defer checkTicker.Stop()
|
||||
|
||||
// Heartbeat polling ticker - polls workers for heartbeat more frequently
|
||||
heartbeatTicker := time.NewTicker(w.heartbeatInterval)
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
case <-heartbeatTicker.C:
|
||||
// Poll all workers for heartbeat (centralized heartbeat management)
|
||||
w.pollHeartbeats()
|
||||
case <-checkTicker.C:
|
||||
// Check for hung workers
|
||||
w.checkWorkers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollHeartbeats polls all registered workers for heartbeat.
|
||||
// This centralizes heartbeat management in the watchdog instead of having
|
||||
// each worker spawn its own heartbeat goroutine.
|
||||
func (w *Watchdog) pollHeartbeats() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for forwardID, state := range w.workers {
|
||||
// If worker has a responder, poll it
|
||||
if state.worker != nil {
|
||||
if state.worker.IsAlive() {
|
||||
state.lastHeartbeat = now
|
||||
state.heartbeatCount++
|
||||
state.isHung = false
|
||||
}
|
||||
}
|
||||
// If no responder, worker must call Heartbeat() directly (legacy mode)
|
||||
// This maintains backward compatibility
|
||||
_ = forwardID // Avoid unused variable warning
|
||||
}
|
||||
}
|
||||
|
||||
// hungWorkerInfo stores information about a hung worker for deferred callback execution
|
||||
type hungWorkerInfo struct {
|
||||
forwardID string
|
||||
@@ -133,8 +212,10 @@ type hungWorkerInfo struct {
|
||||
func (w *Watchdog) checkWorkers() {
|
||||
// Collect hung workers while holding the lock
|
||||
var hungWorkers []hungWorkerInfo
|
||||
var eventBus *events.Bus
|
||||
|
||||
w.mu.Lock()
|
||||
eventBus = w.eventBus
|
||||
now := time.Now()
|
||||
for forwardID, state := range w.workers {
|
||||
timeSinceHeartbeat := now.Sub(state.lastHeartbeat)
|
||||
@@ -169,6 +250,11 @@ func (w *Watchdog) checkWorkers() {
|
||||
// (they trigger reconnection via channels), so concurrent state changes
|
||||
// between detection and callback execution are safe.
|
||||
for _, hw := range hungWorkers {
|
||||
// Publish event if event bus is available
|
||||
if eventBus != nil {
|
||||
eventBus.Publish(events.NewWorkerHungEvent(hw.forwardID, w.hangThreshold.String()))
|
||||
}
|
||||
|
||||
hw.callback(hw.forwardID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user