248 lines
5.1 KiB
Go
248 lines
5.1 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/adapter"
|
|
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/models"
|
|
)
|
|
|
|
// Worker represents a single job processing worker
|
|
type Worker struct {
|
|
ID int
|
|
QueueNumber int
|
|
InstanceID int64
|
|
db adapter.DBAdapter
|
|
logger adapter.Logger
|
|
jobChan chan models.Job
|
|
shutdown chan struct{}
|
|
wg *sync.WaitGroup
|
|
running bool
|
|
mu sync.RWMutex
|
|
lastActivity time.Time
|
|
jobsHandled int64
|
|
timerSeconds int
|
|
fetchSize int
|
|
}
|
|
|
|
// Stats holds worker statistics
|
|
type Stats struct {
|
|
LastActivity time.Time
|
|
JobsHandled int64
|
|
Running bool
|
|
}
|
|
|
|
// Config holds worker configuration
|
|
type Config struct {
|
|
ID int
|
|
QueueNumber int
|
|
InstanceID int64
|
|
DBAdapter adapter.DBAdapter
|
|
Logger adapter.Logger
|
|
BufferSize int
|
|
TimerSeconds int
|
|
FetchSize int
|
|
}
|
|
|
|
// New creates a new worker
|
|
func New(cfg Config) *Worker {
|
|
return &Worker{
|
|
ID: cfg.ID,
|
|
QueueNumber: cfg.QueueNumber,
|
|
InstanceID: cfg.InstanceID,
|
|
db: cfg.DBAdapter,
|
|
logger: cfg.Logger.With("worker_id", cfg.ID).With("queue", cfg.QueueNumber),
|
|
jobChan: make(chan models.Job, cfg.BufferSize),
|
|
shutdown: make(chan struct{}),
|
|
wg: &sync.WaitGroup{},
|
|
timerSeconds: cfg.TimerSeconds,
|
|
fetchSize: cfg.FetchSize,
|
|
}
|
|
}
|
|
|
|
// Start begins the worker processing loop
|
|
func (w *Worker) Start(ctx context.Context) error {
|
|
w.mu.Lock()
|
|
if w.running {
|
|
w.mu.Unlock()
|
|
return fmt.Errorf("worker %d already running", w.ID)
|
|
}
|
|
w.running = true
|
|
w.mu.Unlock()
|
|
|
|
w.logger.Info("worker starting")
|
|
|
|
w.wg.Add(1)
|
|
go w.processLoop(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully stops the worker
|
|
func (w *Worker) Stop() error {
|
|
w.mu.Lock()
|
|
if !w.running {
|
|
w.mu.Unlock()
|
|
return nil
|
|
}
|
|
w.mu.Unlock()
|
|
|
|
w.logger.Info("worker stopping")
|
|
close(w.shutdown)
|
|
w.wg.Wait()
|
|
|
|
w.mu.Lock()
|
|
w.running = false
|
|
w.mu.Unlock()
|
|
|
|
w.logger.Info("worker stopped")
|
|
return nil
|
|
}
|
|
|
|
// AddJob adds a job to the worker's queue
|
|
func (w *Worker) AddJob(job models.Job) error {
|
|
select {
|
|
case w.jobChan <- job:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("worker %d job channel is full", w.ID)
|
|
}
|
|
}
|
|
|
|
// processLoop is the main worker processing loop
|
|
func (w *Worker) processLoop(ctx context.Context) {
|
|
defer w.wg.Done()
|
|
defer w.recoverPanic()
|
|
|
|
timer := time.NewTimer(time.Duration(w.timerSeconds) * time.Second)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case job := <-w.jobChan:
|
|
w.updateActivity()
|
|
w.processJobs(ctx, &job)
|
|
|
|
case <-timer.C:
|
|
// Timer expired - fetch jobs from database
|
|
if w.timerSeconds > 0 {
|
|
w.updateActivity()
|
|
w.processJobs(ctx, nil)
|
|
}
|
|
timer.Reset(time.Duration(w.timerSeconds) * time.Second)
|
|
|
|
case <-w.shutdown:
|
|
w.logger.Info("worker shutdown signal received")
|
|
return
|
|
|
|
case <-ctx.Done():
|
|
w.logger.Info("worker context cancelled")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// processJobs processes jobs from the queue
|
|
func (w *Worker) processJobs(ctx context.Context, specificJob *models.Job) {
|
|
defer w.recoverPanic()
|
|
|
|
for i := 0; i < w.fetchSize; i++ {
|
|
var jobID int64
|
|
|
|
if specificJob != nil && specificJob.ID > 0 {
|
|
jobID = specificJob.ID
|
|
specificJob = nil // Only process once
|
|
} else {
|
|
// Fetch next job from database
|
|
var err error
|
|
jobID, err = w.fetchNextJob(ctx)
|
|
if err != nil {
|
|
w.logger.Error("failed to fetch job", "error", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if jobID <= 0 {
|
|
// No more jobs
|
|
return
|
|
}
|
|
|
|
// Run the job
|
|
if err := w.runJob(ctx, jobID); err != nil {
|
|
w.logger.Error("failed to run job", "job_id", jobID, "error", err)
|
|
} else {
|
|
w.jobsHandled++
|
|
}
|
|
}
|
|
}
|
|
|
|
// fetchNextJob fetches the next job from the queue
|
|
func (w *Worker) fetchNextJob(ctx context.Context) (int64, error) {
|
|
var retval int
|
|
var errmsg string
|
|
var jobID int64
|
|
|
|
err := w.db.QueryRow(ctx,
|
|
"SELECT p_retval, p_errmsg, p_job_id FROM broker_get($1, $2)",
|
|
w.QueueNumber, w.InstanceID,
|
|
).Scan(&retval, &errmsg, &jobID)
|
|
|
|
if err != nil {
|
|
return 0, fmt.Errorf("query error: %w", err)
|
|
}
|
|
|
|
if retval > 0 {
|
|
return 0, fmt.Errorf("broker_get error: %s", errmsg)
|
|
}
|
|
|
|
return jobID, nil
|
|
}
|
|
|
|
// runJob executes a job
|
|
func (w *Worker) runJob(ctx context.Context, jobID int64) error {
|
|
w.logger.Debug("running job", "job_id", jobID)
|
|
|
|
var retval int
|
|
var errmsg string
|
|
|
|
err := w.db.QueryRow(ctx,
|
|
"SELECT p_retval, p_errmsg FROM broker_run($1)",
|
|
jobID,
|
|
).Scan(&retval, &errmsg)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("query error: %w", err)
|
|
}
|
|
|
|
if retval > 0 {
|
|
return fmt.Errorf("broker_run error: %s", errmsg)
|
|
}
|
|
|
|
w.logger.Debug("job completed", "job_id", jobID)
|
|
return nil
|
|
}
|
|
|
|
// updateActivity updates the last activity timestamp
|
|
func (w *Worker) updateActivity() {
|
|
w.mu.Lock()
|
|
w.lastActivity = time.Now()
|
|
w.mu.Unlock()
|
|
}
|
|
|
|
// GetStats returns worker statistics
|
|
func (w *Worker) GetStats() (lastActivity time.Time, jobsHandled int64, running bool) {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
return w.lastActivity, w.jobsHandled, w.running
|
|
}
|
|
|
|
// recoverPanic recovers from panics in the worker
|
|
func (w *Worker) recoverPanic() {
|
|
if r := recover(); r != nil {
|
|
w.logger.Error("worker panic recovered", "panic", r)
|
|
}
|
|
}
|