workerpool

package module
v2.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 16 Imported by: 0

README

workerpool

Go Version License Build Status Go Report Card Go Reference

Пул воркеров с изоляцией по тенантам: общий глобальный пул горутин, per-tenant лимит конкурентности через взвешенный семафор, повторные попытки с экспоненциальным backoff и jitter, трассировка/метрики через OpenTelemetry и корректный graceful shutdown.

cfg := workerpool.Config{
	WorkerCount:           64,
	TaskQueueSize:         512,
	TenantQueueSize:       32,
	GracefulTimeout:       30 * time.Second,
	TaskTimeout:           2 * time.Minute,
	TenantRefreshInterval: 30 * time.Second,
	RetryPolicy: workerpool.RetryPolicy{
		Attempts: workerpool.AttemptsConfig{
			Count:    3,
			MinDelay: time.Second,
			MaxDelay: 30 * time.Second,
		},
	},
}

manager, err := workerpool.NewWorkerManager(workerpool.WorkerManagerParams{
	TenantProvider: myProvider, // реализует workerpool.TenantProvider
	Config:         cfg,
})
if err != nil {
	log.Fatal(err)
}

if err := manager.Start(); err != nil {
	log.Fatal(err)
}
defer manager.Stop()

🚀 Быстрый старт

go get github.com/alfzs/workerpool/v2
package main

import (
	"context"
	"log"
	"log/slog"
	"time"

	"github.com/google/uuid"

	"github.com/alfzs/workerpool/v2"
)

func main() {
	cfg := workerpool.Config{
		WorkerCount:           64,
		TaskQueueSize:         512,
		TenantQueueSize:       32,
		GracefulTimeout:       30 * time.Second,
		TaskTimeout:           2 * time.Minute,
		TenantRefreshInterval: 30 * time.Second,
		RetryPolicy: workerpool.RetryPolicy{
			Attempts: workerpool.AttemptsConfig{
				Count:    3,
				MinDelay: time.Second,
				MaxDelay: 30 * time.Second,
			},
		},
	}

	// NewWorkerManager вызывает cfg.Validate() внутри.
	manager, err := workerpool.NewWorkerManager(workerpool.WorkerManagerParams{
		TenantProvider: myProvider, // реализует workerpool.TenantProvider
		Config:         cfg,
	})
	if err != nil {
		log.Fatal(err)
	}

	if err := manager.Start(); err != nil {
		log.Fatal(err)
	}
	defer manager.Stop()

	registry := workerpool.NewExecutorRegistry()
	registry.MustRegister("sync_orders", &SyncOrdersExecutor{})

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	exec, _ := registry.Get("sync_orders")
	err = manager.SubmitTask(tenantID, workerpool.Task{
		Ctx:      ctx,
		TaskID:   uuid.New(),
		TenantID: tenantID,
		Executor: exec,
		Complete: func(err error) {
			cancel()
			if err != nil {
				slog.Error("task failed", "error", err)
			}
		},
	})
	if err != nil {
		cancel()
		log.Println("submit failed:", err)
	}
}

✨ Возможности

Изоляция по тенантам

Каждый тенант получает собственную горутину-диспетчер и взвешенный семафор размером Tenant.WorkerLimit(). Задачи тенанта проходят через семафор диспетчера, прежде чем попасть в общий глобальный пул — итоговая конкурентность ограничена min(сумма лимитов тенантов, Config.WorkerCount). Список тенантов периодически обновляется через TenantProvider (интервал — Config.TenantRefreshInterval), а изменение лимита воркеров применяется на следующем цикле обновления без прерывания уже выполняющихся задач.

Общий глобальный пул

Config.WorkerCount горутин обслуживают задачи всех тенантов одновременно. Задачи, поступающие при заполненной очереди (Config.TaskQueueSize), отклоняются немедленно — SubmitTask и внутренняя передача в пул неблокирующие.

Повторные попытки с backoff и jitter

