package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
if err := run(logger); err != nil {
logger.Error("stopped", "error", err.Error())
os.Exit(1)
}
}
func run(logger *slog.Logger) error {
if done, err := commandHelp(os.Args[1:], os.Stdout); done {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if len(os.Args) > 1 {
if len(os.Args) != 2 || os.Args[1] != "migrate" {
return fmt.Errorf("usage: contentd [migrate]")
}
conn, err := pgx.Connect(ctx, databaseURL(os.Getenv("DATABASE_URL")))
if err != nil {
return fmt.Errorf("migration database connection failed")
}
defer conn.Close(context.Background())
if err = migrate(ctx, conn); err != nil {
return err
}
logger.Info("schema ready", "version", 1)
return nil
}
c, err := loadConfig(os.Getenv)
if err != nil {
return err
}
config, err := pgxpool.ParseConfig(c.Database)
if err != nil {
return fmt.Errorf("invalid DATABASE_URL")
}
config.MaxConns = int32(c.Connections)
config.ConnConfig.ConnectTimeout = 5 * time.Second
if c.Trace != "" {
config.ConnConfig.Tracer = SQLTracer{}
}
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return fmt.Errorf("database pool configuration failed")
}
defer pool.Close()
if err = pool.Ping(ctx); err != nil {
return fmt.Errorf("database unavailable at startup")
}
if c.Profile == "production" {
var hash string
if err = pool.QueryRow(ctx, "SELECT sha256 FROM content_schema_migrations WHERE version=1 AND (SELECT count(*) FROM content_schema_migrations)=1").Scan(&hash); err != nil || hash != schemaHash() {
return fmt.Errorf("schema version/checksum unavailable or incompatible; run migrate first")
}
}
app := &Application{store: &Store{pool: pool}, production: c.Profile == "production"}
var handler http.Handler = app
if c.Trace != "" {
handler = traceHTTP(handler, c.Trace)
}
server := &http.Server{Addr: c.Address}
if c.Profile == "production" {
handler = &operationalHandler{next: handler, ready: pool.Ping, hosts: c.Hosts, slots: make(chan struct{}, c.Inflight), logger: logger}
server.ReadHeaderTimeout = 5 * time.Second
server.ReadTimeout = 15 * time.Second
server.WriteTimeout = 30 * time.Second
server.IdleTimeout = 60 * time.Second
server.MaxHeaderBytes = 32 << 10
}
server.Handler = handler
stop, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stopSignals()
done := make(chan error, 1)
go func() { done <- server.ListenAndServe() }()
logger.Info("starting", "profile", c.Profile, "pid", os.Getpid(), "address", c.Address)
select {
case err = <-done:
if err != http.ErrServerClosed {
return err
}
case <-stop.Done():
drain, end := context.WithTimeout(context.Background(), 10*time.Second)
defer end()
if err = server.Shutdown(drain); err != nil {
server.Close()
return fmt.Errorf("graceful shutdown exceeded deadline")
}
if err = <-done; err != http.ErrServerClosed {
return err
}
}
logger.Info("stopped cleanly")
return nil
}