package queue import ( "context" "fmt" "log/slog" "sync" "time" "github.com/google/uuid" ) // MemoryQueue is an in-memory job queue that dispatches jobs to registered handlers // in goroutines. Use this for local development when no database is available. // Implements Producer and JobReader so the service handlers can enqueue and query jobs. type MemoryQueue struct { handlers map[string]Handler jobs map[string]*Job mu sync.RWMutex logger *slog.Logger } // NewMemoryQueue creates an in-memory queue for standalone/development mode. func NewMemoryQueue(logger *slog.Logger) *MemoryQueue { if logger == nil { logger = slog.Default() } return &MemoryQueue{ handlers: make(map[string]Handler), jobs: make(map[string]*Job), logger: logger, } } // RegisterHandler registers a handler for a specific job type. // When Enqueue is called with this job type, the handler runs in a goroutine. func (q *MemoryQueue) RegisterHandler(jobType string, handler Handler) { q.mu.Lock() defer q.mu.Unlock() q.handlers[jobType] = handler q.logger.Info("registered in-memory job handler", "job_type", jobType) } // Enqueue creates a job and immediately dispatches it to the registered handler in a goroutine. func (q *MemoryQueue) Enqueue(ctx context.Context, jobType string, payload map[string]any) (string, error) { return q.EnqueueWithOptions(ctx, Job{ Type: jobType, Payload: payload, }) } // EnqueueWithOptions creates a job with custom options and dispatches it. func (q *MemoryQueue) EnqueueWithOptions(_ context.Context, job Job) (string, error) { if job.ID == "" { job.ID = uuid.New().String() } if job.CreatedAt.IsZero() { job.CreatedAt = time.Now().UTC() } job.Status = StatusPending q.mu.RLock() handler, ok := q.handlers[job.Type] q.mu.RUnlock() if !ok { return "", fmt.Errorf("no handler registered for job type %q", job.Type) } // Store job for status tracking. q.mu.Lock() q.jobs[job.ID] = &job q.mu.Unlock() q.logger.Info("dispatching in-memory job", "job_id", job.ID, "job_type", job.Type) // Process in background goroutine (mirrors worker behavior). go func() { q.mu.Lock() job.Status = StatusRunning now := time.Now().UTC() job.StartedAt = &now q.mu.Unlock() if err := handler(context.Background(), &job); err != nil { q.mu.Lock() job.Status = StatusFailed job.Error = err.Error() completed := time.Now().UTC() job.CompletedAt = &completed q.mu.Unlock() q.logger.Error("in-memory job failed", "job_id", job.ID, "job_type", job.Type, "error", err) } else { q.mu.Lock() job.Status = StatusCompleted completed := time.Now().UTC() job.CompletedAt = &completed q.mu.Unlock() q.logger.Info("in-memory job completed", "job_id", job.ID, "job_type", job.Type) } }() return job.ID, nil } // GetJob returns a job by ID. Returns ErrJobNotFound if the job doesn't exist. func (q *MemoryQueue) GetJob(_ context.Context, jobID string) (*Job, error) { q.mu.RLock() defer q.mu.RUnlock() job, ok := q.jobs[jobID] if !ok { return nil, ErrJobNotFound } cp := *job return &cp, nil } // Compile-time checks. var ( _ Producer = (*MemoryQueue)(nil) _ JobReader = (*MemoryQueue)(nil) )