Skip to content

Commit 68dfbb1

Browse files
author
Neven Miculinic
committed
feat(chpool): add healthcheck function
1 parent dbcdccf commit 68dfbb1

5 files changed

Lines changed: 124 additions & 30 deletions

File tree

‎chpool/client.go‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package chpool
22

33
import (
44
"context"
5-
"time"
6-
7-
"github.com/jackc/puddle/v2"
85

96
"github.com/ClickHouse/ch-go"
7+
"github.com/jackc/puddle/v2"
108
)
119

1210
// Client is an acquired *ch.Client from a Pool.
@@ -21,14 +19,16 @@ func (c *Client) Release() {
2119
return
2220
}
2321

24-
client := c.client()
25-
26-
if client.IsClosed() || time.Since(c.res.CreationTime()) > c.p.options.MaxConnLifetime {
27-
c.res.Destroy()
28-
return
29-
}
30-
31-
c.res.Release()
22+
// calling async since connIsHealthy may block
23+
go func() {
24+
if c.p.connIsHealthy(c.res) {
25+
c.p.options.ClientOptions.Logger.Debug("chpool: releasing connection")
26+
c.res.Release()
27+
} else {
28+
c.p.options.ClientOptions.Logger.Debug("chpool: destoying connection")
29+
c.res.Destroy()
30+
}
31+
}()
3232
}
3333

