You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
5.6 KiB
278 lines
5.6 KiB
// Package pool contains Telegram connections pool. |
|
package pool |
|
|
|
import ( |
|
"context" |
|
"sync" |
|
|
|
"github.com/go-faster/errors" |
|
"go.uber.org/atomic" |
|
"go.uber.org/zap" |
|
|
|
"github.com/gotd/td/bin" |
|
"github.com/gotd/td/internal/tdsync" |
|
) |
|
|
|
// DC represents connection pool to one data center. |
|
type DC struct { |
|
id int |
|
|
|
// Connection constructor. |
|
newConn func() Conn |
|
|
|
// Wrappers for external world, like logs or PRNG. |
|
log *zap.Logger // immutable |
|
|
|
// DC context. Will be canceled by Run on exit. |
|
ctx context.Context // immutable |
|
cancel context.CancelFunc // immutable |
|
|
|
// Connections supervisor. |
|
grp *tdsync.Supervisor |
|
// Free connections. |
|
free []*poolConn |
|
// Total connections. |
|
total int64 |
|
// Connection id monotonic counter. |
|
nextConn atomic.Int64 |
|
freeReq *reqMap |
|
// DC mutex. |
|
mu sync.Mutex |
|
|
|
// Limit of connections. |
|
max int64 // immutable |
|
|
|
// Signal connection for cases when all connections are dead, but all requests waiting for |
|
// free connection in 3rd acquire case. |
|
stuck *tdsync.ResetReady |
|
|
|
closed atomic.Bool |
|
} |
|
|
|
// NewDC creates new uninitialized DC. |
|
func NewDC(ctx context.Context, id int, newConn func() Conn, opts DCOptions) *DC { |
|
ctx, cancel := context.WithCancel(ctx) |
|
|
|
opts.setDefaults() |
|
return &DC{ |
|
id: id, |
|
newConn: newConn, |
|
log: opts.Logger, |
|
ctx: ctx, |
|
cancel: cancel, |
|
grp: tdsync.NewSupervisor(ctx), |
|
freeReq: newReqMap(), |
|
max: opts.MaxOpenConnections, |
|
stuck: tdsync.NewResetReady(), |
|
} |
|
} |
|
|
|
func (c *DC) createConnection(id int64) *poolConn { |
|
conn := &poolConn{ |
|
Conn: c.newConn(), |
|
id: id, |
|
dc: c, |
|
deleted: atomic.NewBool(false), |
|
dead: tdsync.NewReady(), |
|
} |
|
|
|
c.grp.Go(func(groupCtx context.Context) (err error) { |
|
defer c.dead(conn, err) |
|
return conn.Run(groupCtx) |
|
}) |
|
|
|
return conn |
|
} |
|
|
|
func (c *DC) dead(r *poolConn, deadErr error) { |
|
if r.deleted.Swap(true) { |
|
return // Already deleted. |
|
} |
|
|
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
c.total-- |
|
remaining := c.total |
|
if remaining < 0 { |
|
panic("unreachable: remaining can't be less than zero") |
|
} |
|
|
|
idx := -1 |
|
for i, conn := range c.free { |
|
// Search connection by pointer. |
|
if conn.id == r.id { |
|
idx = i |
|
} |
|
} |
|
|
|
if idx >= 0 { |
|
// Delete by index from slice tricks. |
|
copy(c.free[idx:], c.free[idx+1:]) |
|
// Delete reference to prevent resource leaking. |
|
c.free[len(c.free)-1] = nil |
|
c.free = c.free[:len(c.free)-1] |
|
} |
|
|
|
r.dead.Signal() |
|
c.stuck.Reset() |
|
|
|
c.log.Debug("Connection died", |
|
zap.Int64("remaining", remaining), |
|
zap.Int64("conn_id", r.id), |
|
zap.Error(deadErr), |
|
) |
|
} |
|
|
|
func (c *DC) pop() (r *poolConn, ok bool) { |
|
l := len(c.free) |
|
if l > 0 { |
|
r, c.free = c.free[l-1], c.free[:l-1] |
|
|
|
return r, true |
|
} |
|
|
|
return |
|
} |
|
|
|
func (c *DC) release(r *poolConn) { |
|
if r == nil { |
|
return |
|
} |
|
|
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
if c.freeReq.transfer(r) { |
|
c.log.Debug("Transfer connection to requester", zap.Int64("conn_id", r.id)) |
|
return |
|
} |
|
c.log.Debug("Connection released", zap.Int64("conn_id", r.id)) |
|
c.free = append(c.free, r) |
|
} |
|
|
|
var errDCIsClosed = errors.New("DC is closed") |
|
|
|
func (c *DC) acquire(ctx context.Context) (r *poolConn, err error) { // nolint:gocyclo |
|
retry: |
|
c.mu.Lock() |
|
// 1st case: have free connections. |
|
if r, ok := c.pop(); ok { |
|
c.mu.Unlock() |
|
select { |
|
case <-r.Dead(): |
|
c.dead(r, nil) |
|
goto retry |
|
default: |
|
} |
|
c.log.Debug("Re-using free connection", zap.Int64("conn_id", r.id)) |
|
return r, nil |
|
} |
|
|
|
// 2nd case: no free connections, but can create one. |
|
// c.max < 1 means unlimited |
|
if c.max < 1 || c.total < c.max { |
|
c.total++ |
|
c.mu.Unlock() |
|
|
|
id := c.nextConn.Inc() |
|
c.log.Debug("Creating new connection", |
|
zap.Int64("conn_id", id), |
|
) |
|
conn := c.createConnection(id) |
|
select { |
|
case <-ctx.Done(): |
|
return nil, ctx.Err() |
|
case <-c.ctx.Done(): |
|
return nil, errors.Wrap(c.ctx.Err(), "DC closed") |
|
case <-conn.Ready(): |
|
return conn, nil |
|
case <-conn.Dead(): |
|
c.dead(conn, nil) |
|
goto retry |
|
} |
|
} |
|
|
|
// 3rd case: no free connections, can't create yet one, wait for free. |
|
key, ch := c.freeReq.request() |
|
c.mu.Unlock() |
|
c.log.Debug("Waiting for free connect", zap.Int64("request_id", int64(key))) |
|
|
|
select { |
|
case conn := <-ch: |
|
c.log.Debug("Got connection for request", |
|
zap.Int64("conn_id", conn.id), |
|
zap.Int64("request_id", int64(key)), |
|
) |
|
return conn, nil |
|
case <-c.stuck.Ready(): |
|
c.log.Debug("Some connection dead, try to create new connection, cancel waiting") |
|
|
|
c.freeReq.delete(key) |
|
select { |
|
default: |
|
case conn, ok := <-ch: |
|
if ok && conn != nil { |
|
return conn, nil |
|
} |
|
} |
|
|
|
goto retry |
|
case <-ctx.Done(): |
|
err = ctx.Err() |
|
case <-c.ctx.Done(): |
|
err = errors.Wrap(c.ctx.Err(), "DC closed") |
|
} |
|
|
|
// Executed only if at least one of context is Done. |
|
c.freeReq.delete(key) |
|
select { |
|
default: |
|
case conn, ok := <-ch: |
|
if ok && conn != nil { |
|
c.release(conn) |
|
} |
|
} |
|
|
|
return nil, err |
|
} |
|
|
|
// Invoke sends MTProto request using one of pool connection. |
|
func (c *DC) Invoke(ctx context.Context, input bin.Encoder, output bin.Decoder) error { |
|
if c.closed.Load() { |
|
return errDCIsClosed |
|
} |
|
|
|
for { |
|
conn, err := c.acquire(ctx) |
|
if err != nil { |
|
if errors.Is(err, ErrConnDead) { |
|
continue |
|
} |
|
return errors.Wrap(err, "acquire connection") |
|
} |
|
|
|
c.log.Debug("DC Invoke") |
|
err = conn.Invoke(ctx, input, output) |
|
c.release(conn) |
|
if err != nil { |
|
c.log.Debug("DC Invoke failed", zap.Error(err)) |
|
return errors.Wrap(err, "invoke pool") |
|
} |
|
|
|
c.log.Debug("DC Invoke complete") |
|
return err |
|
} |
|
} |
|
|
|
// Close waits while all ongoing requests will be done or until given context is done. |
|
// Then, closes the DC. |
|
func (c *DC) Close() error { |
|
if c.closed.Swap(true) { |
|
return errors.New("DC already closed") |
|
} |
|
c.log.Debug("Closing DC") |
|
defer c.log.Debug("DC closed") |
|
|
|
c.cancel() |
|
return c.grp.Wait() |
|
}
|
|
|