package handlers import ( "context" "time" "git.threesix.ai/jordan/sp2-verify-1770324794/pkg/logging" "git.threesix.ai/jordan/sp2-verify-1770324794/pkg/redisqueue" ) // AsyncJobProcessor processes jobs from the Redis queue. type AsyncJobProcessor struct { logger *logging.Logger queue *redisqueue.RedisQueue simulationDuration time.Duration pollTimeout time.Duration } // AsyncJobProcessorConfig holds configuration for the async job processor. type AsyncJobProcessorConfig struct { SimulationDuration time.Duration PollTimeout time.Duration } // NewAsyncJobProcessor creates a new async job processor. func NewAsyncJobProcessor(queue *redisqueue.RedisQueue, logger *logging.Logger, cfg AsyncJobProcessorConfig) *AsyncJobProcessor { if cfg.SimulationDuration == 0 { cfg.SimulationDuration = 2 * time.Second } if cfg.PollTimeout == 0 { cfg.PollTimeout = 5 * time.Second } return &AsyncJobProcessor{ logger: logger.WithComponent("async-job-processor"), queue: queue, simulationDuration: cfg.SimulationDuration, pollTimeout: cfg.PollTimeout, } } // Run starts the async job processing loop. // It continuously polls the Redis queue for jobs and processes them. func (p *AsyncJobProcessor) Run(ctx context.Context) { p.logger.Info("async job processor started", "simulation_duration", p.simulationDuration, "poll_timeout", p.pollTimeout, ) for { select { case <-ctx.Done(): p.logger.Info("async job processor stopping") return default: if err := p.processNextJob(ctx); err != nil { if err == redisqueue.ErrNoJob { // No jobs available, continue polling continue } p.logger.Error("error processing job", "error", err) // Brief pause on error to avoid tight loop time.Sleep(time.Second) } } } } // processNextJob dequeues and processes a single job from the Redis queue. func (p *AsyncJobProcessor) processNextJob(ctx context.Context) error { // Dequeue with timeout (blocks until job available or timeout) job, err := p.queue.Dequeue(ctx, p.pollTimeout) if err != nil { return err } p.logger.Info("processing job", "job_id", job.ID, "type", job.Type, "payload", job.Payload, ) // Simulate work select { case <-ctx.Done(): // Context cancelled during work - don't mark as failed, let it be reprocessed p.logger.Warn("job processing interrupted", "job_id", job.ID) return ctx.Err() case <-time.After(p.simulationDuration): // Work completed } // Mark job as completed if err := p.queue.Complete(ctx, job.ID); err != nil { p.logger.Error("failed to complete job", "job_id", job.ID, "error", err, ) return err } p.logger.Info("job completed", "job_id", job.ID) return nil }