Config.RetryPolicy управляет количеством попыток и экспоненциальным backoff между ними с полным jitter (равномерное случайное значение в [0, cap]) для предотвращения thundering herd при массовых сбоях. При использовании River как job store установите RetryPolicy.Attempts.Count = 1, чтобы не дублировать ретраи River.

Реестр executor'ов

ExecutorRegistry сопоставляет строковые ключи (хранящиеся, например, в job store) с реализациями TaskExecutor — задание в базе хранит только ключ, а конкретная реализация разрешается во время исполнения.

Трассировка и метрики через OpenTelemetry

Вокруг каждого вызова Executor.Execute создаётся OTel-span. Если Task.Ctx уже содержит активный span вызывающего кода (River worker, HTTP-обработчик), span пула автоматически становится его дочерним. Настройте глобальные провайдеры через otel.SetTracerProvider и otel.SetMeterProvider до старта менеджера; без настройки используется noop-реализация без накладных расходов.

Graceful shutdown

Stop() отменяет контексты обновлятора и всех диспетчеров, дожидается выхода горутин, затем останавливает пул. Пул сливает очередь в течение Config.GracefulTimeout; по истечении таймаута контексты всех активных задач отменяются принудительно.

Health-снимки

WorkerManager.Health() возвращает снимок состояния (глубина очередей, количество тенантов, лимиты) для liveness/readiness-проб и дашбордов.

Полная схема архитектуры и интеграции с River — в ARCHITECTURE.md.

🤝 Contributing

Правила участия — в CONTRIBUTING.md.

📄 License

Проект распространяется по лицензии MIT — см. LICENSE.

Documentation

Overview

Package workerpool реализует пул воркеров с изоляцией по тенантам, ограничением конкурентности на тенанта, трассировкой через OpenTelemetry и корректным graceful shutdown.

Компоненты

Пакет состоит из трёх взаимодействующих частей:

  • WorkerManager — управление жизненным циклом тенантов и контроль конкурентности через взвешенный семафор.
  • Pool — общий пул горутин фиксированного размера: исполняет задачи, снимает OTel-метрики и трассировку, реализует повторные попытки.
  • ExecutorRegistry — сопоставляет строковые ключи (хранящиеся в job store) с конкретными реализациями TaskExecutor.

Для персистентного планирования и multi-instance развёртывания пакет спроектирован под использование совместно с River (github.com/riverqueue/river) в роли job store. Полная схема интеграции описана в ARCHITECTURE.md.

Быстрый старт

cfg := workerpool.Config{
	WorkerCount:           64,
	TaskQueueSize:         512,
	TenantQueueSize:       32,
	GracefulTimeout:       30 * time.Second,
	TaskTimeout:           2 * time.Minute,
	TenantRefreshInterval: 30 * time.Second,
	RetryPolicy: workerpool.RetryPolicy{
		Attempts: workerpool.AttemptsConfig{
			Count:    3,
			MinDelay: time.Second,
			MaxDelay: 30 * time.Second,
		},
	},
}

// NewWorkerManager вызывает cfg.Validate() внутри.
manager, err := workerpool.NewWorkerManager(workerpool.WorkerManagerParams{
	TenantProvider: myProvider, // реализует workerpool.TenantProvider
	Config:         cfg,
})
if err != nil {
	log.Fatal(err)
}

if err := manager.Start(); err != nil {
	log.Fatal(err)
}
defer manager.Stop()

// Реестр executor'ов: ключи соответствуют полю ExecutorKey в job store.
registry := workerpool.NewExecutorRegistry()
registry.MustRegister("sync_orders", &SyncOrdersExecutor{})

// Разовая задача для конкретного тенанта.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
exec, _ := registry.Get("sync_orders")
err = manager.SubmitTask(tenantID, workerpool.Task{
	Ctx:      ctx,
	TaskID:   uuid.New(),
	TenantID: tenantID,
	Executor: exec,
	Complete: func(err error) {
		cancel()
		if err != nil {
			slog.ErrorContext(ctx, "task failed", "error", err)
		}
	},
})
if err != nil {
	cancel()
	log.Println("submit failed:", err)
}

