Files
greenlight/cmd/api/main.go

145 lines
5.8 KiB
Go

package main
import (
"context"
"database/sql"
"flag"
"log/slog"
"os"
"time"
"greenlight.craftr.fr/internal/data"
// Import the pq driver so that it can register itself with the database/sql package. Note that we alias this import to the blank identifier, to stop the Go compiler complaining that the package isn't being used.
_ "github.com/lib/pq"
)
/*
Declare a string containing the application version number.
Later we'll generate this automatically at build time, but for now we'll just store the
version number as a hard-coded global constant.
*/
const version = "0.0.1"
/*
Define a config struct to hold all the configuration settings for our application.
For now, the only configuration settings will be the network port that we want the server to listen on,
and the name of the current operating environment for the application (development, staging, production, etc.)
We'll read in the configuration settings from command-line flags when the application starts.
*/
type config struct {
port int
env string
// Add a db struct field to hold the configuration settings for our database connection pool. For now, this only holds the DSN, which we will read in from a command-line flag.
db struct {
dsn string
// Add maxOpenConns, maxIdleConns and maxIdleTime fields to hold the configuration settings for the connection pool
maxOpenConns int
maxIdleConns int
maxIdleTime time.Duration
}
// Add a new limiter struct containing fields for the requests-per-second and burst values, and a boolean field which we can use to enable/disable rate limiting altogether
limiter struct {
rps float64
burst int
enabled bool
}
}
// application struct hold the dependencies for our HTTP handlers, helpers, and middleware.
type application struct {
config config
logger *slog.Logger
models data.Models
}
func main() {
// Declare an instance of the config struct.
var cfg config
/*
Read the value of the port and env command-line flags into the config struct.
We default to using the port number 4000 and the environment "development" if no corresponding flags are provided.
*/
flag.IntVar(&cfg.port, "port", 4000, "API server port")
flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production")
// Read the DSN value from the db-dsn command-line flag into the config struct. We default to using our development DSN if no flag is provided.
// Use the value of the GREENLIGHT_DB_DSN environment variable as the default value for our db-dsn command-line flag.
flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")
// Read the connection pool settings from command-line flags into the config struct.
flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")
// Create command line flags to read the setting values into the config struct. Notice taht we use true as the default for the 'enabled' setting
flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second")
flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst")
flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter")
flag.Parse()
// Initialize a new structured logger which writes log entries to standard out stream.
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Call the openDB() helper function to create the connection pool, passing in the config struct. If this returns an error, we log it and exit the application immediately.
db, err := openDB(cfg)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
// Defer a call to db.Close() so that the connection pool is closed before the main() function exists
defer db.Close()
// Also log a message to say that the connection pool has been successfully established.
logger.Info("database connection pool established")
// Declare an instance of the application struct, containing the config struct and the logger.
app := &application{
config: cfg,
logger: logger,
models: data.NewModels(db),
}
// Call app.serve() to start the server.
err = app.serve()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
}
// openDB() function returns a sql.DB connection pool.
func openDB(cfg config) (*sql.DB, error) {
// Use sql.Open() to create an empty connection pool, using the DSN from the config struct.
db, err := sql.Open("postgres", cfg.db.dsn)
if err != nil {
return nil, err
}
// Set the maximum number of open (in-use + idle) connections in the pool. Note that passing a value less than or equal to 0 will mean there is no limit.
db.SetMaxOpenConns(cfg.db.maxOpenConns)
// Set the maximum number of idle connections in the pool. Again, passing a value less than or equal to 0 will mean there is no limit.
db.SetMaxIdleConns(cfg.db.maxIdleConns)
// Set the maximum idle timeout for connections in the pool. Passing a duration less than or equal to 0 will mean that connections are not closed due to their idle time.
db.SetConnMaxIdleTime(cfg.db.maxIdleTime)
// Create a context with a 5-second timeout deadline.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Use PingContext() to establish a new connection to the database, passing in the context we created above as parameter. If the connection couldn't be established successfully within the 5-second deadline, then this will return an error. If we get this error, or any other, we close the connection pool and return the error.
err = db.PingContext(ctx)
if err != nil {
db.Close()
return nil, err
}
// Return the sql.DB connection pool.
return db, nil
}