3434
func (c *Client) Do(ctx context.Context, q ch.Query) (err error) {

‎chpool/client_test.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package chpool
22

33
import (
44
"context"
5+
"sync/atomic"
56
"testing"
67

8+
"github.com/ClickHouse/ch-go"
9+
"github.com/stretchr/testify/assert"
710
"github.com/stretchr/testify/require"
811
)
912

@@ -17,6 +20,24 @@ func TestClient_Do(t *testing.T) {
1720
testDo(t, conn)
1821
}
1922

23+
func TestClient_ReleaseHealthCheck(t *testing.T) {
24+
t.Parallel()
25+
var healthCheckCnt int64
26+
p := PoolConnOpt(t, Options{
27+
HealthCheckFunc: func(ctx context.Context, client *ch.Client) error {
28+
atomic.AddInt64(&healthCheckCnt, 1)
29+
return nil
30+
},
31+
})
32+
conn, err := p.Acquire(context.Background())
33+
require.NoError(t, err)
34+
assert.Equal(t, int64(0), atomic.LoadInt64(&healthCheckCnt))
35+
36+
conn.Release()
37+
waitForReleaseToComplete()
38+
assert.Equal(t, int64(1), atomic.LoadInt64(&healthCheckCnt))
39+
}
40+
2041
func TestClient_Ping(t *testing.T) {
2142
t.Parallel()
2243
p := PoolConn(t)

‎chpool/conn.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package chpool
22

33
import (
4+
"time"
5+
46
"github.com/jackc/puddle/v2"
57

68
"github.com/ClickHouse/ch-go"
79
)
810

911
type connResource struct {
10-
client *ch.Client
11-
clients []Client
12+
lastHealthCheckTimestamp time.Time
13+
client *ch.Client
14+
clients []Client
1215
}
1316

1417
func (cr *connResource) getConn(p *Pool, res *puddle.Resource[*connResource]) *Client {

‎chpool/pool.go‎

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"time"
88

99
"github.com/ClickHouse/ch-go"
10-
1110
"github.com/jackc/puddle/v2"
11+
"go.uber.org/zap"
1212
)
1313

1414
// Pool of connections to ClickHouse.
@@ -23,19 +23,26 @@ type Pool struct {
2323

2424
// Options for Pool.
2525
type Options struct {
26-
ClientOptions ch.Options
27-
MaxConnLifetime time.Duration
28-
MaxConnIdleTime time.Duration
29-
MaxConns int32
30-
MinConns int32
31-
HealthCheckPeriod time.Duration
26+
ClientOptions ch.Options
27+
MaxConnLifetime time.Duration
28+
MaxConnIdleTime time.Duration
29+
MaxConns int32
30+
MinConns int32
31+
HealthCheckPeriod time.Duration
32+
HealthCheckFunc func(ctx context.Context, client *ch.Client) error
33+
HealthCheckTimeout time.Duration
34+
}
35+
36+
func DefaultHealthCheckFunc(ctx context.Context, client *ch.Client) error {
37+
return client.Ping(ctx)
3238
}
3339

3440
// Defaults for pool.
3541
const (
36-
DefaultMaxConnLifetime = time.Hour
37-
DefaultMaxConnIdleTime = time.Minute * 30
38-
DefaultHealthCheckPeriod = time.Minute
42+
DefaultMaxConnLifetime = time.Hour
43+
DefaultMaxConnIdleTime = time.Minute * 30
44+
DefaultHealthCheckPeriod = time.Minute
45+
DefaultHealthCheckTimeout = time.Second
3946
)
4047

4148
func (o *Options) setDefaults() {
@@ -51,6 +58,15 @@ func (o *Options) setDefaults() {
5158
if o.HealthCheckPeriod == 0 {
5259
o.HealthCheckPeriod = DefaultHealthCheckPeriod
5360
}
61+
if o.HealthCheckFunc == nil {
62+
o.HealthCheckFunc = DefaultHealthCheckFunc
63+
}
64+
if o.HealthCheckTimeout == 0 {
65+
o.HealthCheckTimeout = DefaultHealthCheckTimeout
66+
}
67+
if o.ClientOptions.Logger == nil {
68+
o.ClientOptions.Logger = zap.NewNop()
69+
}
5470
}
5571

5672
// Dial returns a pool of connections to ClickHouse.
@@ -162,16 +178,46 @@ func (p *Pool) backgroundHealthCheck() {
162178
func (p *Pool) checkIdleConnsHealth() {
163179
resources := p.pool.AcquireAllIdle()
164180

165-
now := time.Now()
181+
wg := sync.WaitGroup{}
166182
for _, res := range resources {
167-
if now.Sub(res.CreationTime()) > p.options.MaxConnLifetime {
168-
res.Destroy()
169-
} else if res.IdleDuration() > p.options.MaxConnIdleTime {
170-
res.Destroy()
171-
} else {
172-
res.ReleaseUnused()
183+
res := res
184+
wg.Add(1)
185+
go func() {
186+
if res.IdleDuration() > p.options.MaxConnIdleTime || !p.connIsHealthy(res) {
187+
res.Destroy()
188+
} else {
189+
res.ReleaseUnused()
190+
}
191+
}()
192+
wg.Wait()
193+
}
194+
}
195+
196+
func (p *Pool) connIsHealthy(res *puddle.Resource[*connResource]) bool {
197+
logger := p.options.ClientOptions.Logger
198+
if res.Value().client.IsClosed() {
199+
logger.Debug("chpool: connection is closed")
200+
return false
201+
}
202+
203+
if time.Since(res.CreationTime()) > p.options.MaxConnLifetime {
204+
logger.Debug("chpool: connection over max lifetime")
205+
return false
206+
}
207+
208+
if p.options.HealthCheckFunc != nil {
209+
ctx, cancel := context.WithTimeout(context.Background(), p.options.HealthCheckTimeout)
210+
defer cancel()
211+
if err := p.options.HealthCheckFunc(ctx, res.Value().client); err != nil {
212+
if logger := p.options.ClientOptions.Logger; logger != nil {
213+
logger.Warn("chpool: health check failed", zap.Error(err))
214+
}
215+
return false
173216
}
174217
}
218+
219+
res.Value().lastHealthCheckTimestamp = time.Now()
220+
return true
175221
}
176222

177223
func (p *Pool) checkMinConns() {

‎chpool/pool_test.go‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package chpool
22

33
import (
44
"context"
5+
"sync/atomic"
56
"testing"
67
"time"
78

9+
"github.com/ClickHouse/ch-go"
10+
811
"github.com/stretchr/testify/assert"
912
"github.com/stretchr/testify/require"
1013
)
@@ -61,6 +64,7 @@ func TestPool_Ping(t *testing.T) {
6164
p := PoolConn(t)
6265

6366
require.NoError(t, p.Ping(context.Background()))
67+
waitForReleaseToComplete()
6468

6569
stats := p.Stat()
6670
assert.EqualValues(t, 0, stats.AcquiredResources())
@@ -78,3 +82,23 @@ func TestPool_Acquire(t *testing.T) {
7882
waitForReleaseToComplete()
7983
require.EqualValues(t, 2, p.Stat().AcquireCount())
8084
}
85+
86+
func TestPool_backgroundHealthCheck(t *testing.T) {
87+
t.Parallel()
88+
var healthCheckCnt int64
89+
p := PoolConnOpt(t, Options{
90+
MinConns: 1,
91+
HealthCheckFunc: func(ctx context.Context, client *ch.Client) error {
92+
atomic.AddInt64(&healthCheckCnt, 1)
93+
return nil
94+
},
95+
HealthCheckPeriod: 500 * time.Millisecond,
96+
})
97+
p.checkMinConns()
98+
p.checkIdleConnsHealth()
99+
assert.GreaterOrEqual(t, int64(1), atomic.LoadInt64(&healthCheckCnt))
100+
101+
hc := atomic.LoadInt64(&healthCheckCnt)
102+
time.Sleep(750 * time.Millisecond)
103+
assert.Equal(t, hc+1, atomic.LoadInt64(&healthCheckCnt))
104+
}

0 commit comments

Comments
 (0)