Модель конкурентности

  • Глобальный пул: WorkerCount горутин, разделяемых между всеми тенантами.
  • На тенанта: одна горутина-диспетчер и взвешенный семафор размером Tenant.WorkerLimit(). Не более Limit задач тенанта выполняются в пуле одновременно.
  • Итоговая конкурентность ≤ min(сумма лимитов тенантов, WorkerCount).

Логирование и трассировка

Логирование ведётся через WorkerManagerParams.Logger; если не задан — через slog.Default(). Внутренние логи пакета используют *Context-варианты (slog.ErrorContext и т.п.) с наиболее релевантным доступным контекстом (Task.Ctx для операций конкретной задачи), чтобы обработчик логгера мог извлечь trace_id/span_id из активного OTel-спана и связать лог с трассировкой (например через go.opentelemetry.io/contrib/bridges/otelslog). Вызывающему коду рекомендуется следовать тому же соглашению в собственных обработчиках Task.Complete.

OTel-span создаётся вокруг каждого вызова Executor.Execute. Если Task.Ctx содержит активный span вызывающего кода (River worker, HTTP-обработчик), span пула автоматически становится его дочерним. Настройте глобальные провайдеры через otel.SetTracerProvider и otel.SetMeterProvider до старта менеджера; при отсутствии настройки используется noop-реализация.

Корректное завершение

Stop() отменяет контексты обновлятора и всех диспетчеров, ожидает выхода горутин, затем останавливает пул. Пул сливает очередь в течение GracefulTimeout; по истечении — принудительно отменяет контексты всех активных задач.

Безопасность конкурентного доступа

Все экспортированные методы безопасны для конкурентного использования.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrPoolStopping возвращается addTask, если пул уже в процессе остановки.
	// Постоянная ошибка — повторная попытка не поможет.
	ErrPoolStopping = errors.New("workerpool: pool is stopping")

	// ErrQueueFull возвращается addTask, если очередь глобального пула
	// заполнена. Временная ошибка — вызывающий код может повторить попытку.
	ErrQueueFull = errors.New("workerpool: pool queue full")
)

Ошибки глобального пула. Проверяются через errors.Is.

View Source
var (
	// ErrTenantNotFound возвращается SubmitTask, если тенант с указанным ID
	// отсутствует в последнем снимке TenantProvider.List. Постоянная
	// ошибка, пока тенант не появится в очередном обновлении.
	ErrTenantNotFound = errors.New("workerpool: tenant not found")

	// ErrTenantShuttingDown возвращается SubmitTask, если тенант уже удалён
	// из списка TenantProvider и его диспетчер завершается. Постоянная ошибка.
	ErrTenantShuttingDown = errors.New("workerpool: tenant is shutting down")

	// ErrTenantQueueFull возвращается SubmitTask, если буфер задач тенанта
	// заполнен. Временная ошибка — вызывающий код может повторить попытку.
	ErrTenantQueueFull = errors.New("workerpool: tenant queue full")

	// ErrDispatcherStopped передаётся через Task.Complete, если диспетчер
	// тенанта остановился до захвата слота конкурентности для задачи.
	ErrDispatcherStopped = errors.New("workerpool: dispatcher stopped before task could run")
)

Ошибки WorkerManager. Проверяются через errors.Is.

View Source
var (
	// ErrTaskNilContext возвращается SubmitTask, если Task.Ctx равен nil.
	ErrTaskNilContext = errors.New("workerpool: task context is nil")

	// ErrTaskNoExecutor возвращается SubmitTask, если у задачи не задан ни
	// Task.Executor, ни Task.ExecutorKey.
	ErrTaskNoExecutor = errors.New("workerpool: task must set either executor or executor key")

	// ErrNoExecutorRegistry возвращается SubmitTask, если у задачи задан
	// Task.ExecutorKey, но WorkerManagerParams.ExecutorRegistry не настроен.
	ErrNoExecutorRegistry = errors.New("workerpool: task sets executor key but no executor registry is configured")
)

