storj/pkg/rpc/rpcpool/pool.go
Jeff Wendling 51d5d8656a pkg/rpc: drpc connection pooling
keep a pool of connections open when dialing for drpc. this
makes it so that long lived clients (like lib/uplink's Project)
don't continue to use a bad connection forever. it also allows
for concurrent rpcs.

Change-Id: If649b286050e4f09c413fadc3e1ce88f5fc6e600
2019-10-22 18:15:24 -06:00

164 lines
3.7 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package rpcpool
import (
"context"
"sync"
"github.com/zeebo/errs"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/drpc"
"storj.io/drpc/drpcconn"
)
var mon = monkit.Package()
// Error is the class of errors returned by this package.
var Error = errs.Class("rpcpool")
// Dialer is the type of function to create a new connection.
type Dialer = func(context.Context) (drpc.Transport, error)
// Conn implements drpc.Conn but keeps a pool of connections open.
type Conn struct {
mu sync.Mutex
pool chan *drpcconn.Conn
done chan struct{}
dial Dialer
}
var _ drpc.Conn = (*Conn)(nil)
// New returns a new Conn that will keep cap connections open using the provided
// dialer when it needs new ones.
func New(cap int, dial Dialer) *Conn {
return &Conn{
pool: make(chan *drpcconn.Conn, cap),
done: make(chan struct{}),
dial: dial,
}
}
// Close closes all of the pool's connections and ensures no new ones will be made.
func (c *Conn) Close() (err error) {
var pool chan *drpcconn.Conn
// only one call will ever see a non-nil pool variable. additionally, anyone
// holding the mutex will either see a nil c.pool or a non-closed c.pool.
c.mu.Lock()
pool, c.pool = c.pool, nil
c.mu.Unlock()
if pool != nil {
close(pool)
for conn := range pool {
err = errs.Combine(err, conn.Close())
}
close(c.done)
}
<-c.done
return err
}
// newConn creates a new connection using the dialer.
func (c *Conn) newConn(ctx context.Context) (_ *drpcconn.Conn, err error) {
defer mon.Task()(&ctx)(&err)
tr, err := c.dial(ctx)
if err != nil {
return nil, err
}
return drpcconn.New(tr), nil
}
// getConn attempts to get a pooled connection or dials a new one if necessary.
func (c *Conn) getConn(ctx context.Context) (_ *drpcconn.Conn, err error) {
defer mon.Task()(&ctx)(&err)
c.mu.Lock()
pool := c.pool
c.mu.Unlock()
for {
select {
case conn, ok := <-pool:
if !ok {
return nil, Error.New("connection pool closed")
}
// if the connection died in the pool, try again
if conn.Closed() {
continue
}
return conn, nil
default:
return c.newConn(ctx)
}
}
}
// Put places the connection back into the pool if there's room. It
// closes the connection if there is no room or the pool is closed. If the
// connection is closed, it does not attempt to place it into the pool.
func (c *Conn) Put(conn *drpcconn.Conn) error {
c.mu.Lock()
defer c.mu.Unlock()
// if the connection is closed already, don't replace it.
if conn.Closed() {
return nil
}
select {
case c.pool <- conn:
return nil
default:
return conn.Close()
}
}
// Transport returns nil because there is no well defined transport to use.
func (c *Conn) Transport() drpc.Transport { return nil }
// Invoke implements drpc.Conn's Invoke method using a pooled connection.
func (c *Conn) Invoke(ctx context.Context, rpc string, in drpc.Message, out drpc.Message) (err error) {
defer mon.Task()(&ctx)(&err)
conn, err := c.getConn(ctx)
if err != nil {
return err
}
err = conn.Invoke(ctx, rpc, in, out)
return errs.Combine(err, c.Put(conn))
}
// NewStream implements drpc.Conn's NewStream method using a pooled connection. It
// waits for the stream to be finished before replacing the connection into the pool.
func (c *Conn) NewStream(ctx context.Context, rpc string) (_ drpc.Stream, err error) {
defer mon.Task()(&ctx)(&err)
conn, err := c.getConn(ctx)
if err != nil {
return nil, err
}
stream, err := conn.NewStream(ctx, rpc)
if err != nil {
return nil, err
}
// the stream's done channel is closed when we're sure no reads/writes are
// coming in for that stream anymore. it has been fully terminated.
go func() {
<-stream.Context().Done()
_ = c.Put(conn)
}()
return stream, nil
}