func (b *ETCDBindingStorage) Init() error {
	var cli *clientv3.Client
	var err error
	if b.cli == nil {
		cli, err = clientv3.New(clientv3.Config{
			Endpoints:   b.etcdEndpoints,
			DialTimeout: b.etcdDialTimeout,
		})
		if err != nil {
			return err
		}
		b.cli = cli
	}
	b.cli.KV = namespace.NewKV(b.cli.KV, b.etcdPrefix)
	err = b.bootstrapLease()
	if err != nil {
		return err
	}
	if b.thisServer.Frontend {
		b.setupOnSessionCloseCB()
		b.setupOnAfterSessionBindCB()
	}
	return nil
}
Init starts the binding storage module
package modules
import (
	"context"
	"fmt"
	"time"
	clientv3 "go.etcd.io/etcd/client/v3"
	"go.etcd.io/etcd/client/v3/namespace"
	"github.com/topfreegames/pitaya/v2/cluster"
	"github.com/topfreegames/pitaya/v2/config"
	"github.com/topfreegames/pitaya/v2/constants"
	"github.com/topfreegames/pitaya/v2/logger"
	"github.com/topfreegames/pitaya/v2/session"
)
type ETCDBindingStorage struct {
	Base
	cli             *clientv3.Client
	etcdEndpoints   []string
	etcdPrefix      string
	etcdDialTimeout time.Duration
	leaseTTL        time.Duration
	leaseID         clientv3.LeaseID
	thisServer      *cluster.Server
	sessionPool     session.SessionPool
	stopChan        chan struct{}
}
func NewETCDBindingStorage(server *cluster.Server, sessionPool session.SessionPool, conf config.ETCDBindingConfig) *ETCDBindingStorage {
	b := &ETCDBindingStorage{
		thisServer:  server,
		sessionPool: sessionPool,
		stopChan:    make(chan struct{}),
	}
	b.etcdDialTimeout = conf.DialTimeout
	b.etcdEndpoints = conf.Endpoints
	b.etcdPrefix = conf.Prefix
	b.leaseTTL = conf.LeaseTTL
	return b
}
func getUserBindingKey(uid, frontendType string) string {
	return fmt.Sprintf("bindings/%s/%s", frontendType, uid)
}
func (b *ETCDBindingStorage) PutBinding(uid string) error {
	_, err := b.cli.Put(context.Background(), getUserBindingKey(uid, b.thisServer.Type), b.thisServer.ID, clientv3.WithLease(b.leaseID))
	return err
}
func (b *ETCDBindingStorage) removeBinding(uid string) error {
	_, err := b.cli.Delete(context.Background(), getUserBindingKey(uid, b.thisServer.Type))
	return err
}
func (b *ETCDBindingStorage) GetUserFrontendID(uid, frontendType string) (string, error) {
	etcdRes, err := b.cli.Get(context.Background(), getUserBindingKey(uid, frontendType))
	if err != nil {
		return "", err
	}
	if len(etcdRes.Kvs) == 0 {
		return "", constants.ErrBindingNotFound
	}
	return string(etcdRes.Kvs[0].Value), nil
}
func (b *ETCDBindingStorage) setupOnSessionCloseCB() {
	b.sessionPool.OnSessionClose(func(s session.Session) {
		if s.UID() != "" {
			err := b.removeBinding(s.UID())
			if err != nil {
				logger.Log.Errorf("error removing binding info from storage: %v", err)
			}
		}
	})
}
func (b *ETCDBindingStorage) setupOnAfterSessionBindCB() {
	b.sessionPool.OnAfterSessionBind(func(ctx context.Context, s session.Session) error {
		return b.PutBinding(s.UID())
	})
}
func (b *ETCDBindingStorage) watchLeaseChan(c <-chan *clientv3.LeaseKeepAliveResponse) {
	for {
		select {
		case <-b.stopChan:
			return
		case kaRes := <-c:
			if kaRes == nil {
				logger.Log.Warn("[binding storage] sd: error renewing etcd lease, rebootstrapping")
				for {
					err := b.bootstrapLease()
					if err != nil {
						logger.Log.Warn("[binding storage] sd: error rebootstrapping lease, will retry in 5 seconds")
						time.Sleep(5 * time.Second)
						continue
					} else {
						return
					}
				}
			}
		}
	}
}
func (b *ETCDBindingStorage) bootstrapLease() error {
	l, err := b.cli.Grant(context.TODO(), int64(b.leaseTTL.Seconds()))
	if err != nil {
		return err
	}
	b.leaseID = l.ID
	logger.Log.Debugf("[binding storage] sd: got leaseID: %x", l.ID)
	c, err := b.cli.KeepAlive(context.TODO(), b.leaseID)
	if err != nil {
		return err
	}
	<-c
	go b.watchLeaseChan(c)
	return nil
}