Skip to content

Commit 0c516c1

Browse files
methanebradfitz
authored andcommitted
database/sql: Add DB.SetConnMaxLifetime
Long lived connections may make some DB operation difficult. (e.g. retiring load balanced DB server.) So SetConnMaxLifetime closes long lived connections. It can be used to limit maximum idle time, too. Closing idle connections reduces active connections while application is idle and avoids connections are closed by server side (cause errBadConn while querying). fixes #9851 Change-Id: I2e8e824219c1bee7f4b885d38ed96d11b7202b56 Reviewed-on: https://go-review.googlesource.com/6580 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 2cb265d commit 0c516c1

File tree

2 files changed

+219
-41
lines changed

2 files changed

+219
-41
lines changed

src/database/sql/sql.go

+118-11
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ import (
2121
"sort"
2222
"sync"
2323
"sync/atomic"
24+
"time"
2425
)
2526

2627
var (
2728
driversMu sync.RWMutex
2829
drivers = make(map[string]driver.Driver)
2930
)
3031

32+
// nowFunc returns the current time; it's overridden in tests.
33+
var nowFunc = time.Now
34+
3135
// Register makes a database driver available by the provided name.
3236
// If Register is called twice with the same name or if driver is nil,
3337
// it panics.
@@ -235,12 +239,14 @@ type DB struct {
235239
// maybeOpenNewConnections sends on the chan (one send per needed connection)
236240
// It is closed during db.Close(). The close tells the connectionOpener
237241
// goroutine to exit.
238-
openerCh chan struct{}
239-
closed bool
240-
dep map[finalCloser]depSet
241-
lastPut map[*driverConn]string // stacktrace of last conn's put; debug only
242-
maxIdle int // zero means defaultMaxIdleConns; negative means 0
243-
maxOpen int // <= 0 means unlimited
242+
openerCh chan struct{}
243+
closed bool
244+
dep map[finalCloser]depSet
245+
lastPut map[*driverConn]string // stacktrace of last conn's put; debug only
246+
maxIdle int // zero means defaultMaxIdleConns; negative means 0
247+
maxOpen int // <= 0 means unlimited
248+
maxLifetime time.Duration // maximum amount of time a connection may be reused
249+
cleanerCh chan struct{}
244250
}
245251

