Skip to content

Commit

Permalink
Use options pattern for database controller.
Browse files Browse the repository at this point in the history
While here, add `build-all` target to Makefile.

Signed-off-by: Ville Valkonen <weezel@users.noreply.github.com>
  • Loading branch information
weezel committed Dec 17, 2023
1 parent 9f80650 commit 8e05f12
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 83 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ DB_PASSWORD ?= $(shell awk -F '=' '/^DB_PASSWORD/ { print $$NF }' .env)
COMPOSE_FILE ?= docker-compose.yml


all: test-unit lint build
all: test-unit lint build-all

_build: dist/$(APP_NAME)

build-all:
make -C cmd/dbmigrate
make -C cmd/webserver

dist/$(APP_NAME):
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) \
$(GO) build $(LDFLAGS) \
Expand Down
2 changes: 0 additions & 2 deletions cmd/dbmigrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ var schemasDir = "schemas"
var sqlMigrations embed.FS

func init() {
log.SetFlags(0)

var err error
wd, err = os.Getwd()
if err != nil {
Expand Down
13 changes: 9 additions & 4 deletions cmd/webserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
"weezel/example-gin/pkg/postgres"

l "weezel/example-gin/pkg/logger"

"github.com/jackc/pgx/v5/pgxpool"
)

// These will be filled in the build time by -X flag
Expand Down Expand Up @@ -53,8 +51,15 @@ func main() {
l.Logger.Panic().Err(err).Msg("Failed to parse config")
}

var dbConn *pgxpool.Pool
dbConn, err = postgres.New(ctx, cfg.Postgres)
dbCtrl := postgres.New(
postgres.WithUsername(cfg.Postgres.Username),
postgres.WithPassword(cfg.Postgres.Password),
postgres.WithPort(cfg.Postgres.Port),
postgres.WithDBName(cfg.Postgres.DBName),
postgres.WithSSLMode(postgres.SSLModeDisable), // This is running on localhost only
postgres.WithApplicationName("example-gin"),
)
dbConn, err := dbCtrl.Connect(ctx)
if err != nil {
l.Logger.Fatal().Err(err).Msg("Database connection failed")
}
Expand Down
212 changes: 168 additions & 44 deletions pkg/postgres/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package postgres

import (
"context"
"database/sql"
"errors"
"fmt"
"math"
"sync"
"time"
"weezel/example-gin/pkg/config"

Expand All @@ -14,58 +14,182 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)

const (
dbConnRetries = 3
postgresConfig = "sslmode=disable&pool_max_conns=5"
)
var ErrorDatabaseRetriesExceeded = errors.New("database retries exceeded")

type SSLModes string

var (
once sync.Once
dbPool *pgxpool.Pool
dbErr error
SSLModeDisable SSLModes = "disable"
SSLModeAllow SSLModes = "allow"
SSLModePrefer SSLModes = "prefer"
SSLModeRequire SSLModes = "require"
SSLModeVerifyCA SSLModes = "verify-ca"
SSLModeVerifyFull SSLModes = "verify-full"
)

var ErrorDatabaseRetriesExceeded = errors.New("databse retries exceeded")

// New initializes database connection once. Also known as singleton.
func New(ctx context.Context, dbConf config.Postgres) (*pgxpool.Pool, error) {
pgConfigURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?"+postgresConfig,
dbConf.Username,
dbConf.Password,
dbConf.Hostname,
dbConf.Port,
dbConf.DBName)
var retries int
once.Do(func() {
dbPool, dbErr = pgxpool.New(ctx, pgConfigURL)
if dbErr != nil {
l.Logger.Fatal().Err(dbErr).Msg("Failed to start database")
}
type Contollerer interface {
Connect(context.Context) error
}

type Controller struct {
username string
password string
hostname string
port string
dbName string
applicationName string
dbURL string
sslMode SSLModes
poolMaxConns uint
maxConnRetries uint
}

type Option func(*Controller)

func New(opts ...Option) *Controller {
ctrl := &Controller{
port: "5432",
maxConnRetries: 5,
poolMaxConns: 5,
username: "postgres",
hostname: "localhost",
dbName: "postgres",
sslMode: SSLModePrefer,
}

for _, opt := range opts {
opt(ctrl)
}

started := time.Now()
for {
if dbErr = dbPool.Ping(ctx); dbErr == nil {
break
}
ctrl.dbURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s&pool_max_conns=%d&application_name=%s",
ctrl.username,
ctrl.password,
ctrl.hostname,
ctrl.port,
ctrl.dbName,
ctrl.sslMode,
ctrl.poolMaxConns,
ctrl.applicationName,
)

dbPool, dbErr = pgxpool.New(ctx, pgConfigURL)
if dbErr == nil {
break
}
return ctrl
}

delay := math.Ceil(math.Pow(2, float64(retries)))
time.Sleep(time.Duration(delay) * time.Second)
retries++
func WithUsername(username string) Option {
return func(pc *Controller) {
pc.username = username
}
}

l.Logger.Warn().Msgf("Retrying db connection %d/%d (%s since started)",
retries, dbConnRetries, time.Since(started))
func WithPassword(password string) Option {
return func(pc *Controller) {
pc.password = password
}
}

func WithHostname(hostname string) Option {
return func(pc *Controller) {
pc.hostname = hostname
}
}

func WithPort(port string) Option {
return func(pc *Controller) {
pc.port = port
}
}

func WithDBName(dbName string) Option {
return func(pc *Controller) {
pc.dbName = dbName
}
}

if retries > dbConnRetries {
dbErr = ErrorDatabaseRetriesExceeded
return
}
func WithSSLMode(sslMode SSLModes) Option {
switch sslMode {
case SSLModeDisable, SSLModeAllow, SSLModePrefer, SSLModeRequire, SSLModeVerifyCA, SSLModeVerifyFull:
break
default:
err := fmt.Errorf("invalid SSLMode: %s", sslMode)
l.Logger.Fatal().Err(err).Msg("Unsupported SSL mode given")
}

return func(pc *Controller) {
pc.sslMode = sslMode
}
}

func WithPoolMaxConns(poolMaxConns uint) Option {
return func(pc *Controller) {
pc.poolMaxConns = poolMaxConns
}
}

func WithApplicationName(applicationName string) Option {
return func(pc *Controller) {
pc.applicationName = applicationName
}
}

func (p *Controller) Connect(ctx context.Context) (*pgxpool.Pool, error) {
dbPool, err := pgxpool.New(ctx, p.dbURL)
if err != nil {
return nil, fmt.Errorf("db connect: %w", err)
}

started := time.Now()
var retries uint
for {
if err = dbPool.Ping(ctx); err == nil {
break
}
})

return dbPool, dbErr
dbPool, err = pgxpool.New(ctx, p.dbURL)
if err == nil {
break
}

delay := math.Ceil(math.Pow(2, float64(retries)))
time.Sleep(time.Duration(delay) * time.Second)
retries++

l.Logger.Warn().Msgf("Retrying db connection %d/%d (%s since started)",
retries, p.maxConnRetries, time.Since(started))

if retries > p.maxConnRetries {
return nil, fmt.Errorf("%w [%d/%d]",
ErrorDatabaseRetriesExceeded,
retries,
p.maxConnRetries,
)
}
}

return dbPool, nil
}

// NewMigrationConnection opens a new connection for database migrations
func NewMigrationConnection(cfg config.Postgres) (*sql.DB, error) {
psqlConfig := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s&application_name=%s",
cfg.Username,
cfg.Password,
cfg.Hostname,
cfg.Port,
cfg.DBName,
cfg.TLS,
"example-gin migrations",
)
l.Logger.Debug().
Str("username", cfg.Username).
Str("password", cfg.Password[0:1]+"...").
Str("hostname", cfg.Hostname).
Str("port", cfg.Port).
Str("dbname", cfg.DBName).
Msg("Database migrate connection initialization")
dbConn, err := sql.Open("pgx", psqlConfig)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}

return dbConn, err
}
32 changes: 0 additions & 32 deletions pkg/postgres/migration.go

This file was deleted.

0 comments on commit 8e05f12

Please sign in to comment.