package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "time" "git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/config" "git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/database" "git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/growthreport" "github.com/redis/go-redis/v9" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) cfg, err := config.Load() if err != nil { logger.Error("configuration failed", "error", err) os.Exit(1) } if err := cfg.ValidateAPI(); err != nil { logger.Error("worker configuration failed", "error", err) os.Exit(1) } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() pool, err := database.Open(ctx, cfg.DatabaseURL) if err != nil { logger.Error("database connection failed", "error", err) os.Exit(1) } defer pool.Close() redisClient := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr, Password: cfg.RedisPassword}) defer redisClient.Close() repository := growthreport.NewRepository(pool) coordinator := growthreport.NewCoordinator(redisClient, growthreport.CoordinatorConfig{CreatePerMinute: cfg.AICreatePerMinute, UncachedPerDay: cfg.AIUncachedScopesPerDay, DailyRequests: cfg.AIDailyRequestBudget, DailyTokens: cfg.AIDailyTokenBudget, MaxConcurrent: cfg.AIMaxConcurrent}) avalai := growthreport.NewAvalAIClient(growthreport.AvalAIConfig{BaseURL: cfg.AvalAIBaseURL, APIKey: cfg.AvalAIAPIKey, Model: cfg.AvalAIModel, Timeout: 30 * time.Second}, &http.Client{}) processor := growthreport.NewProcessor(repository, growthreport.NewPostgreSQLBuilder(pool), avalai, coordinator, cfg.AIGrowthEnabled && cfg.AvalAIAPIKey != "") logger.Info("growth worker ready", "ai_enabled", cfg.AIGrowthEnabled && cfg.AvalAIAPIKey != "") poll := time.NewTicker(time.Second) defer poll.Stop() cleanup := time.NewTicker(24 * time.Hour) defer cleanup.Stop() for { select { case <-ctx.Done(): return case <-poll.C: workCtx, workCancel := context.WithTimeout(ctx, 2*time.Minute) err := processor.ProcessNext(workCtx) workCancel() if err != nil && !errors.Is(err, growthreport.ErrNotFound) { logger.Error("growth report processing failed", "error", err) } case <-cleanup.C: cleanupCtx, cleanupCancel := context.WithTimeout(ctx, 30*time.Second) deleted, err := repository.CleanupSuperseded(cleanupCtx, time.Now().UTC().AddDate(0, 0, -90)) cleanupCancel() if err != nil { logger.Error("growth cleanup failed", "error", err) } else if deleted > 0 { logger.Info("growth cleanup complete", "deleted_reports", deleted) } } } }