246252
// connReuseStrategy determines how (*DB).conn returns database connections.
@@ -260,7 +266,8 @@ const (
260266
// interfaces returned via that Conn, such as calls on Tx, Stmt,
261267
// Result, Rows)
262268
type driverConn struct {
263-
db *DB
269+
db *DB
270+
createdAt time.Time
264271

265272
sync.Mutex // guards following
266273
ci driver.Conn
@@ -284,6 +291,13 @@ func (dc *driverConn) removeOpenStmt(si driver.Stmt) {
284291
delete(dc.openStmt, si)
285292
}
286293

294+
func (dc *driverConn) expired(timeout time.Duration) bool {
295+
if timeout <= 0 {
296+
return false
297+
}
298+
return dc.createdAt.Add(timeout).Before(nowFunc())
299+
}
300+
287301
func (dc *driverConn) prepareLocked(query string) (driver.Stmt, error) {
288302
si, err := dc.ci.Prepare(query)
289303
if err == nil {
@@ -506,6 +520,9 @@ func (db *DB) Close() error {
506520
return nil
507521
}
508522
close(db.openerCh)
523+
if db.cleanerCh != nil {
524+
close(db.cleanerCh)
525+
}
509526
var err error
510527
fns := make([]func() error, 0, len(db.freeConn))
511528
for _, dc := range db.freeConn {
@@ -594,6 +611,84 @@ func (db *DB) SetMaxOpenConns(n int) {
594611
}
595612
}
596613

614+
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
615+
//
616+
// Expired connections may be closed lazily before reuse.
617+
//
618+
// If d <= 0, connections are reused forever.
619+
func (db *DB) SetConnMaxLifetime(d time.Duration) {
620+
if d < 0 {
621+
d = 0
622+
}
623+
db.mu.Lock()
624+
// wake cleaner up when lifetime is shortened.
625+
if d > 0 && d < db.maxLifetime && db.cleanerCh != nil {
626+
select {
627+
case db.cleanerCh <- struct{}{}:
628+
default:
629+
}
630+
}
631+
db.maxLifetime = d
632+
db.startCleanerLocked()
633+
db.mu.Unlock()
634+
}
635+
636+
// startCleanerLocked starts connectionCleaner if needed.
637+
func (db *DB) startCleanerLocked() {
638+
if db.maxLifetime > 0 && db.numOpen > 0 && db.cleanerCh == nil {
639+
db.cleanerCh = make(chan struct{}, 1)
640+
go db.connectionCleaner(db.maxLifetime)
641+
}
642+
}
643+
644+
func (db *DB) connectionCleaner(d time.Duration) {
645+
const minInterval = time.Second
646+
647+
if d < minInterval {
648+
d = minInterval
649+
}
650+
t := time.NewTimer(d)
651+
652+
for {
653+
select {
654+
case <-t.C:
655+
case <-db.cleanerCh: // maxLifetime was changed or db was closed.
656+
}
657+
658+
db.mu.Lock()
659+
d = db.maxLifetime
660+
if db.closed || db.numOpen == 0 || d <= 0 {
661+
db.cleanerCh = nil
662+
db.mu.Unlock()
663+
return
664+
}
665+
666+
expiredSince := nowFunc().Add(-d)
667+
var closing []*driverConn
668+
for i := 0; i < len(db.freeConn); i++ {
669+
c := db.freeConn[i]
670+
if c.createdAt.Before(expiredSince) {
671+
closing = append(closing, c)
672+
last := len(db.freeConn) - 1
673+
db.freeConn[i] = db.freeConn[last]
674+
db.freeConn[last] = nil
675+
db.freeConn = db.freeConn[:last]
676+
i--
677+
}
678+
}
679+
db.mu.Unlock()
680+
681+
for _, c := range closing {
682+
c.Close()
683+
}
684+
685+
if d < minInterval {
686+
d = minInterval
687+
}
688+
t.Reset(d)
689+
}
690+
}
691+
597692
// DBStats contains database statistics.
598693
type DBStats struct {
599694
// OpenConnections is the number of open connections to the database.
@@ -657,8 +752,9 @@ func (db *DB) openNewConnection() {
657752
return
658753
}
659754
dc := &driverConn{
660-
db: db,
661-
ci: ci,
755+
db: db,
756+
createdAt: nowFunc(),
757+
ci: ci,
662758
}
663759
if db.putConnDBLocked(dc, err) {
664760
db.addDepLocked(dc, dc)
@@ -685,6 +781,7 @@ func (db *DB) conn(strategy connReuseStrategy) (*driverConn, error) {
685781
db.mu.Unlock()
686782
return nil, errDBClosed
687783
}
784+
lifetime := db.maxLifetime
688785

689786
// Prefer a free connection, if possible.
690787
numFree := len(db.freeConn)
@@ -694,6 +791,10 @@ func (db *DB) conn(strategy connReuseStrategy) (*driverConn, error) {
694791
db.freeConn = db.freeConn[:numFree-1]
695792
conn.inUse = true
696793
db.mu.Unlock()
794+
if conn.expired(lifetime) {
795+
conn.Close()
796+
return nil, driver.ErrBadConn
797+
}
697798
return conn, nil
698799
}
699800

@@ -709,6 +810,10 @@ func (db *DB) conn(strategy connReuseStrategy) (*driverConn, error) {
709810
if !ok {
710811
return nil, errDBClosed
711812
}
813+
if ret.err == nil && ret.conn.expired(lifetime) {
814+
ret.conn.Close()
815+
return nil, driver.ErrBadConn
816+
}
712817
return ret.conn, ret.err
713818
}
714819

@@ -724,8 +829,9 @@ func (db *DB) conn(strategy connReuseStrategy) (*driverConn, error) {
724829
}
725830
db.mu.Lock()
726831
dc := &driverConn{
727-
db: db,
728-
ci: ci,
832+
db: db,
833+
createdAt: nowFunc(),
834+
ci: ci,
729835
}
730836
db.addDepLocked(dc, dc)
731837
dc.inUse = true
@@ -835,6 +941,7 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
835941
return true
836942
} else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
837943
db.freeConn = append(db.freeConn, dc)
944+
db.startCleanerLocked()
838945
return true
839946
}
840947
return false

src/database/sql/sql_test.go

+101-30
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ func (db *DB) numFreeConns() int {
142142
return len(db.freeConn)
143143
}
144144

145+
// clearAllConns closes all connections in db.
146+
func (db *DB) clearAllConns(t *testing.T) {
147+
db.SetMaxIdleConns(0)
148+
149+
if g, w := db.numFreeConns(), 0; g != w {
150+
t.Errorf("free conns = %d; want %d", g, w)
151+
}
152+
153+
if n := db.numDepsPollUntil(0, time.Second); n > 0 {
154+
t.Errorf("number of dependencies = %d; expected 0", n)
155+
db.dumpDeps(t)
156+
}
157+
}
158+
145159
func (db *DB) dumpDeps(t *testing.T) {
146160
for fc := range db.dep {
147161
db.dumpDep(t, 0, fc, map[finalCloser]bool{})
@@ -991,16 +1005,7 @@ func TestMaxOpenConns(t *testing.T) {
9911005

9921006
// Force the number of open connections to 0 so we can get an accurate
9931007
// count for the test
994-
db.SetMaxIdleConns(0)
995-
996-
if g, w := db.numFreeConns(), 0; g != w {
997-
t.Errorf("free conns = %d; want %d", g, w)
998-
}
999-
1000-
if n := db.numDepsPollUntil(0, time.Second); n > 0 {
1001-
t.Errorf("number of dependencies = %d; expected 0", n)
1002-
db.dumpDeps(t)
1003-
}
1008+
db.clearAllConns(t)
10041009

10051010
driver.mu.Lock()
10061011
opens0 := driver.openCount
@@ -1096,16 +1101,7 @@ func TestMaxOpenConns(t *testing.T) {
10961101
db.dumpDeps(t)
10971102
}
10981103

1099-
db.SetMaxIdleConns(0)
1100-
1101-
if g, w := db.numFreeConns(), 0; g != w {
1102-
t.Errorf("free conns = %d; want %d", g, w)
1103-
}
1104-
1105-
if n := db.numDepsPollUntil(0, time.Second); n > 0 {
1106-
t.Errorf("number of dependencies = %d; expected 0", n)
1107-
db.dumpDeps(t)
1108-
}
1104+
db.clearAllConns(t)
11091105
}
11101106

11111107
// Issue 9453: tests that SetMaxOpenConns can be lowered at runtime
@@ -1263,6 +1259,90 @@ func TestStats(t *testing.T) {
12631259
}
12641260
}
12651261

1262+
func TestConnMaxLifetime(t *testing.T) {
1263+
t0 := time.Unix(1000000, 0)
1264+
offset := time.Duration(0)
1265+
1266+
nowFunc = func() time.Time { return t0.Add(offset) }
1267+
defer func() { nowFunc = time.Now }()
1268+
1269+
db := newTestDB(t, "magicquery")
1270+
defer closeDB(t, db)
1271+
1272+
driver := db.driver.(*fakeDriver)
1273+
1274+
// Force the number of open connections to 0 so we can get an accurate
1275+
// count for the test
1276+
db.clearAllConns(t)
1277+
1278+
driver.mu.Lock()
1279+
opens0 := driver.openCount
1280+
closes0 := driver.closeCount
1281+
driver.mu.Unlock()
1282+
1283+
db.SetMaxIdleConns(10)
1284+
db.SetMaxOpenConns(10)
1285+
1286+
tx, err := db.Begin()
1287+
if err != nil {
1288+
t.Fatal(err)
1289+
}
1290+
1291+
offset = time.Second
1292+
tx2, err := db.Begin()
1293+
if err != nil {
1294+
t.Fatal(err)
1295+
}
1296+
1297+
tx.Commit()
1298+
tx2.Commit()
1299+
1300+
driver.mu.Lock()
1301+
opens := driver.openCount - opens0
1302+
closes := driver.closeCount - closes0
1303+
driver.mu.Unlock()
1304+
1305+
if opens != 2 {
1306+
t.Errorf("opens = %d; want 2", opens)
1307+
}
1308+
if closes != 0 {
1309+
t.Errorf("closes = %d; want 0", closes)
1310+
}
1311+
if g, w := db.numFreeConns(), 2; g != w {
1312+
t.Errorf("free conns = %d; want %d", g, w)
1313+
}
1314+
1315+
// Expire first conn
1316+
offset = time.Second * 11
1317+
db.SetConnMaxLifetime(time.Second * 10)
1318+
if err != nil {
1319+
t.Fatal(err)
1320+
}
1321+
1322+
tx, err = db.Begin()
1323+
if err != nil {
1324+
t.Fatal(err)
1325+
}
1326+
tx2, err = db.Begin()
1327+
if err != nil {
1328+
t.Fatal(err)
1329+
}
1330+
tx.Commit()
1331+
tx2.Commit()
1332+
1333+
driver.mu.Lock()
1334+
opens = driver.openCount - opens0
1335+
closes = driver.closeCount - closes0
1336+
driver.mu.Unlock()
1337+
1338+
if opens != 3 {
1339+
t.Errorf("opens = %d; want 3", opens)
1340+
}
1341+
if closes != 1 {
1342+
t.Errorf("closes = %d; want 1", closes)
1343+
}
1344+
}
1345+
12661346
// golang.org/issue/5323
12671347
func TestStmtCloseDeps(t *testing.T) {
12681348
if testing.Short() {
@@ -1356,16 +1436,7 @@ func TestStmtCloseDeps(t *testing.T) {
13561436
db.dumpDeps(t)
13571437
}
13581438

1359-
db.SetMaxIdleConns(0)
1360-
1361-
if g, w := db.numFreeConns(), 0; g != w {
1362-
t.Errorf("free conns = %d; want %d", g, w)
1363-
}
1364-
1365-
if n := db.numDepsPollUntil(0, time.Second); n > 0 {
1366-
t.Errorf("number of dependencies = %d; expected 0", n)
1367-
db.dumpDeps(t)
1368-
}
1439+
db.clearAllConns(t)
13691440
}
13701441

13711442
// golang.org/issue/5046

0 commit comments

Comments
 (0)