Ошибки валидации Task в SubmitTask. Проверяются через errors.Is.

View Source
var (
	// ErrEmptyExecutorKey возвращается Register при попытке зарегистрировать
	// executor с пустым ключом.
	ErrEmptyExecutorKey = errors.New("workerpool: executor key cannot be empty")

	// ErrNilExecutor возвращается Register при попытке зарегистрировать nil
	// в качестве executor'а.
	ErrNilExecutor = errors.New("workerpool: executor cannot be nil")

	// ErrExecutorAlreadyRegistered возвращается Register, если ключ уже занят.
	ErrExecutorAlreadyRegistered = errors.New("workerpool: executor already registered")

	// ErrExecutorNotFound возвращается Get, если ключ не зарегистрирован.
	ErrExecutorNotFound = errors.New("workerpool: executor not found")
)

Ошибки ExecutorRegistry. Проверяются через errors.Is.

Functions

This section is empty.

Types

type AttemptsConfig

type AttemptsConfig struct {
	// Count — суммарное количество попыток, включая первую.
	// При использовании River установите 1, чтобы не дублировать ретраи.
	// Должно быть >= 1.
	Count int `yaml:"count" env-default:"3"`

	// MinDelay — начальная пауза перед первой повторной попыткой.
	MinDelay time.Duration `yaml:"min_delay" env-default:"1s"`

	// MaxDelay — верхняя граница экспоненциального backoff.
	// Должно быть >= MinDelay.
	MaxDelay time.Duration `yaml:"max_delay" env-default:"30s"`
}

AttemptsConfig определяет параметры повторных попыток.

type Config

type Config struct {
	// WorkerCount — количество горутин в глобальном пуле исполнения.
	// Все тенанты разделяют эту ёмкость. Должно быть > 0.
	WorkerCount int `yaml:"worker_count" env-default:"256"`

	// TaskQueueSize — ёмкость канала глобального пула.
	// Задачи, поступающие при заполненном канале, отклоняются немедленно.
	// Должно быть > 0.
	TaskQueueSize int `yaml:"task_queue_size" env-default:"512"`

	// TenantQueueSize — ёмкость буфера задач каждого тенанта.
	// Устанавливается однократно при создании тенанта; не изменяется
	// при обновлении лимита воркеров. Должно быть > 0.
	TenantQueueSize int `yaml:"tenant_queue_size" env-default:"64"`

	// GracefulTimeout — максимальное время ожидания завершения активных задач
	// при вызове Stop(). По истечении таймаута все активные контексты задач
	// принудительно отменяются. Должно быть > 0.
	GracefulTimeout time.Duration `yaml:"graceful_timeout" env-default:"30s"`

	// TaskTimeout — дедлайн по умолчанию для одного выполнения задачи.
	// Вызывающий код может задать более жёсткий дедлайн через Task.Ctx.
	// Должно быть > 0.
	TaskTimeout time.Duration `yaml:"task_timeout" env-default:"5m"`

	// TenantRefreshInterval — как часто менеджер перезапрашивает список
	// тенантов у TenantProvider. Должно быть > 0.
	TenantRefreshInterval time.Duration `yaml:"tenant_refresh_interval" env-default:"30s"`

	// RetryPolicy задаёт поведение при повторных попытках выполнения задачи.
	// При использовании River как job store River управляет внешними ретраями;
	// в этом случае установите Attempts.Count = 1.
	RetryPolicy RetryPolicy `yaml:"retry_policy"`
}

Config содержит все настраиваемые параметры пула воркеров. Нулевые значения недопустимы — всегда вызывайте Validate() перед передачей конфига в NewWorkerManager.

func (Config) Validate

func (c Config) Validate() error

Validate проверяет корректность всех обязательных полей. Возвращает объединённую ошибку со всеми нарушениями сразу, чтобы вызывающий код мог вывести полный список проблем в одном сообщении.

type ExecutorRegistry

type ExecutorRegistry struct {
	// contains filtered or unexported fields
}

