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 ¶
var ( // ErrPoolStopping возвращается addTask, если пул уже в процессе остановки. // Постоянная ошибка — повторная попытка не поможет. ErrPoolStopping = errors.New("workerpool: pool is stopping") // ErrQueueFull возвращается addTask, если очередь глобального пула // заполнена. Временная ошибка — вызывающий код может повторить попытку. ErrQueueFull = errors.New("workerpool: pool queue full") )
Ошибки глобального пула. Проверяются через errors.Is.
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.
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.
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.
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.