Documentation
¶
Overview ¶
Package pgfallback lets a pgx or GORM application use the read addresses of a database only while none of its rw addresses takes a connection, and go back to rw as soon as one does.
The application keeps its own connection string with the rw hosts. Pool adds the read addresses to a pgxpool.Config, Stdlib to database/sql and GORM through stdlib.OpenDB options:
cfg, err := pgxpool.ParseConfig(dsn) ... err = pgfallback.Pool(cfg, "vm1:5433", "vm2:5433", "vm3:5433")
For each new connection pgx tries the rw hosts first, as it always does, and then the read addresses in the order given. A connection that landed on a read address is a read connection. While read connections are in use, the package tries a full connect to the rw hosts in the background, at most once a second. Once one works, read connections are closed when they are handed out or released, so new connections go to rw again. Nothing asks the server for its role.
The config must use TLS on every host, set connect_timeout, and leave target_session_attrs at any.
How to set it up, which errors an application sees while there is no primary, and how far behind a read on the read port can be: https://github.com/djosh34/pgfallback/blob/main/README.md
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NoPrimary ¶
NoPrimary reports whether err is or wraps a PostgreSQL error with SQLSTATE 25006, read_only_sql_transaction, which a write gets on a standby.
func Pool ¶
Pool adds the read addresses, each "host:port", to cfg. It keeps every hook cfg already has (BeforeConnect, AfterConnect, PrepareConn, BeforeAcquire, AfterRelease, and the AfterConnect of its ConnConfig) and calls it too. Call it once cfg is complete, right before pgxpool.NewWithConfig: a later change to cfg reaches neither the wrapped hooks nor the attempts at rw.
Example ¶
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/djosh34/pgfallback"
)
const exampleDSN = "host=node1.example.com,node2.example.com,node3.example.com port=5432 dbname=orders user=orders " +
"sslmode=verify-full sslrootcert=ca.crt sslcert=orders-app.crt sslkey=orders-app.key connect_timeout=2"
// connectPool opens a pgxpool.Pool that falls back to the read port.
func connectPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
err = pgfallback.Pool(cfg, "node1.example.com:5433", "node2.example.com:5433", "node3.example.com:5433")
if err != nil {
return nil, err
}
return pgxpool.NewWithConfig(ctx, cfg)
}
func main() {
pool, err := connectPool(context.Background(), exampleDSN)
if err != nil {
log.Print(err)
return
}
defer pool.Close()
}
Output:
Example (DialTimeout) ¶
A pool whose TCP connects give up after 500 ms (README.md, A short dial timeout).
package main
import (
"context"
"log"
"net"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/djosh34/pgfallback"
)
const exampleDSN = "host=node1.example.com,node2.example.com,node3.example.com port=5432 dbname=orders user=orders " +
"sslmode=verify-full sslrootcert=ca.crt sslcert=orders-app.crt sslkey=orders-app.key connect_timeout=2"
func main() {
cfg, err := pgxpool.ParseConfig(exampleDSN)
if err != nil {
log.Print(err)
return
}
cfg.ConnConfig.DialFunc = (&net.Dialer{Timeout: 500 * time.Millisecond}).DialContext
err = pgfallback.Pool(cfg, "node1.example.com:5433", "node2.example.com:5433", "node3.example.com:5433")
if err != nil {
log.Print(err)
return
}
pool, err := pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
log.Print(err)
return
}
defer pool.Close()
}
Output:
func Stdlib ¶
func Stdlib(read ...string) ([]stdlib.OptionOpenDB, error)
Stdlib returns the stdlib.OpenDB options that add the read addresses, each "host:port", for database/sql, or for GORM through postgres.Config.OptionOpenDB. They set OptionBeforeConnect and OptionResetSession. The config is checked when a connection is opened.
Example ¶
package main
import (
"database/sql"
"log"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
"github.com/djosh34/pgfallback"
)
const exampleDSN = "host=node1.example.com,node2.example.com,node3.example.com port=5432 dbname=orders user=orders " +
"sslmode=verify-full sslrootcert=ca.crt sslcert=orders-app.crt sslkey=orders-app.key connect_timeout=2"
// connectDB opens a *sql.DB that falls back to the read port.
func connectDB(dsn string) (*sql.DB, error) {
cfg, err := pgx.ParseConfig(dsn)
if err != nil {
return nil, err
}
opts, err := pgfallback.Stdlib("node1.example.com:5433", "node2.example.com:5433", "node3.example.com:5433")
if err != nil {
return nil, err
}
return stdlib.OpenDB(*cfg, opts...), nil
}
func main() {
db, err := connectDB(exampleDSN)
if err != nil {
log.Print(err)
return
}
if err := db.Close(); err != nil {
log.Print(err)
}
}
Output:
Example (Gorm) ¶
package main
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/djosh34/pgfallback"
)
const exampleDSN = "host=node1.example.com,node2.example.com,node3.example.com port=5432 dbname=orders user=orders " +
"sslmode=verify-full sslrootcert=ca.crt sslcert=orders-app.crt sslkey=orders-app.key connect_timeout=2"
// connectGORM opens a *gorm.DB that falls back to the read port.
func connectGORM(dsn string) (*gorm.DB, error) {
opts, err := pgfallback.Stdlib("node1.example.com:5433", "node2.example.com:5433", "node3.example.com:5433")
if err != nil {
return nil, err
}
return gorm.Open(postgres.New(postgres.Config{DSN: dsn, OptionOpenDB: opts}), &gorm.Config{})
}
func main() {
db, err := connectGORM(exampleDSN)
if err != nil {
log.Print(err)
return
}
sqlDB, err := db.DB()
if err != nil {
log.Print(err)
return
}
if err := sqlDB.Close(); err != nil {
log.Print(err)
}
}
Output:
Types ¶
This section is empty.