ExecutorRegistry сопоставляет строковые ключи с реализациями TaskExecutor. Служит мостом между job store (например, River/Postgres) и пулом: задание, сохранённое в базе, хранит только строковый ключ executor'а; реестр разрешает его в конкретную реализацию во время исполнения.

Все методы безопасны для конкурентного использования. Нулевое значение ExecutorRegistry{} готово к использованию — вызывать NewExecutorRegistry не обязательно.

Example

ExampleExecutorRegistry demonstrates registering a TaskExecutor under a string key and resolving it back at execution time — the pattern used when a job store (e.g. River) persists only the key.

package main

import (
	"context"
	"fmt"

	"github.com/google/uuid"

	workerpool "github.com/alfzs/workerpool/v2"
)

// echoExecutor is a minimal TaskExecutor that always succeeds.
type echoExecutor struct{}

func (echoExecutor) Execute(_ context.Context, _ uuid.UUID, _ int) error { return nil }

func main() {
	registry := workerpool.NewExecutorRegistry()
	registry.MustRegister("echo", echoExecutor{})

	exec, err := registry.Get("echo")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(exec.Execute(context.Background(), uuid.Nil, 0))
}
Output:
<nil>

func NewExecutorRegistry

func NewExecutorRegistry() *ExecutorRegistry

NewExecutorRegistry создаёт пустой реестр.

func (*ExecutorRegistry) Get

func (r *ExecutorRegistry) Get(key string) (TaskExecutor, error)

Get возвращает executor, зарегистрированный под ключом key. Возвращает ошибку, если ключ не найден.

func (*ExecutorRegistry) Keys

func (r *ExecutorRegistry) Keys() []string

Keys возвращает снимок всех зарегистрированных ключей в произвольном порядке.

func (*ExecutorRegistry) MustRegister

func (r *ExecutorRegistry) MustRegister(key string, exec TaskExecutor)

MustRegister аналогичен Register, но паникует при ошибке. Предназначен для использования в init() или TestMain, где ошибка регистрации является программной ошибкой и не должна возникать в рантайме.

func (*ExecutorRegistry) Register

func (r *ExecutorRegistry) Register(key string, exec TaskExecutor) error

Register регистрирует executor под заданным ключом. Возвращает ошибку, если ключ пустой, executor равен nil или ключ уже зарегистрирован. Ключи чувствительны к регистру.

type HealthStatus

type HealthStatus struct {
	// Healthy равен true, пока менеджер работает и не находится в процессе
	// остановки. Используйте это поле как основной сигнал живости.
	Healthy bool `json:"healthy"`

	// Stopping равен true после вызова Stop().
	Stopping bool `json:"stopping"`

	// PoolQueueDepth — количество задач, ожидающих в очереди глобального пула.
	// Значение, близкое к PoolQueueCapacity, сигнализирует о backpressure.
	PoolQueueDepth int `json:"pool_queue_depth"`

	// PoolQueueCapacity — максимальная ёмкость очереди глобального пула
	// (Config.TaskQueueSize).
	PoolQueueCapacity int `json:"pool_queue_capacity"`

	// PoolWorkerCount — количество горутин-воркеров в глобальном пуле
	// (Config.WorkerCount).
	PoolWorkerCount int `json:"pool_worker_count"`

	// TenantCount — количество тенантов, отслеживаемых менеджером на момент снимка.
	TenantCount int `json:"tenant_count"`

	// Tenants содержит детальную информацию по каждому тенанту.
	// Порядок элементов не определён.
	Tenants []TenantHealth `json:"tenants"`
}

HealthStatus — снимок внутреннего состояния WorkerManager в конкретный момент времени. Предназначен для liveness/readiness-проб и операционных дашбордов.

