Files
futriix/internal/cluster/worker_pool.go

396 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/worker_pool.go
// Назначение: Lock-free пул воркеров для ограничения количества горутин.
// Использует атомарные операции и lock-free структуры данных.
package cluster
import (
"fmt"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"futriis/internal/log"
)
// WorkerPool представляет lock-free пул воркеров для выполнения задач
type WorkerPool struct {
maxWorkers int32 // Максимальное количество воркеров
activeWorkers atomic.Int32 // Текущее количество активных воркеров
tasks *LockFreeQueue // Lock-free очередь задач
stopChan chan struct{}
wg sync.WaitGroup
logger *log.Logger
// Статистика (атомарные счётчики)
submittedTasks atomic.Uint64
completedTasks atomic.Uint64
failedTasks atomic.Uint64
rejectedTasks atomic.Uint64
lastSubmitTime atomic.Int64
lastCompleteTime atomic.Int64
}
// Task представляет задачу для выполнения в пуле
type Task struct {
ID string
Execute func() error
CreatedAt int64
RetryCount int32
}
// LockFreeQueue представляет lock-free очередь на основе CAS операций
type LockFreeQueue struct {
head atomic.Value // *queueNode
tail atomic.Value // *queueNode
size atomic.Int64
}
type queueNode struct {
value *Task
next atomic.Value // *queueNode
}
// NewLockFreeQueue создаёт новую lock-free очередь
func NewLockFreeQueue() *LockFreeQueue {
q := &LockFreeQueue{}
dummy := &queueNode{}
q.head.Store(dummy)
q.tail.Store(dummy)
return q
}
// Enqueue добавляет задачу в очередь (lock-free)
func (q *LockFreeQueue) Enqueue(task *Task) bool {
newNode := &queueNode{value: task}
for {
tailVal := q.tail.Load()
if tailVal == nil {
continue
}
tail := tailVal.(*queueNode)
nextVal := tail.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if tailVal != q.tail.Load() {
continue
}
if next != nil {
q.tail.CompareAndSwap(tailVal, next)
continue
}
if tail.next.CompareAndSwap(nil, newNode) {
q.tail.CompareAndSwap(tailVal, newNode)
q.size.Add(1)
return true
}
}
}
// Dequeue извлекает задачу из очереди (lock-free)
func (q *LockFreeQueue) Dequeue() *Task {
for {
headVal := q.head.Load()
if headVal == nil {
return nil
}
head := headVal.(*queueNode)
tailVal := q.tail.Load()
if tailVal == nil {
return nil
}
tail := tailVal.(*queueNode)
nextVal := head.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if headVal != q.head.Load() {
continue
}
if head == tail {
if next == nil {
return nil
}
q.tail.CompareAndSwap(tailVal, next)
continue
}
if next == nil {
return nil
}
task := next.value
if q.head.CompareAndSwap(headVal, next) {
q.size.Add(-1)
return task
}
}
}
// Size возвращает текущий размер очереди (lock-free)
func (q *LockFreeQueue) Size() int64 {
return q.size.Load()
}
// NewWorkerPool создаёт новый lock-free пул воркеров
func NewWorkerPool(maxWorkers int, logger *log.Logger) *WorkerPool {
if maxWorkers <= 0 {
maxWorkers = 500
}
wp := &WorkerPool{
maxWorkers: int32(maxWorkers),
tasks: NewLockFreeQueue(),
stopChan: make(chan struct{}),
logger: logger,
}
// Запускаем диспетчер задач (не блокирующий)
go wp.dispatcher()
if logger != nil {
logger.Debug(fmt.Sprintf("Lock-free worker pool created: maxWorkers=%d", maxWorkers))
}
return wp
}
// dispatcher управляет воркерами (lock-free)
func (wp *WorkerPool) dispatcher() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker pool dispatcher panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем диспетчер
go wp.dispatcher()
}
}()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-wp.stopChan:
return
case <-ticker.C:
// Динамически регулируем количество воркеров
wp.adjustWorkers()
}
}
}
// adjustWorkers динамически регулирует количество воркеров (lock-free)
func (wp *WorkerPool) adjustWorkers() {
queueSize := wp.tasks.Size()
activeWorkers := wp.activeWorkers.Load()
// Если есть задачи и есть место для новых воркеров
if queueSize > 0 && activeWorkers < wp.maxWorkers {
// Проверяем, нужно ли добавить воркер
if activeWorkers == 0 || queueSize > int64(activeWorkers)*2 {
wp.addWorker()
}
}
// Уменьшаем количество воркеров, если нет задач и воркеры простаивают
if queueSize == 0 && activeWorkers > 0 {
// Не уменьшаем ниже 1 воркера
if activeWorkers > 1 {
// Проверяем, что не было задач в последнее время
lastComplete := wp.lastCompleteTime.Load()
if time.Now().UnixMilli()-lastComplete > 5000 {
wp.removeWorker()
}
}
}
}
// addWorker добавляет нового воркера (lock-free)
func (wp *WorkerPool) addWorker() {
if wp.activeWorkers.Load() >= wp.maxWorkers {
return
}
wp.activeWorkers.Add(1)
wp.wg.Add(1)
go wp.worker()
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Added worker, active: %d/%d", wp.activeWorkers.Load(), wp.maxWorkers))
}
}
// removeWorker удаляет воркера (lock-free)
func (wp *WorkerPool) removeWorker() {
if wp.activeWorkers.Load() <= 1 {
return
}
// Отправляем специальную задачу на остановку воркера
task := &Task{
ID: fmt.Sprintf("stop_worker_%d", time.Now().UnixNano()),
Execute: func() error { return nil },
CreatedAt: time.Now().UnixMilli(),
}
if wp.tasks.Enqueue(task) {
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Stop signal sent to worker, active: %d/%d", wp.activeWorkers.Load()-1, wp.maxWorkers))
}
}
}
// worker выполняет задачи из очереди (lock-free)
func (wp *WorkerPool) worker() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker panicked: %v\n%s", r, debug.Stack()))
}
wp.activeWorkers.Add(-1)
wp.wg.Done()
// Создаём нового воркера вместо упавшего
if wp.activeWorkers.Load() < wp.maxWorkers {
wp.addWorker()
}
}
}()
for {
select {
case <-wp.stopChan:
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
default:
task := wp.tasks.Dequeue()
if task == nil {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Проверяем специальную задачу остановки
if len(task.ID) >= 12 && task.ID[:12] == "stop_worker_" {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Выполняем задачу
startTime := time.Now().UnixMilli()
err := task.Execute()
duration := time.Now().UnixMilli() - startTime
if err != nil {
wp.failedTasks.Add(1)
if wp.logger != nil && duration > 100 {
wp.logger.Warn(fmt.Sprintf("Task %s failed after %dms: %v", task.ID, duration, err))
}
} else {
wp.completedTasks.Add(1)
wp.lastCompleteTime.Store(time.Now().UnixMilli())
if wp.logger != nil && duration > 1000 {
wp.logger.Debug(fmt.Sprintf("Task %s completed in %dms", task.ID, duration))
}
}
}
}
}
// Submit отправляет задачу в пул (lock-free, неблокирующий)
func (wp *WorkerPool) Submit(task *Task) error {
if task.Execute == nil {
return fmt.Errorf("task execute function is nil")
}
task.CreatedAt = time.Now().UnixMilli()
if !wp.tasks.Enqueue(task) {
wp.rejectedTasks.Add(1)
return fmt.Errorf("failed to enqueue task (queue full)")
}
wp.submittedTasks.Add(1)
wp.lastSubmitTime.Store(task.CreatedAt)
// Асинхронно добавляем воркера при необходимости
if wp.activeWorkers.Load() == 0 {
go wp.addWorker()
}
return nil
}
// SubmitFunc отправляет функцию как задачу (lock-free)
func (wp *WorkerPool) SubmitFunc(id string, fn func() error) error {
return wp.Submit(&Task{
ID: id,
Execute: fn,
})
}
// GetStats возвращает статистику пула (lock-free)
func (wp *WorkerPool) GetStats() map[string]interface{} {
return map[string]interface{}{
"max_workers": wp.maxWorkers,
"active_workers": wp.activeWorkers.Load(),
"queue_size": wp.tasks.Size(),
"submitted_tasks": wp.submittedTasks.Load(),
"completed_tasks": wp.completedTasks.Load(),
"failed_tasks": wp.failedTasks.Load(),
"rejected_tasks": wp.rejectedTasks.Load(),
"last_submit_time": wp.lastSubmitTime.Load(),
"last_complete_time": wp.lastCompleteTime.Load(),
}
}
// Stop останавливает пул воркеров
func (wp *WorkerPool) Stop() {
close(wp.stopChan)
// Ждём завершения всех воркеров с таймаутом
done := make(chan struct{})
go func() {
wp.wg.Wait()
close(done)
}()
select {
case <-done:
if wp.logger != nil {
wp.logger.Debug("Worker pool stopped gracefully")
}
case <-time.After(10 * time.Second):
if wp.logger != nil {
wp.logger.Warn("Worker pool stop timeout, forcing shutdown")
}
}
}