Skip to content

Commit f4ad20b

Browse files
perf(redis): tune client pool config (#24)
1 parent 9424b06 commit f4ad20b

7 files changed

Lines changed: 217 additions & 24 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ http://localhost:8080/mcp
181181
```text
182182
PKGSITE_BASE_URL=https://pkg.go.dev/v1beta
183183
KV_REDIS_URL=redis://localhost:9736/0
184+
KV_REDIS_POOL_SIZE=4
185+
KV_REDIS_MIN_IDLE_CONNS=2
186+
KV_REDIS_MAX_IDLE_CONNS=4
187+
KV_REDIS_MAX_ACTIVE_CONNS=8
188+
KV_REDIS_POOL_TIMEOUT=250ms
189+
KV_REDIS_DIAL_TIMEOUT=1s
190+
KV_REDIS_READ_TIMEOUT=750ms
191+
KV_REDIS_WRITE_TIMEOUT=750ms
192+
KV_REDIS_CONN_MAX_IDLE_TIME=10m
193+
KV_REDIS_DISABLE_IDENTITY=true
184194
PKGSITE_HTTP_TIMEOUT=10s
185195
PKGSITE_CACHE_DISABLED=false
186196
RATE_LIMIT_ENABLED=true

internal/config/config.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,25 @@ type Pkgsite struct {
2525
}
2626

2727
type KV struct {
28-
RedisURL string
28+
RedisURL string
29+
RedisPool RedisPool
30+
RedisTimeouts RedisTimeouts
31+
RedisConnMaxIdle time.Duration
32+
RedisDisableIdentity bool
33+
}
34+
35+
type RedisPool struct {
36+
Size int
37+
MinIdleConns int
38+
MaxIdleConns int
39+
MaxActiveConns int
40+
Timeout time.Duration
41+
}
42+
43+
type RedisTimeouts struct {
44+
Dial time.Duration
45+
Read time.Duration
46+
Write time.Duration
2947
}
3048

3149
type RateLimit struct {
@@ -62,6 +80,20 @@ func read(getenv func(string) string) (Config, error) {
6280
Port: p.str("PORT", "8080"),
6381
KV: KV{
6482
RedisURL: p.str("KV_REDIS_URL", ""),
83+
RedisPool: RedisPool{
84+
Size: p.intVal("KV_REDIS_POOL_SIZE", 4),
85+
MinIdleConns: p.intVal("KV_REDIS_MIN_IDLE_CONNS", 2),
86+
MaxIdleConns: p.intVal("KV_REDIS_MAX_IDLE_CONNS", 4),
87+
MaxActiveConns: p.intVal("KV_REDIS_MAX_ACTIVE_CONNS", 8),
88+
Timeout: p.duration("KV_REDIS_POOL_TIMEOUT", 250*time.Millisecond),
89+
},
90+
RedisTimeouts: RedisTimeouts{
91+
Dial: p.duration("KV_REDIS_DIAL_TIMEOUT", time.Second),
92+
Read: p.duration("KV_REDIS_READ_TIMEOUT", 750*time.Millisecond),
93+
Write: p.duration("KV_REDIS_WRITE_TIMEOUT", 750*time.Millisecond),
94+
},
95+
RedisConnMaxIdle: p.duration("KV_REDIS_CONN_MAX_IDLE_TIME", 10*time.Minute),
96+
RedisDisableIdentity: p.boolean("KV_REDIS_DISABLE_IDENTITY", true),
6597
},
6698
Observability: Observability{
6799
ServiceName: p.str("O11Y_SERVICE_NAME", "pkgsite-mcp"),

internal/config/config_test.go

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,36 @@ func TestReadDefaults(t *testing.T) {
3030
if got.KV.RedisURL != "" {
3131
t.Fatalf("RedisURL = %q, want empty", got.KV.RedisURL)
3232
}
33+
if got.KV.RedisPool.Size != 4 {
34+
t.Fatalf("RedisPool.Size = %d, want 4", got.KV.RedisPool.Size)
35+
}
36+
if got.KV.RedisPool.MinIdleConns != 2 {
37+
t.Fatalf("RedisPool.MinIdleConns = %d, want 2", got.KV.RedisPool.MinIdleConns)
38+
}
39+
if got.KV.RedisPool.MaxIdleConns != 4 {
40+
t.Fatalf("RedisPool.MaxIdleConns = %d, want 4", got.KV.RedisPool.MaxIdleConns)
41+
}
42+
if got.KV.RedisPool.MaxActiveConns != 8 {
43+
t.Fatalf("RedisPool.MaxActiveConns = %d, want 8", got.KV.RedisPool.MaxActiveConns)
44+
}
45+
if got.KV.RedisPool.Timeout != 250*time.Millisecond {
46+
t.Fatalf("RedisPool.Timeout = %s, want 250ms", got.KV.RedisPool.Timeout)
47+
}
48+
if got.KV.RedisTimeouts.Dial != time.Second {
49+
t.Fatalf("RedisTimeouts.Dial = %s, want 1s", got.KV.RedisTimeouts.Dial)
50+
}
51+
if got.KV.RedisTimeouts.Read != 750*time.Millisecond {
52+
t.Fatalf("RedisTimeouts.Read = %s, want 750ms", got.KV.RedisTimeouts.Read)
53+
}
54+
if got.KV.RedisTimeouts.Write != 750*time.Millisecond {
55+
t.Fatalf("RedisTimeouts.Write = %s, want 750ms", got.KV.RedisTimeouts.Write)
56+
}
57+
if got.KV.RedisConnMaxIdle != 10*time.Minute {
58+
t.Fatalf("RedisConnMaxIdle = %s, want 10m", got.KV.RedisConnMaxIdle)
59+
}
60+
if !got.KV.RedisDisableIdentity {
61+
t.Fatal("RedisDisableIdentity = false, want true")
62+
}
3363
if got.Pkgsite.CacheDisabled {
3464
t.Fatal("CacheDisabled = true, want false")
3565
}
@@ -57,21 +87,31 @@ func TestReadOverrides(t *testing.T) {
5787
t.Parallel()
5888

5989
got, err := read(mapGetenv(map[string]string{
60-
"PORT": "9090",
61-
"O11Y_SERVICE_NAME": "pkgsite-test",
62-
"O11Y_ENVIRONMENT": "test",
63-
"O11Y_FLUSH_TIMEOUT": "5s",
64-
"O11Y_TRACES_SAMPLE_RATE": "0.25",
65-
"O11Y_ENABLE_LOGS": "false",
66-
"O11Y_ENABLE_METRICS": "false",
67-
"PKGSITE_BASE_URL": "http://example.test",
68-
"PKGSITE_HTTP_TIMEOUT": "250ms",
69-
"KV_REDIS_URL": "redis://localhost:6379/0",
70-
"PKGSITE_CACHE_DISABLED": "true",
71-
"RATE_LIMIT_ENABLED": "false",
72-
"RATE_LIMIT_REQUESTS": "10",
73-
"RATE_LIMIT_WINDOW": "30s",
74-
"SENTRY_DSN": "https://public@example.invalid/1",
90+
"PORT": "9090",
91+
"O11Y_SERVICE_NAME": "pkgsite-test",
92+
"O11Y_ENVIRONMENT": "test",
93+
"O11Y_FLUSH_TIMEOUT": "5s",
94+
"O11Y_TRACES_SAMPLE_RATE": "0.25",
95+
"O11Y_ENABLE_LOGS": "false",
96+
"O11Y_ENABLE_METRICS": "false",
97+
"PKGSITE_BASE_URL": "http://example.test",
98+
"PKGSITE_HTTP_TIMEOUT": "250ms",
99+
"KV_REDIS_URL": "redis://localhost:6379/0",
100+
"KV_REDIS_POOL_SIZE": "12",
101+
"KV_REDIS_MIN_IDLE_CONNS": "3",
102+
"KV_REDIS_MAX_IDLE_CONNS": "6",
103+
"KV_REDIS_MAX_ACTIVE_CONNS": "18",
104+
"KV_REDIS_POOL_TIMEOUT": "400ms",
105+
"KV_REDIS_DIAL_TIMEOUT": "2s",
106+
"KV_REDIS_READ_TIMEOUT": "900ms",
107+
"KV_REDIS_WRITE_TIMEOUT": "950ms",
108+
"KV_REDIS_CONN_MAX_IDLE_TIME": "12m",
109+
"KV_REDIS_DISABLE_IDENTITY": "false",
110+
"PKGSITE_CACHE_DISABLED": "true",
111+
"RATE_LIMIT_ENABLED": "false",
112+
"RATE_LIMIT_REQUESTS": "10",
113+
"RATE_LIMIT_WINDOW": "30s",
114+
"SENTRY_DSN": "https://public@example.invalid/1",
75115
}))
76116
if err != nil {
77117
t.Fatalf("Read returned error: %v", err)
@@ -103,6 +143,36 @@ func TestReadOverrides(t *testing.T) {
103143
if got.KV.RedisURL != "redis://localhost:6379/0" {
104144
t.Fatalf("RedisURL = %q, want override", got.KV.RedisURL)
105145
}
146+
if got.KV.RedisPool.Size != 12 {
147+
t.Fatalf("RedisPool.Size = %d, want 12", got.KV.RedisPool.Size)
148+
}
149+
if got.KV.RedisPool.MinIdleConns != 3 {
150+
t.Fatalf("RedisPool.MinIdleConns = %d, want 3", got.KV.RedisPool.MinIdleConns)
151+
}
152+
if got.KV.RedisPool.MaxIdleConns != 6 {
153+
t.Fatalf("RedisPool.MaxIdleConns = %d, want 6", got.KV.RedisPool.MaxIdleConns)
154+
}
155+
if got.KV.RedisPool.MaxActiveConns != 18 {
156+
t.Fatalf("RedisPool.MaxActiveConns = %d, want 18", got.KV.RedisPool.MaxActiveConns)
157+
}
158+
if got.KV.RedisPool.Timeout != 400*time.Millisecond {
159+
t.Fatalf("RedisPool.Timeout = %s, want 400ms", got.KV.RedisPool.Timeout)
160+
}
161+
if got.KV.RedisTimeouts.Dial != 2*time.Second {
162+
t.Fatalf("RedisTimeouts.Dial = %s, want 2s", got.KV.RedisTimeouts.Dial)
163+
}
164+
if got.KV.RedisTimeouts.Read != 900*time.Millisecond {
165+
t.Fatalf("RedisTimeouts.Read = %s, want 900ms", got.KV.RedisTimeouts.Read)
166+
}
167+
if got.KV.RedisTimeouts.Write != 950*time.Millisecond {
168+
t.Fatalf("RedisTimeouts.Write = %s, want 950ms", got.KV.RedisTimeouts.Write)
169+
}
170+
if got.KV.RedisConnMaxIdle != 12*time.Minute {
171+
t.Fatalf("RedisConnMaxIdle = %s, want 12m", got.KV.RedisConnMaxIdle)
172+
}
173+
if got.KV.RedisDisableIdentity {
174+
t.Fatal("RedisDisableIdentity = true, want false")
175+
}
106176
if !got.Pkgsite.CacheDisabled {
107177
t.Fatal("CacheDisabled = false, want true")
108178
}
@@ -148,12 +218,14 @@ func TestReadParseErrorReportsAllFailures(t *testing.T) {
148218
_, err := read(mapGetenv(map[string]string{
149219
"O11Y_FLUSH_TIMEOUT": "soon",
150220
"PKGSITE_HTTP_TIMEOUT": "later",
221+
"KV_REDIS_POOL_SIZE": "several",
151222
"RATE_LIMIT_REQUESTS": "lots",
152223
}))
153224
if err == nil {
154225
t.Fatal("Read returned nil error, want parse error")
155226
}
156-
const want = `config: parsing O11Y_FLUSH_TIMEOUT="soon": time: invalid duration "soon"
227+
const want = `config: parsing KV_REDIS_POOL_SIZE="several": strconv.Atoi: parsing "several": invalid syntax
228+
config: parsing O11Y_FLUSH_TIMEOUT="soon": time: invalid duration "soon"
157229
config: parsing PKGSITE_HTTP_TIMEOUT="later": time: invalid duration "later"
158230
config: parsing RATE_LIMIT_REQUESTS="lots": strconv.Atoi: parsing "lots": invalid syntax`
159231
if err.Error() != want {

internal/httpserver/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func Run(ctx context.Context, cfg Config, logger *slog.Logger) error {
5959
}()
6060
logger = obs.Logger
6161

62-
store, err := kvredis.New(cfg.KV.RedisURL)
62+
store, err := kvredis.New(cfg.KV)
6363
if err != nil {
6464
return fmt.Errorf("configure kv store: %w", err)
6565
}

internal/kv/redis/redis.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"time"
99

10+
"github.com/garrettladley/pkgsite-mcp/internal/config"
1011
"github.com/garrettladley/pkgsite-mcp/internal/kv"
1112
"github.com/redis/go-redis/extra/redisotel/v9"
1213
goredis "github.com/redis/go-redis/v9"
@@ -23,18 +24,19 @@ type Store struct {
2324

2425
var _ kv.Store = (*Store)(nil)
2526

26-
func New(redisURL string) (kv.Store, error) {
27-
if redisURL == "" {
27+
func New(cfg config.KV) (kv.Store, error) {
28+
if cfg.RedisURL == "" {
2829
return nil, nil
2930
}
30-
return NewStore(redisURL)
31+
return NewStore(cfg)
3132
}
3233

33-
func NewStore(redisURL string) (*Store, error) {
34-
opts, err := goredis.ParseURL(redisURL)
34+
func NewStore(cfg config.KV) (*Store, error) {
35+
opts, err := goredis.ParseURL(cfg.RedisURL)
3536
if err != nil {
3637
return nil, err
3738
}
39+
applyOptions(opts, cfg)
3840
client := goredis.NewClient(opts)
3941
if err := redisotel.InstrumentTracing(client); err != nil {
4042
return nil, err
@@ -45,6 +47,19 @@ func NewStore(redisURL string) (*Store, error) {
4547
return &Store{client: client}, nil
4648
}
4749

50+
func applyOptions(opts *goredis.Options, cfg config.KV) {
51+
opts.DisableIdentity = cfg.RedisDisableIdentity
52+
opts.PoolSize = cfg.RedisPool.Size
53+
opts.MinIdleConns = cfg.RedisPool.MinIdleConns
54+
opts.MaxIdleConns = cfg.RedisPool.MaxIdleConns
55+
opts.MaxActiveConns = cfg.RedisPool.MaxActiveConns
56+
opts.PoolTimeout = cfg.RedisPool.Timeout
57+
opts.DialTimeout = cfg.RedisTimeouts.Dial
58+
opts.ReadTimeout = cfg.RedisTimeouts.Read
59+
opts.WriteTimeout = cfg.RedisTimeouts.Write
60+
opts.ConnMaxIdleTime = cfg.RedisConnMaxIdle
61+
}
62+
4863
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
4964
value, err := s.client.Get(ctx, key).Bytes()
5065
if errors.Is(err, goredis.Nil) {

internal/kv/redis/redis_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package redis
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/garrettladley/pkgsite-mcp/internal/config"
8+
goredis "github.com/redis/go-redis/v9"
9+
)
10+
11+
func TestApplyOptions(t *testing.T) {
12+
t.Parallel()
13+
14+
cfg := config.KV{
15+
RedisPool: config.RedisPool{
16+
Size: 4,
17+
MinIdleConns: 2,
18+
MaxIdleConns: 4,
19+
MaxActiveConns: 8,
20+
Timeout: 250 * time.Millisecond,
21+
},
22+
RedisTimeouts: config.RedisTimeouts{
23+
Dial: time.Second,
24+
Read: 750 * time.Millisecond,
25+
Write: 750 * time.Millisecond,
26+
},
27+
RedisConnMaxIdle: 10 * time.Minute,
28+
RedisDisableIdentity: true,
29+
}
30+
opts := &goredis.Options{}
31+
32+
applyOptions(opts, cfg)
33+
34+
if !opts.DisableIdentity {
35+
t.Fatal("DisableIdentity = false, want true")
36+
}
37+
if opts.PoolSize != 4 {
38+
t.Fatalf("PoolSize = %d, want 4", opts.PoolSize)
39+
}
40+
if opts.MinIdleConns != 2 {
41+
t.Fatalf("MinIdleConns = %d, want 2", opts.MinIdleConns)
42+
}
43+
if opts.MaxIdleConns != 4 {
44+
t.Fatalf("MaxIdleConns = %d, want 4", opts.MaxIdleConns)
45+
}
46+
if opts.MaxActiveConns != 8 {
47+
t.Fatalf("MaxActiveConns = %d, want 8", opts.MaxActiveConns)
48+
}
49+
if opts.PoolTimeout != 250*time.Millisecond {
50+
t.Fatalf("PoolTimeout = %s, want 250ms", opts.PoolTimeout)
51+
}
52+
if opts.DialTimeout != time.Second {
53+
t.Fatalf("DialTimeout = %s, want 1s", opts.DialTimeout)
54+
}
55+
if opts.ReadTimeout != 750*time.Millisecond {
56+
t.Fatalf("ReadTimeout = %s, want 750ms", opts.ReadTimeout)
57+
}
58+
if opts.WriteTimeout != 750*time.Millisecond {
59+
t.Fatalf("WriteTimeout = %s, want 750ms", opts.WriteTimeout)
60+
}
61+
if opts.ConnMaxIdleTime != 10*time.Minute {
62+
t.Fatalf("ConnMaxIdleTime = %s, want 10m", opts.ConnMaxIdleTime)
63+
}
64+
}

internal/mcpserver/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func RunStdio(ctx context.Context) error {
5151
}
5252
}()
5353

54-
store, err := kvredis.New(cfg.KV.RedisURL)
54+
store, err := kvredis.New(cfg.KV)
5555
if err != nil {
5656
return fmt.Errorf("configure kv store: %w", err)
5757
}

0 commit comments

Comments
 (0)