Tenants[].TenantID раскрывает идентификаторы активных тенантов. Сам пакет не выполняет сетевого взаимодействия, поэтому это не уязвимость библиотеки, но если HealthStatus транслируется напрямую в неаутентифицированный HTTP-эндпоинт (/health, /ready), это позволяет перечислить тенантов извне (см. docs/SECURITY_AUDIT.md, находка №3). Аутентифицируйте такой эндпоинт или уберите/агрегируйте TenantID перед отдачей наружу.

type JitterConfig

type JitterConfig struct {
	// MinDelay — минимальная задержка jitter. Должно быть >= 0.
	MinDelay time.Duration `yaml:"min_delay" env-default:"0s"`

	// MaxDelay — максимальная задержка jitter. Должно быть >= MinDelay.
	MaxDelay time.Duration `yaml:"max_delay" env-default:"5s"`
}

JitterConfig определяет границы случайной задержки перед первым запуском.

type RetryPolicy

type RetryPolicy struct {
	// Jitter — случайная задержка перед самым первым выполнением задачи,
	// используемая для размазывания нагрузки по кластеру.
	Jitter JitterConfig `yaml:"jitter"`

	// Attempts — параметры повторных попыток при ошибках.
	Attempts AttemptsConfig `yaml:"attempts"`
}

RetryPolicy определяет поведение при повторных попытках внутри пула.

type Task

type Task struct {
	// Ctx — контекст задачи с дедлайном и сигналом отмены.
	// Если Ctx содержит активный OTel-span (например, из River worker или
	// HTTP-обработчика), span пула станет его дочерним — иерархия
	// трассировки выстраивается автоматически через контекст.
	Ctx context.Context //nolint:containedctx // Task is a data envelope passed by value through channels, not a long-lived object; ctx must travel with it

	// TaskID — уникальный идентификатор конкретного запуска
	// (например, ID job в River). Используется в логах и атрибутах span.
	TaskID uuid.UUID

	// TenantID — идентификатор тенанта; используется для маршрутизации,
	// логирования и атрибутов метрик.
	TenantID uuid.UUID

	// ExecutorKey — строковый ключ для разрешения executor'а через
	// ExecutorRegistry. Заполняется, если executor хранится в реестре
	// (типичный случай при использовании River как job store).
	ExecutorKey string

	// Executor — реализация, вызываемая напрямую. Имеет приоритет над
	// ExecutorKey. Удобна для разовых задач без регистрации в реестре.
	Executor TaskExecutor

	// Complete вызывается ровно один раз после завершения задачи —
	// успешного, после исчерпания повторных попыток, после паники или
	// при остановке диспетчера. err == nil означает успех.
	Complete func(err error)
}

Task — единица работы, передаваемая в пул. Поля Ctx, TaskID, TenantID, Executor заполняются вызывающим кодом до Submit; Complete оборачивается внутри диспетчера и не должен изменяться после передачи задачи.

type TaskExecutor

type TaskExecutor interface {
	// Execute выполняет работу. Не-nil ошибка инициирует повторную попытку
	// согласно RetryPolicy пула. workerID идентифицирует слот глобального
	// пула, исполняющий задачу (начинается с 0).
	Execute(ctx context.Context, tenantID uuid.UUID, workerID int) error
}

TaskExecutor выполняет фактическую работу одного запуска задачи. Реализация должна соблюдать отмену контекста и быть безопасна для конкурентного вызова из нескольких горутин одновременно.

Значение паники, восстановленной пулом при выполнении Execute, и возвращаемая из Execute ошибка логируются пулом целиком, без редактирования (см. docs/SECURITY_AUDIT.md, находка №2). Реализации не должны встраивать секреты или персональные данные непосредственно в текст panic() или в возвращаемые ошибки, если логи пула могут быть доступны сторонам, для которых эти данные не предназначены.

type Tenant

type Tenant interface {
	// ID возвращает уникальный идентификатор тенанта. Никогда не должен
	// возвращать uuid.Nil.
	ID() uuid.UUID

	// WorkerLimit возвращает максимальное количество одновременно
	// выполняемых задач для этого тенанта. Изменение значения вступает в силу
	// на следующем цикле обновления.
	WorkerLimit() int
}

