package main //go:generate planter postgres://planter:planter@localhost:5432/inventory_2?sslmode=disable -o diagrams/database_schema.uml //go:generate plantuml diagrams/*.uml -tsvg //go:generate plantuml diagrams/etsy/*.uml -tsvg //go:generate npx @tailwindcss/cli -i styles/source.css -o styles/index.css import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/lmittmann/tint" "ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/authentication" etsy_platform "ruben/inventory2/internal/domains/platforms/etsy" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server" ) const ( etsyAPIKeystring = "38ncokqh0jih5jshfk8iv4n5" etsyAPISharedSecret = "jaaw0tyizf" ) func main() { logger := logging.New(tint.NewHandler(os.Stderr, &tint.Options{ AddSource: true, Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { // this can perform general key=value log cleanup if a.Key == slog.SourceKey && len(groups) == 0 { source := a.Value.Any().(*slog.Source) source.File = strings.TrimPrefix(source.File, "/home/angel/go/src/ruben/inventory2/internal") } return a }, // Time format (Default: time.StampMilli) //TimeFormat: "", })) logger.Info("application starting") if err := runApp(context.Background(), logger); err != nil { panic(err) } logger.Info("application shutdown") } func runApp(ctx context.Context, logger *logging.Logger) error { ctx, shutdown := context.WithCancel(ctx) defer shutdown() // connect to the database connPool, err := newPool(ctx) if err != nil { return fmt.Errorf("failed to initialize database connection pool: %w", err) } // start background processes auth, err := authentication.New(ctx, connPool, logger.WithGroup("authenticator")) if err != nil { return fmt.Errorf("failed to construct authenticator: %w", err) } authErrCh := runAuthProcesses(ctx, auth) // start http server srvErrCh := runServer(ctx, logger, connPool, auth) // wait for interrupt signal or unrecoverable failure, then shutdown osSignalCh := make(chan os.Signal, 1) signal.Notify(osSignalCh, syscall.SIGINT, syscall.SIGTERM) var ( alreadyShutdown struct { server bool authProcesses bool } ) select { case s := <-osSignalCh: logger.Info("application received shutdown signal", "signal", s) logger.Info("shutting down") case err := <-authErrCh: alreadyShutdown.authProcesses = true logger.Error("auth processes shutdown unexpectedly") if err != nil { logger.Error("auth processes encountered error", "error", err) } case err := <-srvErrCh: alreadyShutdown.server = true logger.Error("server shutdown unexpectedly") if err != nil { logger.Error("server encountered error", "error", err) } } shutdown() // capture application errors that occurred during or caused shutdown var errs []error if !alreadyShutdown.authProcesses { if err := <-authErrCh; err != nil { errs = append(errs, fmt.Errorf("auth processes experienced an error: %w", err)) } logger.Info("auth processes shut down") } if !alreadyShutdown.server { if err := <-srvErrCh; err != nil { errs = append(errs, fmt.Errorf("server experienced an error: %w", err)) } logger.Info("server shut down") } return errors.Join(errs...) } func runAuthProcesses(ctx context.Context, auth *authentication.Authenticator) <-chan error { errCh := make(chan error, 1) go func() { defer close(errCh) if err := auth.RunBackgroundCleanup(ctx); err != nil { errCh <- err } }() return errCh } func runServer(ctx context.Context, logger *logging.Logger, connPool *pgxpool.Pool, auth *authentication.Authenticator) <-chan error { r := server.Router( ctx, logger.WithGroup("server"), "./", raw_events.NewStore(logger.WithGroup("raw-event-store"), connPool), accounts.NewStore(logger, connPool), etsy_platform.NewPlatform( logger, func(acctID int64) string { return fmt.Sprintf("/oauth/account/%d/auth_code", acctID) }, etsyAPIKeystring, etsyAPISharedSecret, connPool, ), auth, ) srv := &http.Server{ Addr: ":8082", // local Handler: r, } ctx, cancel := context.WithCancel(ctx) alreadyShutdownCh := make(chan struct{}, 1) runningErrCh := make(chan error, 1) go func() { defer close(runningErrCh) defer cancel() defer close(alreadyShutdownCh) logger.Info("server running on 8082...") if err := srv.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { runningErrCh <- fmt.Errorf("server experienced error: %w", err) } } }() shutdownErrCh := make(chan error, 1) go func() { defer close(shutdownErrCh) select { case <-alreadyShutdownCh: return case <-ctx.Done(): } shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 60*time.Second) defer cancelShutdown() if err := srv.Shutdown(shutdownCtx); err != nil { shutdownErrCh <- fmt.Errorf("error occurred attempting to shutdown server: %w", err) } }() errCh := make(chan error, 1) go func() { defer close(errCh) err1 := <-runningErrCh err2 := <-shutdownErrCh if err := errors.Join(err1, err2); err != nil { errCh <- err } }() return errCh }