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.

252 lines
5.8 KiB

package manager
import (
"context"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-faster/errors"
"go.uber.org/zap"
"github.com/gotd/td/bin"
"github.com/gotd/td/clock"
"github.com/gotd/td/internal/mtproto"
"github.com/gotd/td/internal/pool"
"github.com/gotd/td/internal/tdsync"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
)
type protoConn interface {
Invoke(ctx context.Context, input bin.Encoder, output bin.Decoder) error
Run(ctx context.Context, f func(ctx context.Context) error) error
Ping(ctx context.Context) error
}
//go:generate go run -modfile=../../../_tools/go.mod golang.org/x/tools/cmd/stringer -type=ConnMode
// ConnMode represents connection mode.
type ConnMode byte
const (
// ConnModeUpdates is update connection mode.
ConnModeUpdates ConnMode = iota
// ConnModeData is data connection mode.
ConnModeData
// ConnModeCDN is CDN connection mode.
ConnModeCDN
)
// Conn is a Telegram client connection.
type Conn struct {
// Connection parameters.
mode ConnMode // immutable
// MTProto connection.
proto protoConn // immutable
// InitConnection parameters.
appID int // immutable
device DeviceConfig // immutable
// setup is callback which called after initConnection, but before ready signaling.
// This is necessary to transfer auth from previous connection to another DC.
setup SetupCallback // nilable
// Wrappers for external world, like logs or PRNG.
// Should be immutable.
clock clock.Clock // immutable
log *zap.Logger // immutable
// Handler passed by client.
handler Handler // immutable
// State fields.
cfg tg.Config
ongoing int
latest time.Time
mux sync.Mutex
sessionInit *tdsync.Ready // immutable
gotConfig *tdsync.Ready // immutable
dead *tdsync.Ready // immutable
connBackoff func(ctx context.Context) backoff.BackOff // immutable
}
// OnSession implements mtproto.Handler.
func (c *Conn) OnSession(session mtproto.Session) error {
c.log.Info("SessionInit")
c.sessionInit.Signal()
// Waiting for config, because OnSession can occur before we set config.
select {
case <-c.gotConfig.Ready():
case <-c.dead.Ready():
return nil
}
c.mux.Lock()
cfg := c.cfg
c.mux.Unlock()
return c.handler.OnSession(cfg, session)
}
func (c *Conn) trackInvoke() func() {
start := c.clock.Now()
c.mux.Lock()
defer c.mux.Unlock()
c.ongoing++
c.latest = start
return func() {
c.mux.Lock()
defer c.mux.Unlock()
c.ongoing--
end := c.clock.Now()
c.latest = end
c.log.Debug("Invoke",
zap.Duration("duration", end.Sub(start)),
zap.Int("ongoing", c.ongoing),
)
}
}
// Run initialize connection.
func (c *Conn) Run(ctx context.Context) (err error) {
defer c.dead.Signal()
defer func() {
if err != nil && ctx.Err() == nil {
c.log.Debug("Connection dead", zap.Error(err))
}
}()
return c.proto.Run(ctx, func(ctx context.Context) error {
// Signal death on init error. Otherwise connection shutdown
// deadlocks in OnSession that occurs before init fails.
err := c.init(ctx)
if err != nil {
c.dead.Signal()
}
return err
})
}
func (c *Conn) waitSession(ctx context.Context) error {
select {
case <-c.sessionInit.Ready():
return nil
case <-c.dead.Ready():
return pool.ErrConnDead
case <-ctx.Done():
return ctx.Err()
}
}
// Ready returns channel to determine connection readiness.
// Useful for pooling.
func (c *Conn) Ready() <-chan struct{} {
return c.sessionInit.Ready()
}
// Invoke implements Invoker.
func (c *Conn) Invoke(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
// Tracking ongoing invokes.
defer c.trackInvoke()()
if err := c.waitSession(ctx); err != nil {
return errors.Wrap(err, "waitSession")
}
return c.proto.Invoke(ctx, c.wrapRequest(noopDecoder{input}), output)
}
// OnMessage implements mtproto.Handler.
func (c *Conn) OnMessage(b *bin.Buffer) error {
return c.handler.OnMessage(b)
}
type noopDecoder struct {
bin.Encoder
}
func (n noopDecoder) Decode(b *bin.Buffer) error {
return errors.New("not implemented")
}
func (c *Conn) wrapRequest(req bin.Object) bin.Object {
if c.mode != ConnModeUpdates {
return &tg.InvokeWithoutUpdatesRequest{
Query: req,
}
}
return req
}
func (c *Conn) init(ctx context.Context) error {
c.log.Debug("Initializing")
q := c.wrapRequest(&tg.InitConnectionRequest{
APIID: c.appID,
DeviceModel: c.device.DeviceModel,
SystemVersion: c.device.SystemVersion,
AppVersion: c.device.AppVersion,
SystemLangCode: c.device.SystemLangCode,
LangPack: c.device.LangPack,
LangCode: c.device.LangCode,
Proxy: c.device.Proxy,
Params: c.device.Params,
Query: c.wrapRequest(&tg.HelpGetConfigRequest{}),
})
req := c.wrapRequest(&tg.InvokeWithLayerRequest{
Layer: tg.Layer,
Query: q,
})
var cfg tg.Config
if err := backoff.RetryNotify(func() error {
if err := c.proto.Invoke(ctx, req, &cfg); err != nil {
if tgerr.Is(err, tgerr.ErrFloodWait) {
// Server sometimes returns FLOOD_WAIT(0) if you create
// multiple connections in short period of time.
//
// See https://github.com/gotd/td/issues/388.
return errors.Wrap(err, "flood wait")
}
// Not retrying other errors.
return backoff.Permanent(errors.Wrap(err, "invoke"))
}
return nil
}, c.connBackoff(ctx), func(err error, duration time.Duration) {
c.log.Debug("Retrying connection initialization",
zap.Error(err), zap.Duration("duration", duration),
)
}); err != nil {
return errors.Wrap(err, "initConnection")
}
if c.setup != nil {
if err := c.setup(ctx, c); err != nil {
return errors.Wrap(err, "setup")
}
}
c.mux.Lock()
c.latest = c.clock.Now()
c.cfg = cfg
c.mux.Unlock()
c.gotConfig.Signal()
return nil
}
// Ping calls ping for underlying protocol connection.
func (c *Conn) Ping(ctx context.Context) error {
return c.proto.Ping(ctx)
}