diff --git a/conn.go b/conn.go index b67eb98ac..23d358c3c 100644 --- a/conn.go +++ b/conn.go @@ -224,6 +224,7 @@ func (cc *ConnConfig) networkAddresses() ([]net.Addr, error) { // goroutines. type Conn struct { conn net.Conn // the underlying TCP or unix domain socket connection + creationTime time.Time // the time indicating when the connection is established lastActivityTime time.Time // the last time the connection was used wbuf []byte pid uint32 // backend pid @@ -457,7 +458,7 @@ func connect(config ConnConfig, connInfo *pgtype.ConnInfo) (c *Conn, err error) } c.addr = addr - + c.creationTime = time.Now() return c, nil } @@ -602,6 +603,10 @@ func (c *Conn) connect(config ConnConfig, network, address string, tlsConfig *tl } } +func (c *Conn) CreationTime() time.Time { + return c.creationTime +} + func initPostgresql(c *Conn) (*pgtype.ConnInfo, error) { const ( namedOIDQuery = `select t.oid, diff --git a/conn_pool.go b/conn_pool.go index 95e1b015e..cd09cb31e 100644 --- a/conn_pool.go +++ b/conn_pool.go @@ -13,9 +13,10 @@ import ( type ConnPoolConfig struct { ConnConfig - MaxConnections int // max simultaneous connections to use, default 5, must be at least 2 - AfterConnect func(*Conn) error // function to call on every new connection - AcquireTimeout time.Duration // max wait time when all connections are busy (0 means no timeout) + MaxConnections int // max simultaneous connections to use, default 5, must be at least 2 + MaxConnLifetime time.Duration // the duration since creation after which a connection will be automatically closed. + AfterConnect func(*Conn) error // function to call on every new connection + AcquireTimeout time.Duration // max wait time when all connections are busy (0 means no timeout) } type ConnPool struct { @@ -25,6 +26,7 @@ type ConnPool struct { config ConnConfig // config used when establishing connection inProgressConnects int maxConnections int + maxConnLifetime time.Duration resetCount int afterConnect func(*Conn) error logger Logger @@ -60,6 +62,7 @@ func NewConnPool(config ConnPoolConfig) (p *ConnPool, err error) { p.config = config.ConnConfig p.connInfo = minimalConnInfo p.maxConnections = config.MaxConnections + p.maxConnLifetime = config.MaxConnLifetime if p.maxConnections == 0 { p.maxConnections = 5 } @@ -226,7 +229,7 @@ func (p *ConnPool) Release(conn *Conn) { p.cond.L.Lock() - if conn.poolResetCount != p.resetCount { + if conn.poolResetCount != p.resetCount || (p.maxConnLifetime != 0 && time.Now().Sub(conn.CreationTime()) > p.maxConnLifetime) { conn.Close() p.cond.L.Unlock() p.cond.Signal() diff --git a/conn_pool_test.go b/conn_pool_test.go index db645e637..27924ee53 100644 --- a/conn_pool_test.go +++ b/conn_pool_test.go @@ -15,6 +15,7 @@ import ( func createConnPool(t *testing.T, maxConnections int) *pgx.ConnPool { config := pgx.ConnPoolConfig{ConnConfig: *defaultConnConfig, MaxConnections: maxConnections} + config.MaxConnLifetime = 250 * time.Millisecond pool, err := pgx.NewConnPool(config) if err != nil { t.Fatalf("Unable to create connection pool: %v", err) @@ -150,6 +151,32 @@ func TestPoolAcquireAndReleaseCycle(t *testing.T) { releaseAllConnections(pool, allConnections) } +func TestPoolReleaseChecksMaxConnLifetime(t *testing.T) { + t.Parallel() + + config := pgx.ConnPoolConfig{ConnConfig: *defaultConnConfig} + pool, err := pgx.NewConnPool(config) + if err != nil { + t.Fatal("Unable to establish connection pool") + } + defer pool.Close() + + c, err := pool.Acquire() + if err != nil { + t.Fatal("Unable to acquire connection from the pool") + } + + time.Sleep(config.MaxConnLifetime) + + pool.Release(c) + waitForReleaseToComplete() + + stats := pool.Stat() + if stats.CurrentConnections != 0 && stats.AvailableConnections != 0 { + t.Fatal("Unable to recycle connection from the pool") + } +} + func TestPoolNonBlockingConnections(t *testing.T) { t.Parallel() @@ -1227,3 +1254,10 @@ func TestConnPoolBeginEx(t *testing.T) { t.Fatal("Should not be able to create a tx") } } + +// Conn.Release is an asynchronous process that returns immediately. There is no signal when the actual work is +// completed. To test something that relies on the actual work for Conn.Release being completed we must simply wait. +// This function wraps the sleep so there is more meaning for the callers. +func waitForReleaseToComplete() { + time.Sleep(5 * time.Millisecond) +} \ No newline at end of file