Skip to content

Commit f82ebaf

Browse files
committed
[public-api] Implement connection pool with LRU cache
1 parent 7a5f156 commit f82ebaf

File tree

8 files changed

+171
-11
lines changed

8 files changed

+171
-11
lines changed

Diff for: components/gitpod-protocol/go/gitpod-service.go

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"encoding/json"
1212
"errors"
1313
"fmt"
14+
"io"
1415
"net/http"
1516
"net/url"
1617
"sync"
@@ -23,6 +24,8 @@ import (
2324

2425
// APIInterface wraps the
2526
type APIInterface interface {
27+
io.Closer
28+
2629
GetOwnerToken(ctx context.Context, workspaceID string) (res string, err error)
2730
AdminBlockUser(ctx context.Context, req *AdminBlockUserRequest) (err error)
2831
GetLoggedInUser(ctx context.Context) (res *User, err error)

Diff for: components/gitpod-protocol/go/mock.go

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: components/public-api-server/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ require (
3333
github.com/golang/protobuf v1.5.2 // indirect
3434
github.com/gorilla/websocket v1.5.0 // indirect
3535
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
36-
github.com/hashicorp/golang-lru v0.5.1 // indirect
36+
github.com/hashicorp/golang-lru v0.5.4 // indirect
3737
github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb // indirect
3838
github.com/inconshreveable/mousetrap v1.0.0 // indirect
3939
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect

Diff for: components/public-api-server/go.sum

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: components/public-api-server/pkg/proxy/conn.go

+72-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ package proxy
77
import (
88
"context"
99
"fmt"
10-
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
11-
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
1210
"net/url"
1311
"time"
12+
13+
"github.com/gitpod-io/gitpod/common-go/log"
14+
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
15+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
16+
17+
lru "github.com/hashicorp/golang-lru"
1418
)
1519

1620
type ServerConnectionPool interface {
@@ -42,3 +46,69 @@ func (p *NoConnectionPool) Get(ctx context.Context, token string) (gitpod.APIInt
4246

4347
return server, nil
4448
}
49+
50+
func NewConnectionPool(address *url.URL, poolSize int) (*ConnectionPool, error) {
51+
cache, err := lru.NewWithEvict(poolSize, func(key, value interface{}) {
52+
connectionPoolSize.Dec()
53+
54+
// We attempt to gracefully close the connection
55+
conn, ok := value.(gitpod.APIInterface)
56+
if !ok {
57+
return
58+
}
59+
60+
closeErr := conn.Close()
61+
if closeErr != nil {
62+
log.Log.WithError(closeErr).Warn("Failed to close connection to server.")
63+
}
64+
})
65+
if err != nil {
66+
return nil, fmt.Errorf("faield to create LRU cache: %w", err)
67+
}
68+
69+
return &ConnectionPool{
70+
cache: cache,
71+
connConstructor: func(token string) (gitpod.APIInterface, error) {
72+
return gitpod.ConnectToServer(address.String(), gitpod.ConnectToServerOpts{
73+
// We want the connection to persist beyond the lifecycle of a single request
74+
Context: context.Background(),
75+
Token: token,
76+
Log: log.Log,
77+
CloseHandler: func(err error) {
78+
cache.Remove(token)
79+
connectionPoolSize.Dec()
80+
},
81+
})
82+
},
83+
}, nil
84+
85+
}
86+
87+
type ConnectionPool struct {
88+
connConstructor func(token string) (gitpod.APIInterface, error)
89+
90+
// cache stores token to connection mapping
91+
cache *lru.Cache
92+
}
93+
94+
func (p *ConnectionPool) Get(ctx context.Context, token string) (gitpod.APIInterface, error) {
95+
cached, found := p.cache.Get(token)
96+
reportCacheOutcome(found)
97+
if found {
98+
connectionPoolCacheOutcome.WithLabelValues("true").Inc()
99+
conn, ok := cached.(*gitpod.APIoverJSONRPC)
100+
if ok {
101+
return conn, nil
102+
}
103+
}
104+
105+
conn, err := p.connConstructor(token)
106+
if err != nil {
107+
return nil, fmt.Errorf("failed to create new connection to server: %w", err)
108+
}
109+
110+
p.cache.Add(token, conn)
111+
connectionPoolSize.Inc()
112+
113+
return conn, nil
114+
}

Diff for: components/public-api-server/pkg/proxy/conn_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package proxy
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
12+
"github.com/golang/mock/gomock"
13+
lru "github.com/hashicorp/golang-lru"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestConnectionPool(t *testing.T) {
18+
ctrl := gomock.NewController(t)
19+
defer ctrl.Finish()
20+
srv := gitpod.NewMockAPIInterface(ctrl)
21+
22+
cache, err := lru.New(2)
23+
require.NoError(t, err)
24+
pool := &ConnectionPool{
25+
cache: cache,
26+
connConstructor: func(token string) (gitpod.APIInterface, error) {
27+
return srv, nil
28+
},
29+
}
30+
31+
_, err = pool.Get(context.Background(), "foo")
32+
require.NoError(t, err)
33+
require.Equal(t, 1, pool.cache.Len())
34+
35+
_, err = pool.Get(context.Background(), "bar")
36+
require.NoError(t, err)
37+
require.Equal(t, 2, pool.cache.Len())
38+
39+
_, err = pool.Get(context.Background(), "baz")
40+
require.NoError(t, err)
41+
require.Equal(t, 2, pool.cache.Len(), "must keep only last two connectons")
42+
require.True(t, pool.cache.Contains("bar"))
43+
require.True(t, pool.cache.Contains("baz"))
44+
}

Diff for: components/public-api-server/pkg/proxy/prometheusmetrics.go

+31-6
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,45 @@
55
package proxy
66

77
import (
8-
"github.com/prometheus/client_golang/prometheus"
8+
"strconv"
99
"time"
10+
11+
"github.com/prometheus/client_golang/prometheus"
1012
)
1113

1214
func reportConnectionDuration(d time.Duration) {
1315
proxyConnectionCreateDurationSeconds.Observe(d.Seconds())
1416
}
1517

16-
var proxyConnectionCreateDurationSeconds = prometheus.NewHistogram(prometheus.HistogramOpts{
17-
Namespace: "gitpod",
18-
Name: "public_api_proxy_connection_create_duration_seconds",
19-
Help: "Histogram of connection time in seconds",
20-
})
18+
var (
19+
proxyConnectionCreateDurationSeconds = prometheus.NewHistogram(prometheus.HistogramOpts{
20+
Namespace: "gitpod",
21+
Subsystem: "public_api",
22+
Name: "proxy_connection_create_duration_seconds",
23+
Help: "Histogram of connection time in seconds",
24+
})
25+
26+
connectionPoolSize = prometheus.NewGauge(prometheus.GaugeOpts{
27+
Namespace: "gitpod",
28+
Subsystem: "public_api",
29+
Name: "proxy_connection_pool_size",
30+
Help: "Gauge of connections in connection pool",
31+
})
32+
33+
connectionPoolCacheOutcome = prometheus.NewCounterVec(prometheus.CounterOpts{
34+
Namespace: "gitpod",
35+
Subsystem: "public_api",
36+
Name: "proxy_connection_pool_cache_outcomes_total",
37+
Help: "Counter of cachce accesses",
38+
}, []string{"hit"})
39+
)
2140

2241
func RegisterMetrics(registry *prometheus.Registry) {
2342
registry.MustRegister(proxyConnectionCreateDurationSeconds)
43+
registry.MustRegister(connectionPoolSize)
44+
registry.MustRegister(connectionPoolCacheOutcome)
45+
}
46+
47+
func reportCacheOutcome(hit bool) {
48+
connectionPoolCacheOutcome.WithLabelValues(strconv.FormatBool(hit)).Inc()
2449
}

Diff for: components/public-api-server/pkg/server/server.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro
3535
return fmt.Errorf("failed to parse Gitpod API URL: %w", err)
3636
}
3737

38-
connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI}
38+
connPool, err := proxy.NewConnectionPool(gitpodAPI, 3000)
39+
if err != nil {
40+
return fmt.Errorf("failed to setup connection pool: %w", err)
41+
}
3942

4043
srv, err := baseserver.New("public_api_server",
4144
baseserver.WithLogger(logger),
@@ -82,7 +85,6 @@ func register(srv *baseserver.Server, connPool proxy.ServerConnectionPool) error
8285
proxy.RegisterMetrics(srv.MetricsRegistry())
8386

8487
connectMetrics := NewConnectMetrics()
85-
8688
err := connectMetrics.Register(srv.MetricsRegistry())
8789
if err != nil {
8890
return err

0 commit comments

Comments
 (0)