feat: mail service
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"galaxy/mail/internal/service/executeattempt"
|
||||
)
|
||||
|
||||
// AttemptExecutionService executes one claimed in-progress attempt.
|
||||
type AttemptExecutionService interface {
|
||||
// Execute runs one claimed attempt through provider execution and durable
|
||||
// state mutation.
|
||||
Execute(context.Context, executeattempt.WorkItem) error
|
||||
}
|
||||
|
||||
// AttemptWorkerPoolConfig stores the dependencies used by AttemptWorkerPool.
|
||||
type AttemptWorkerPoolConfig struct {
|
||||
// Concurrency stores how many workers run concurrently.
|
||||
Concurrency int
|
||||
|
||||
// WorkQueue stores the claimed attempt handoff channel produced by the
|
||||
// scheduler.
|
||||
WorkQueue <-chan executeattempt.WorkItem
|
||||
|
||||
// Service executes one claimed attempt.
|
||||
Service AttemptExecutionService
|
||||
}
|
||||
|
||||
// AttemptWorkerPool executes claimed attempts concurrently.
|
||||
type AttemptWorkerPool struct {
|
||||
concurrency int
|
||||
workQueue <-chan executeattempt.WorkItem
|
||||
service AttemptExecutionService
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewAttemptWorkerPool constructs one attempt worker pool.
|
||||
func NewAttemptWorkerPool(cfg AttemptWorkerPoolConfig, logger *slog.Logger) (*AttemptWorkerPool, error) {
|
||||
switch {
|
||||
case cfg.Concurrency <= 0:
|
||||
return nil, errors.New("new attempt worker pool: concurrency must be positive")
|
||||
case cfg.WorkQueue == nil:
|
||||
return nil, errors.New("new attempt worker pool: nil work queue")
|
||||
case cfg.Service == nil:
|
||||
return nil, errors.New("new attempt worker pool: nil attempt execution service")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return &AttemptWorkerPool{
|
||||
concurrency: cfg.Concurrency,
|
||||
workQueue: cfg.WorkQueue,
|
||||
service: cfg.Service,
|
||||
logger: logger.With("component", "attempt_worker_pool", "concurrency", cfg.Concurrency),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the attempt worker pool and blocks until ctx is canceled or one
|
||||
// worker returns an execution error.
|
||||
func (pool *AttemptWorkerPool) Run(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return errors.New("run attempt worker pool: nil context")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if pool == nil {
|
||||
return errors.New("run attempt worker pool: nil pool")
|
||||
}
|
||||
|
||||
pool.logger.Info("attempt worker pool started")
|
||||
defer pool.logger.Info("attempt worker pool stopped")
|
||||
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
errs := make(chan error, pool.concurrency)
|
||||
var waitGroup sync.WaitGroup
|
||||
|
||||
for index := 0; index < pool.concurrency; index++ {
|
||||
waitGroup.Add(1)
|
||||
go func(workerIndex int) {
|
||||
defer waitGroup.Done()
|
||||
if err := pool.runWorker(runCtx, workerIndex); err != nil {
|
||||
errs <- err
|
||||
}
|
||||
}(index)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
waitGroup.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
<-done
|
||||
return ctx.Err()
|
||||
case err := <-errs:
|
||||
cancel()
|
||||
<-done
|
||||
return err
|
||||
case <-done:
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return errors.New("run attempt worker pool: workers exited without shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func (pool *AttemptWorkerPool) runWorker(ctx context.Context, workerIndex int) error {
|
||||
pool.logger.Debug("attempt worker started", "worker_index", workerIndex)
|
||||
defer pool.logger.Debug("attempt worker stopped", "worker_index", workerIndex)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case item, ok := <-pool.workQueue:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := pool.service.Execute(ctx, item); err != nil {
|
||||
return fmt.Errorf("attempt worker %d: %w", workerIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops the attempt worker pool within ctx. The pool does not own
|
||||
// additional resources beyond its run loop.
|
||||
func (pool *AttemptWorkerPool) Shutdown(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return errors.New("shutdown attempt worker pool: nil context")
|
||||
}
|
||||
if pool == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user