// Command worker runs background jobs for the analytics service: // computed-trait refresh, segment refresh, reverse-ETL pushes, webhook fan-out. // // Jobs are scheduled and dispatched via riverqueue/river backed by PostgreSQL. // New job kinds are registered in registerWorkers below; periodic schedules // are wired in periodicJobs. package main import ( "context" "encoding/json" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver/riverpgxv5" "go.uber.org/zap" "github.com/dbiz/cdp/data-layer/workers/internal/config" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { cfg, err := config.Load() if err != nil { return err } logger, err := newLogger(cfg.LogLevel) if err != nil { return err } defer func() { _ = logger.Sync() }() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // ---- Postgres pool ---------------------------------------------------- pool, err := pgxpool.New(ctx, cfg.PostgresDSN) if err != nil { return err } defer pool.Close() // ---- river client ----------------------------------------------------- workers := river.NewWorkers() registerWorkers(workers, logger) client, err := river.NewClient(riverpgxv5.New(pool), &river.Config{ Queues: map[string]river.QueueConfig{ river.QueueDefault: {MaxWorkers: cfg.MaxWorkers}, }, Workers: workers, PeriodicJobs: periodicJobs(cfg), Logger: newSlogAdapter(logger), }) if err != nil { return err } if err := client.Start(ctx); err != nil { return err } // ---- HTTP (health) ---------------------------------------------------- r := chi.NewRouter() r.Get("/health", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) r.Get("/ready", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ready"}) }) srv := &http.Server{ Addr: cfg.HTTPAddr, Handler: r, ReadHeaderTimeout: 5 * time.Second, } httpErr := make(chan error, 1) go func() { logger.Info("worker http listening", zap.String("addr", cfg.HTTPAddr)) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { httpErr <- err } }() // ---- Signals ---------------------------------------------------------- sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) select { case <-sigCh: logger.Info("shutdown signal received") case err := <-httpErr: logger.Error("http stopped unexpectedly", zap.Error(err)) } shutCtx, shutCancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer shutCancel() _ = srv.Shutdown(shutCtx) if err := client.Stop(shutCtx); err != nil { logger.Error("river client stop", zap.Error(err)) } return nil } // registerWorkers adds job workers to the registry. Each new job kind // (ComputeTraits, RefreshSegment, ReverseETL, ...) calls river.AddWorker here. func registerWorkers(_ *river.Workers, _ *zap.Logger) { // e.g. river.AddWorker(workers, &job.ComputeTraitsWorker{Repo: traitsRepo, Log: logger}) } // periodicJobs returns the recurring schedules driven by river's built-in // scheduler. Idempotent jobs only — river may retry on failure. func periodicJobs(_ *config.Config) []*river.PeriodicJob { // Real schedules land here once the corresponding workers are wired up. // See CLAUDE_analytics.md → Job Queue (river). return nil } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func newLogger(level string) (*zap.Logger, error) { lvl, err := zap.ParseAtomicLevel(level) if err != nil { lvl = zap.NewAtomicLevelAt(zap.InfoLevel) } cfg := zap.NewProductionConfig() cfg.Level = lvl cfg.EncoderConfig.TimeKey = "ts" cfg.EncoderConfig.MessageKey = "msg" return cfg.Build() }