diff --git a/Makefile b/Makefile index fae6bed..39384e1 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ web: web-all .PHONY: build build: tidy sqlc # build. - @go build -v -ldflags "$(GO_LDFLAGS)" -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go 2>/dev/null + @go build -v -ldflags "$(GO_LDFLAGS)" -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go .PHONY: sqlc sqlc: diff --git a/configs/howmuch.yml b/configs/howmuch.yml index 45abff2..17f7dd5 100644 --- a/configs/howmuch.yml +++ b/configs/howmuch.yml @@ -16,6 +16,10 @@ db: # DB name database: howmuch + max-open-conns: 100 + max-idle-conns: 100 + max-lifetime: 10s + cache: host: 127.0.0.1:6379 password: "" diff --git a/go.mod b/go.mod index dd6d615..d97b4d7 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -65,6 +66,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/internal/howmuch/adapter/repo/db.go b/internal/howmuch/adapter/repo/db.go index 8214510..e1db2f7 100644 --- a/internal/howmuch/adapter/repo/db.go +++ b/internal/howmuch/adapter/repo/db.go @@ -24,17 +24,17 @@ package repo import ( "context" + "database/sql" "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" - "github.com/jackc/pgx/v5" ) type dbRepository struct { - db *pgx.Conn + db *sql.DB } -func NewDBRepository(db *pgx.Conn) repo.DBRepository { +func NewDBRepository(db *sql.DB) repo.DBRepository { return &dbRepository{ db: db, } @@ -46,20 +46,20 @@ func (dr *dbRepository) Transaction( ctx context.Context, txFunc func(txCtx context.Context, tx interface{}) (interface{}, error), ) (interface{}, error) { - tx, err := dr.db.Begin(ctx) + tx, err := dr.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { if p := recover(); p != nil { - tx.Rollback(ctx) + tx.Rollback() log.PanicLog("transaction panicked!") } else if err != nil { - tx.Rollback(ctx) + tx.Rollback() log.ErrorLog("transaction failed!", "err", err) } else { - err = tx.Commit(ctx) + err = tx.Commit() } }() diff --git a/internal/howmuch/adapter/repo/sqlc/db.go b/internal/howmuch/adapter/repo/sqlc/db.go index b931bc5..2248616 100644 --- a/internal/howmuch/adapter/repo/sqlc/db.go +++ b/internal/howmuch/adapter/repo/sqlc/db.go @@ -6,15 +6,14 @@ package sqlc import ( "context" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" + "database/sql" ) type DBTX interface { - Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) - Query(context.Context, string, ...interface{}) (pgx.Rows, error) - QueryRow(context.Context, string, ...interface{}) pgx.Row + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row } func New(db DBTX) *Queries { @@ -25,7 +24,7 @@ type Queries struct { db DBTX } -func (q *Queries) WithTx(tx pgx.Tx) *Queries { +func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, } diff --git a/internal/howmuch/adapter/repo/sqlc/models.go b/internal/howmuch/adapter/repo/sqlc/models.go index d00eeb2..7913d60 100644 --- a/internal/howmuch/adapter/repo/sqlc/models.go +++ b/internal/howmuch/adapter/repo/sqlc/models.go @@ -5,7 +5,7 @@ package sqlc import ( - "github.com/jackc/pgx/v5/pgtype" + "time" ) type Admin struct { @@ -21,6 +21,6 @@ type User struct { FirstName string LastName string Password string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/howmuch/adapter/repo/sqlc/user.sql.go b/internal/howmuch/adapter/repo/sqlc/user.sql.go index 6e9a1da..43837d9 100644 --- a/internal/howmuch/adapter/repo/sqlc/user.sql.go +++ b/internal/howmuch/adapter/repo/sqlc/user.sql.go @@ -7,8 +7,7 @@ package sqlc import ( "context" - - "github.com/jackc/pgx/v5/pgtype" + "time" ) const getUserByEmail = `-- name: GetUserByEmail :one @@ -18,7 +17,7 @@ SELECT id, email, first_name, last_name, password, created_at, updated_at ` func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { - row := q.db.QueryRow(ctx, getUserByEmail, email) + row := q.db.QueryRowContext(ctx, getUserByEmail, email) var i User err := row.Scan( &i.ID, @@ -45,8 +44,8 @@ type InsertUserParams struct { FirstName string LastName string Password string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + CreatedAt time.Time + UpdatedAt time.Time } // MIT License @@ -71,7 +70,7 @@ type InsertUserParams struct { // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) { - row := q.db.QueryRow(ctx, insertUser, + row := q.db.QueryRowContext(ctx, insertUser, arg.Email, arg.FirstName, arg.LastName, diff --git a/internal/howmuch/adapter/repo/user.go b/internal/howmuch/adapter/repo/user.go index a273ced..97814da 100644 --- a/internal/howmuch/adapter/repo/user.go +++ b/internal/howmuch/adapter/repo/user.go @@ -24,6 +24,7 @@ package repo import ( "context" + "database/sql" "errors" "time" @@ -31,16 +32,15 @@ import ( "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" ) type userRepository struct { - db *pgx.Conn + db *sql.DB } const insertTimeout = 1 * time.Second -func NewUserRepository(db *pgx.Conn) repo.UserRepository { +func NewUserRepository(db *sql.DB) repo.UserRepository { return &userRepository{ db: db, } @@ -60,11 +60,11 @@ func (ur *userRepository) Create( FirstName: u.FirstName, LastName: u.LastName, Password: u.Password, - CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, - UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } - tx, ok := transaction.(pgx.Tx) + tx, ok := transaction.(*sql.Tx) if !ok { return nil, errors.New("transaction is not a pgx.Tx") } @@ -82,8 +82,8 @@ func (ur *userRepository) Create( FirstName: userDB.FirstName, LastName: userDB.LastName, Password: userDB.Password, - CreatedAt: userDB.CreatedAt.Time, - UpdatedAt: userDB.CreatedAt.Time, + CreatedAt: userDB.CreatedAt, + UpdatedAt: userDB.CreatedAt, }, nil } @@ -104,7 +104,7 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model. FirstName: userDB.FirstName, LastName: userDB.LastName, Password: userDB.Password, - CreatedAt: userDB.CreatedAt.Time, - UpdatedAt: userDB.CreatedAt.Time, + CreatedAt: userDB.CreatedAt, + UpdatedAt: userDB.CreatedAt, }, nil } diff --git a/internal/howmuch/howmuch.go b/internal/howmuch/howmuch.go index e4d6a86..9e93735 100644 --- a/internal/howmuch/howmuch.go +++ b/internal/howmuch/howmuch.go @@ -38,7 +38,6 @@ import ( "git.vinchent.xyz/vinchent/howmuch/internal/pkg/token" "git.vinchent.xyz/vinchent/howmuch/pkg/version/verflag" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5" "github.com/redis/go-redis/v9" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -117,15 +116,17 @@ func run() error { viper.GetString("db.password"), viper.GetString("db.sslmode"), ) - dbConf, err := pgx.ParseConfig(dbConfString) - if err != nil { - log.FatalLog("DB connection config failure", "err", err, "cfg string", dbConfString) + // TODO: viper conf should be parsed into a struct directly + dbExtraConf := &datastore.DbExtraConf{ + MaxOpenConns: viper.GetInt("db.max-open-conns"), + MaxIdleConns: viper.GetInt("db.max-idle-conns"), + MaxLifetime: viper.GetDuration("db.max-lifetime"), } - dbConn, err := datastore.NewDB(dbConf) - if err != nil { - log.FatalLog("DB connection failure", "err", err) + dbConn := datastore.NewDB(dbConfString, dbExtraConf) + if dbConn == nil { + log.FatalLog("DB connection failure") } - defer dbConn.Close(context.Background()) + defer dbConn.Close() // Init Cache cache := datastore.NewCache(&redis.Options{ diff --git a/internal/howmuch/infra/datastore/db.go b/internal/howmuch/infra/datastore/db.go index 8141785..d3fc37c 100644 --- a/internal/howmuch/infra/datastore/db.go +++ b/internal/howmuch/infra/datastore/db.go @@ -23,22 +23,65 @@ package datastore import ( - "context" + "database/sql" + "time" - "github.com/jackc/pgx/v5" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" + _ "github.com/jackc/pgx/v5" + _ "github.com/jackc/pgx/v5/stdlib" ) +var counts int + +type DbExtraConf struct { + MaxOpenConns int + MaxIdleConns int + MaxLifetime time.Duration +} + // NewDB creates a new database for the application -func NewDB(connConfig *pgx.ConnConfig) (*pgx.Conn, error) { - conn, err := pgx.ConnectConfig(context.Background(), connConfig) +func NewDB(dsn string, opts interface{}) *sql.DB { + var db *sql.DB + var err error + + for { + db, err = openDB(dsn) + if err != nil { + log.WarnLog("postgres not ready", "err", err) + counts++ + } else { + log.InfoLog("connected to postgres") + break + } + + if counts > 10 { + break + } + log.InfoLog("retry in 2 seconds") + time.Sleep(2 * time.Second) + } + + if db == nil { + return nil + } + + extraConf, ok := opts.(DbExtraConf) + if ok { + db.SetMaxOpenConns(extraConf.MaxOpenConns) + db.SetMaxIdleConns(extraConf.MaxIdleConns) + db.SetConnMaxLifetime(extraConf.MaxLifetime) + } + return db +} + +func openDB(dsn string) (*sql.DB, error) { + db, err := sql.Open("pgx", dsn) if err != nil { return nil, err } - // Ping test the conn - if err = conn.Ping(context.Background()); err != nil { + if err := db.Ping(); err != nil { return nil, err } - - return conn, err + return db, nil } diff --git a/internal/howmuch/registry/registry.go b/internal/howmuch/registry/registry.go index 98ed3fe..76a538a 100644 --- a/internal/howmuch/registry/registry.go +++ b/internal/howmuch/registry/registry.go @@ -23,9 +23,10 @@ package registry import ( + "database/sql" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" - "github.com/jackc/pgx/v5" ) // registry is an implementation of Registry interface. @@ -33,7 +34,7 @@ import ( // It might holds other drivers when the projects grows. For example // the object needed to connect to Redis or Kafka. type registry struct { - db *pgx.Conn + db *sql.DB cache core.Cache } @@ -45,7 +46,7 @@ type Registry interface { } // NewRegistry returns a new Registry's implementation. -func NewRegistry(db *pgx.Conn, cache core.Cache) Registry { +func NewRegistry(db *sql.DB, cache core.Cache) Registry { return ®istry{db: db, cache: cache} } diff --git a/sqlc.yml b/sqlc.yml index a512ce5..7c7a57c 100644 --- a/sqlc.yml +++ b/sqlc.yml @@ -28,4 +28,3 @@ sql: gen: go: out: "internal/howmuch/adapter/repo/sqlc" - sql_package: "pgx/v5"