Tenant представляет клиента, чьи задачи выполняются в рамках изолированного лимита конкурентности. Реализация должна быть безопасна для конкурентного использования.

type TenantHealth

type TenantHealth struct {
	// TenantID — идентификатор тенанта.
	TenantID uuid.UUID `json:"tenant_id"`

	// QueueDepth — количество задач, ожидающих в локальном буфере тенанта.
	// Значение, близкое к QueueCapacity, означает, что SubmitTask начнёт
	// возвращать ошибку «очередь заполнена» для этого тенанта.
	QueueDepth int `json:"queue_depth"`

	// QueueCapacity — полная ёмкость буфера задач тенанта
	// (Config.TenantQueueSize).
	QueueCapacity int `json:"queue_capacity"`

	// WorkerLimit — максимальное количество одновременных задач тенанта
	// согласно последнему ответу TenantProvider.
	WorkerLimit int `json:"worker_limit"`
}

TenantHealth — состояние одного тенанта внутри HealthStatus.

type TenantProvider

type TenantProvider interface {
	// List возвращает всех тенантов, которым должны быть назначены
	// воркеры. Тенант, отсутствующий в результате, будет удалён из
	// внутреннего состояния менеджера.
	List(ctx context.Context) ([]Tenant, error)
}

TenantProvider поставляет список тенантов, которым должны быть назначены воркеры; что делает тенанта пригодным для этого списка — решает реализация, workerpool об этом не судит. Реализация должна быть безопасна для конкурентного использования и кешировать результаты — менеджер вызывает List на каждом тике обновления.

type WorkerManager

type WorkerManager struct {
	// contains filtered or unexported fields
}

WorkerManager управляет выполнением задач для нескольких тенантов. Поддерживает по одной горутине-диспетчеру на тенант, которая обеспечивает соблюдение лимита конкурентности через взвешенный семафор перед передачей задачи в общий глобальный пул.

func NewWorkerManager

func NewWorkerManager(p WorkerManagerParams) (*WorkerManager, error)

NewWorkerManager создаёт WorkerManager. Возвращает ошибку, если Config не прошёл валидацию или не удалось инициализировать глобальный пул.

Логирование: если WorkerManagerParams.Logger не задан, используется slog.Default() — единый экземпляр разделяется менеджером и внутренним пулом (у каждого свой атрибут component). OTel-трассировка и метрики используют глобальные провайдеры: настройте otel.SetTracerProvider и otel.SetMeterProvider до старта менеджера.

Example

ExampleNewWorkerManager builds a manager with a single tenant, submits one task, and waits for it to complete via the Task.Complete callback.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/google/uuid"

	workerpool "github.com/alfzs/workerpool/v2"
)

// echoExecutor is a minimal TaskExecutor that always succeeds.
type echoExecutor struct{}

func (echoExecutor) Execute(_ context.Context, _ uuid.UUID, _ int) error { return nil }

// staticTenant is a fixed-limit Tenant implementation for demonstration
// purposes; a real implementation would back WorkerLimit with a value
// that can change between refresh cycles.
type staticTenant struct {
	id    uuid.UUID
	limit int
}

func (t staticTenant) ID() uuid.UUID    { return t.id }
func (t staticTenant) WorkerLimit() int { return t.limit }

// staticProvider is a TenantProvider returning a fixed tenant set; a real
// implementation should cache its own results, since List is called
// on every TenantRefreshInterval tick.
type staticProvider struct{ tenants []workerpool.Tenant }

func (p staticProvider) List(_ context.Context) ([]workerpool.Tenant, error) {
	return p.tenants, nil
}

