mirror of https://github.com/jetkvm/kvm.git
330 lines
9.1 KiB
Go
330 lines
9.1 KiB
Go
package audio
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// Task represents a function to be executed by a worker in the pool
|
|
type Task func()
|
|
|
|
// GoroutinePool manages a pool of reusable goroutines to reduce the overhead
|
|
// of goroutine creation and destruction
|
|
type GoroutinePool struct {
|
|
// Atomic fields must be first for proper alignment on 32-bit systems
|
|
taskCount int64 // Number of tasks processed
|
|
workerCount int64 // Current number of workers
|
|
maxIdleTime time.Duration
|
|
maxWorkers int
|
|
taskQueue chan Task
|
|
workerSem chan struct{} // Semaphore to limit concurrent workers
|
|
shutdown chan struct{}
|
|
shutdownOnce sync.Once
|
|
wg sync.WaitGroup
|
|
logger *zerolog.Logger
|
|
name string
|
|
}
|
|
|
|
// NewGoroutinePool creates a new goroutine pool with the specified parameters
|
|
func NewGoroutinePool(name string, maxWorkers int, queueSize int, maxIdleTime time.Duration) *GoroutinePool {
|
|
logger := logging.GetDefaultLogger().With().Str("component", "goroutine-pool").Str("pool", name).Logger()
|
|
|
|
pool := &GoroutinePool{
|
|
maxWorkers: maxWorkers,
|
|
maxIdleTime: maxIdleTime,
|
|
taskQueue: make(chan Task, queueSize),
|
|
workerSem: make(chan struct{}, maxWorkers),
|
|
shutdown: make(chan struct{}),
|
|
logger: &logger,
|
|
name: name,
|
|
}
|
|
|
|
// Start a supervisor goroutine to monitor pool health
|
|
go pool.supervisor()
|
|
|
|
return pool
|
|
}
|
|
|
|
// Submit adds a task to the pool for execution
|
|
// Returns true if the task was accepted, false if the queue is full
|
|
func (p *GoroutinePool) Submit(task Task) bool {
|
|
select {
|
|
case <-p.shutdown:
|
|
return false // Pool is shutting down
|
|
case p.taskQueue <- task:
|
|
// Task accepted, ensure we have a worker to process it
|
|
p.ensureWorkerAvailable()
|
|
return true
|
|
default:
|
|
// Queue is full
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SubmitWithBackpressure adds a task to the pool with backpressure handling
|
|
// Returns true if task was accepted, false if dropped due to backpressure
|
|
func (p *GoroutinePool) SubmitWithBackpressure(task Task) bool {
|
|
select {
|
|
case <-p.shutdown:
|
|
return false // Pool is shutting down
|
|
case p.taskQueue <- task:
|
|
// Task accepted, ensure we have a worker to process it
|
|
p.ensureWorkerAvailable()
|
|
return true
|
|
default:
|
|
// Queue is full - apply backpressure
|
|
// Check if we're in a high-load situation
|
|
queueLen := len(p.taskQueue)
|
|
queueCap := cap(p.taskQueue)
|
|
workerCount := atomic.LoadInt64(&p.workerCount)
|
|
|
|
// If queue is >90% full and we're at max workers, drop the task
|
|
if queueLen > int(float64(queueCap)*0.9) && workerCount >= int64(p.maxWorkers) {
|
|
p.logger.Warn().Int("queue_len", queueLen).Int("queue_cap", queueCap).Msg("Dropping task due to backpressure")
|
|
return false
|
|
}
|
|
|
|
// Try one more time with a short timeout
|
|
select {
|
|
case p.taskQueue <- task:
|
|
p.ensureWorkerAvailable()
|
|
return true
|
|
case <-time.After(1 * time.Millisecond):
|
|
// Still can't submit after timeout - drop task
|
|
p.logger.Debug().Msg("Task dropped after backpressure timeout")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// ensureWorkerAvailable makes sure at least one worker is available to process tasks
|
|
func (p *GoroutinePool) ensureWorkerAvailable() {
|
|
// Check if we already have enough workers
|
|
currentWorkers := atomic.LoadInt64(&p.workerCount)
|
|
|
|
// Only start new workers if:
|
|
// 1. We have no workers at all, or
|
|
// 2. The queue is growing and we're below max workers
|
|
queueLen := len(p.taskQueue)
|
|
if currentWorkers == 0 || (queueLen > int(currentWorkers) && currentWorkers < int64(p.maxWorkers)) {
|
|
// Try to acquire a semaphore slot without blocking
|
|
select {
|
|
case p.workerSem <- struct{}{}:
|
|
// We got a slot, start a new worker
|
|
p.startWorker()
|
|
default:
|
|
// All worker slots are taken, which means we have enough workers
|
|
}
|
|
}
|
|
}
|
|
|
|
// startWorker launches a new worker goroutine
|
|
func (p *GoroutinePool) startWorker() {
|
|
p.wg.Add(1)
|
|
atomic.AddInt64(&p.workerCount, 1)
|
|
|
|
go func() {
|
|
defer func() {
|
|
atomic.AddInt64(&p.workerCount, -1)
|
|
<-p.workerSem // Release the semaphore slot
|
|
p.wg.Done()
|
|
|
|
// Recover from panics in worker tasks
|
|
if r := recover(); r != nil {
|
|
p.logger.Error().Interface("panic", r).Msg("Worker recovered from panic")
|
|
}
|
|
}()
|
|
|
|
idleTimer := time.NewTimer(p.maxIdleTime)
|
|
defer idleTimer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.shutdown:
|
|
return
|
|
case task, ok := <-p.taskQueue:
|
|
if !ok {
|
|
return // Channel closed
|
|
}
|
|
|
|
// Reset idle timer
|
|
if !idleTimer.Stop() {
|
|
<-idleTimer.C
|
|
}
|
|
idleTimer.Reset(p.maxIdleTime)
|
|
|
|
// Execute the task with panic recovery
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
p.logger.Error().Interface("panic", r).Msg("Task execution panic recovered")
|
|
}
|
|
}()
|
|
task()
|
|
}()
|
|
|
|
atomic.AddInt64(&p.taskCount, 1)
|
|
case <-idleTimer.C:
|
|
// Worker has been idle for too long
|
|
// Keep at least 2 workers alive to handle incoming tasks without creating new goroutines
|
|
if atomic.LoadInt64(&p.workerCount) > 2 {
|
|
return
|
|
}
|
|
// For persistent workers (the minimum 2), use a longer idle timeout
|
|
// This prevents excessive worker creation/destruction cycles
|
|
idleTimer.Reset(p.maxIdleTime * 3) // Triple the idle time for persistent workers
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// supervisor monitors the pool and logs statistics periodically
|
|
func (p *GoroutinePool) supervisor() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.shutdown:
|
|
return
|
|
case <-ticker.C:
|
|
workers := atomic.LoadInt64(&p.workerCount)
|
|
tasks := atomic.LoadInt64(&p.taskCount)
|
|
queueLen := len(p.taskQueue)
|
|
|
|
p.logger.Debug().
|
|
Int64("workers", workers).
|
|
Int64("tasks_processed", tasks).
|
|
Int("queue_length", queueLen).
|
|
Msg("Pool statistics")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the pool
|
|
// If wait is true, it will wait for all tasks to complete
|
|
// If wait is false, it will terminate immediately, potentially leaving tasks unprocessed
|
|
func (p *GoroutinePool) Shutdown(wait bool) {
|
|
p.shutdownOnce.Do(func() {
|
|
close(p.shutdown)
|
|
|
|
if wait {
|
|
// Wait for all tasks to be processed
|
|
if len(p.taskQueue) > 0 {
|
|
p.logger.Debug().Int("remaining_tasks", len(p.taskQueue)).Msg("Waiting for tasks to complete")
|
|
}
|
|
|
|
// Close the task queue to signal no more tasks
|
|
close(p.taskQueue)
|
|
|
|
// Wait for all workers to finish
|
|
p.wg.Wait()
|
|
}
|
|
})
|
|
}
|
|
|
|
// GetStats returns statistics about the pool
|
|
func (p *GoroutinePool) GetStats() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"name": p.name,
|
|
"worker_count": atomic.LoadInt64(&p.workerCount),
|
|
"max_workers": p.maxWorkers,
|
|
"tasks_processed": atomic.LoadInt64(&p.taskCount),
|
|
"queue_length": len(p.taskQueue),
|
|
"queue_capacity": cap(p.taskQueue),
|
|
}
|
|
}
|
|
|
|
// Global pools for different audio processing tasks
|
|
var (
|
|
globalAudioProcessorPool atomic.Pointer[GoroutinePool]
|
|
globalAudioReaderPool atomic.Pointer[GoroutinePool]
|
|
globalAudioProcessorInitOnce sync.Once
|
|
globalAudioReaderInitOnce sync.Once
|
|
)
|
|
|
|
// GetAudioProcessorPool returns the global audio processor pool
|
|
func GetAudioProcessorPool() *GoroutinePool {
|
|
pool := globalAudioProcessorPool.Load()
|
|
if pool != nil {
|
|
return pool
|
|
}
|
|
|
|
globalAudioProcessorInitOnce.Do(func() {
|
|
config := Config
|
|
newPool := NewGoroutinePool(
|
|
"audio-processor",
|
|
config.MaxAudioProcessorWorkers,
|
|
config.AudioProcessorQueueSize,
|
|
config.WorkerMaxIdleTime,
|
|
)
|
|
globalAudioProcessorPool.Store(newPool)
|
|
pool = newPool
|
|
})
|
|
|
|
return globalAudioProcessorPool.Load()
|
|
}
|
|
|
|
// GetAudioReaderPool returns the global audio reader pool
|
|
func GetAudioReaderPool() *GoroutinePool {
|
|
pool := globalAudioReaderPool.Load()
|
|
if pool != nil {
|
|
return pool
|
|
}
|
|
|
|
globalAudioReaderInitOnce.Do(func() {
|
|
config := Config
|
|
newPool := NewGoroutinePool(
|
|
"audio-reader",
|
|
config.MaxAudioReaderWorkers,
|
|
config.AudioReaderQueueSize,
|
|
config.WorkerMaxIdleTime,
|
|
)
|
|
globalAudioReaderPool.Store(newPool)
|
|
pool = newPool
|
|
})
|
|
|
|
return globalAudioReaderPool.Load()
|
|
}
|
|
|
|
// SubmitAudioProcessorTask submits a task to the audio processor pool
|
|
func SubmitAudioProcessorTask(task Task) bool {
|
|
return GetAudioProcessorPool().Submit(task)
|
|
}
|
|
|
|
// SubmitAudioReaderTask submits a task to the audio reader pool
|
|
func SubmitAudioReaderTask(task Task) bool {
|
|
return GetAudioReaderPool().Submit(task)
|
|
}
|
|
|
|
// SubmitAudioProcessorTaskWithBackpressure submits a task with backpressure handling
|
|
func SubmitAudioProcessorTaskWithBackpressure(task Task) bool {
|
|
return GetAudioProcessorPool().SubmitWithBackpressure(task)
|
|
}
|
|
|
|
// SubmitAudioReaderTaskWithBackpressure submits a task with backpressure handling
|
|
func SubmitAudioReaderTaskWithBackpressure(task Task) bool {
|
|
return GetAudioReaderPool().SubmitWithBackpressure(task)
|
|
}
|
|
|
|
// ShutdownAudioPools shuts down all audio goroutine pools
|
|
func ShutdownAudioPools(wait bool) {
|
|
logger := logging.GetDefaultLogger().With().Str("component", "audio-pools").Logger()
|
|
|
|
processorPool := globalAudioProcessorPool.Load()
|
|
if processorPool != nil {
|
|
logger.Info().Msg("Shutting down audio processor pool")
|
|
processorPool.Shutdown(wait)
|
|
}
|
|
|
|
readerPool := globalAudioReaderPool.Load()
|
|
if readerPool != nil {
|
|
logger.Info().Msg("Shutting down audio reader pool")
|
|
readerPool.Shutdown(wait)
|
|
}
|
|
}
|