func main() {
	tenantID := uuid.New()
	provider := staticProvider{tenants: []workerpool.Tenant{staticTenant{id: tenantID, limit: 1}}}

	cfg := workerpool.Config{
		WorkerCount:           2,
		TaskQueueSize:         8,
		TenantQueueSize:       8,
		GracefulTimeout:       time.Second,
		TaskTimeout:           time.Second,
		TenantRefreshInterval: time.Hour,
		RetryPolicy: workerpool.RetryPolicy{
			Attempts: workerpool.AttemptsConfig{
				Count:    1,
				MinDelay: time.Millisecond,
				MaxDelay: time.Millisecond,
			},
		},
	}

	// NewWorkerManager calls cfg.Validate() internally.
	manager, err := workerpool.NewWorkerManager(workerpool.WorkerManagerParams{
		TenantProvider: provider,
		Config:         cfg,
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	// Start loads the initial tenant set synchronously, so the tenant is
	// already known by the time SubmitTask is called below.
	if err := manager.Start(); err != nil {
		fmt.Println(err)
		return
	}
	defer manager.Stop()

	done := make(chan error, 1)

	err = manager.SubmitTask(tenantID, workerpool.Task{
		Ctx:      context.Background(),
		TaskID:   uuid.New(),
		TenantID: tenantID,
		Executor: echoExecutor{},
		Complete: func(err error) { done <- err },
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	select {
	case err := <-done:
		fmt.Println(err)
	case <-time.After(5 * time.Second):
		fmt.Println("timed out waiting for task completion")
	}
}
Output:
<nil>

func (*WorkerManager) GetTenantIDs

func (w *WorkerManager) GetTenantIDs() []uuid.UUID

GetTenantIDs возвращает снимок идентификаторов всех тенантов, которые сейчас отслеживаются менеджером. Порядок не определён.

func (*WorkerManager) Health

func (w *WorkerManager) Health() HealthStatus

Health возвращает снимок текущего состояния WorkerManager.

Снимок консистентен внутри одного окна RLock, но состояние может измениться сразу после возврата. Метод безопасен для конкурентного вызова и не блокирует приём задач.

func (*WorkerManager) Start

func (w *WorkerManager) Start() error

Start запускает глобальный пул, загружает начальный список тенантов и запускает фоновый цикл обновления. Возвращает ошибку, если первичная загрузка тенантов завершилась неудачей — в этом случае пул останавливается и все ресурсы освобождаются.

func (*WorkerManager) Stop

func (w *WorkerManager) Stop()

Stop выполняет штатное завершение WorkerManager.

Последовательность: отмена контекста обновляторя и всех диспетчеров → ожидание выхода всех горутин → остановка пула (с соблюдением GracefulTimeout и принудительной отменой при его истечении).

Идемпотентен: повторный вызов безопасен.

func (*WorkerManager) SubmitTask

func (w *WorkerManager) SubmitTask(tenantID uuid.UUID, task Task) error

SubmitTask помещает задачу в очередь указанного тенанта.

Возвращает ошибку немедленно, без постановки в очередь, если Task.Ctx равен nil, или если ни Task.Executor, ни разрешимый через WorkerManagerParams.ExecutorRegistry Task.ExecutorKey не заданы — это предотвращает панику глубже в воркере при исполнении задачи.

Вызов неблокирующий: если очередь тенанта заполнена или тенант останавливается, немедленно возвращается ошибка без изменения состояния. Безопасен для конкурентного вызова из нескольких горутин.

type WorkerManagerParams

type WorkerManagerParams struct {
	// TenantProvider поставляет список тенантов, которым нужны воркеры.
	TenantProvider TenantProvider

	// Config содержит все настраиваемые параметры. Config.Validate()
	// вызывается внутри NewWorkerManager — передавать невалидный конфиг
	// не нужно.
	Config Config

	// ExecutorRegistry, если задан, используется SubmitTask для разрешения
	// Task.ExecutorKey в Task.Executor у задач, которые не заполнили
	// Executor напрямую. Опционально: если задачи всегда приходят с уже
	// заполненным Executor, оставьте nil.
	ExecutorRegistry *ExecutorRegistry

	// Logger используется менеджером и внутренним пулом (с добавленным
	// атрибутом component). Опционально: если не задан, используется
	// slog.Default().
	Logger *slog.Logger
}

WorkerManagerParams содержит все зависимости для NewWorkerManager.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL