github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/pkg/mux/mux.go (about)

     1  package mux
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/event"
    11  	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/client"
    12  	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/msg"
    13  )
    14  
    15  // Mux will manage all connections and subscriptions. Will check if subscriptions
    16  // limit is reached and spawn new connection when that happens. It will also listen
    17  // to all incomming client messages and reconnect client with all its subscriptions
    18  // in case of a failure
    19  type Mux struct {
    20  	cid                int
    21  	dms                int
    22  	publicChan         chan msg.Msg
    23  	publicClients      map[int]*client.Client
    24  	privateChan        chan msg.Msg
    25  	closeChan          chan bool
    26  	privateClient      *client.Client
    27  	mtx                *sync.RWMutex
    28  	Err                error
    29  	transform          bool
    30  	apikey             string
    31  	apisec             string
    32  	subInfo            map[int64]event.Info
    33  	authenticated      bool
    34  	publicURL          string
    35  	authURL            string
    36  	online             bool
    37  	rateLimitQueueSize int
    38  }
    39  
    40  // api rate limit is 20 calls per minute. 1x3s, 20x1min
    41  const (
    42  	rateLimitDuration     = 3 * time.Second
    43  	maxRateLimitQueueSize = 20
    44  )
    45  
    46  // New returns pointer to instance of mux
    47  func New() *Mux {
    48  	return &Mux{
    49  		publicChan:    make(chan msg.Msg),
    50  		privateChan:   make(chan msg.Msg),
    51  		closeChan:     make(chan bool),
    52  		publicClients: make(map[int]*client.Client),
    53  		mtx:           &sync.RWMutex{},
    54  		subInfo:       map[int64]event.Info{},
    55  		publicURL:     "wss://api-pub.bitfinex.com/ws/2",
    56  		authURL:       "wss://api.bitfinex.com/ws/2",
    57  	}
    58  }
    59  
    60  // TransformRaw enables data transformation and mapping to appropriate
    61  // models before sending it to consumer
    62  func (m *Mux) TransformRaw() *Mux {
    63  	m.transform = true
    64  	return m
    65  }
    66  
    67  // WithAPIKEY accepts and persists api key
    68  func (m *Mux) WithAPIKEY(key string) *Mux {
    69  	m.apikey = key
    70  	return m
    71  }
    72  
    73  // WithDeadManSwitch - when socket is closed, cancel all account orders
    74  func (m *Mux) WithDeadManSwitch() *Mux {
    75  	m.dms = 4
    76  	return m
    77  }
    78  
    79  // WithAPISEC accepts and persists api secret
    80  func (m *Mux) WithAPISEC(sec string) *Mux {
    81  	m.apisec = sec
    82  	return m
    83  }
    84  
    85  // WithPublicURL accepts and persists public api url
    86  func (m *Mux) WithPublicURL(url string) *Mux {
    87  	m.publicURL = url
    88  	return m
    89  }
    90  
    91  // WithAuthURL accepts and persists auth api url
    92  func (m *Mux) WithAuthURL(url string) *Mux {
    93  	m.authURL = url
    94  	return m
    95  }
    96  
    97  func (m *Mux) IsConnected() bool {
    98  	return m.online
    99  }
   100  
   101  func (m *Mux) Close() bool {
   102  	m.closeChan <- true
   103  	return true
   104  }
   105  
   106  // Subscribe - given the details in form of event.Subscribe, subscribes client to public
   107  // channels. If rate limit is reached, calls itself recursively after 1s with same params
   108  func (m *Mux) Subscribe(sub event.Subscribe) *Mux {
   109  	if m.Err != nil {
   110  		return m
   111  	}
   112  
   113  	// if limit is reached, wait 1 second and recuresively
   114  	// call Subscribe again with same subscription details
   115  	if m.rateLimitQueueSize == maxRateLimitQueueSize {
   116  		time.Sleep(1 * time.Second)
   117  		return m.Subscribe(sub)
   118  	}
   119  
   120  	m.mtx.RLock()
   121  	defer m.mtx.RUnlock()
   122  	if m.publicClients[m.cid].SubAdded(sub) {
   123  		return m
   124  	}
   125  
   126  	if m.Err = m.publicClients[m.cid].Subscribe(sub); m.Err != nil {
   127  		return m
   128  	}
   129  
   130  	if limitReached := m.publicClients[m.cid].SubsLimitReached(); limitReached {
   131  		log.Printf("subs limit is reached on cid: %d, spawning new conn\n", m.cid)
   132  		m.addPublicClient()
   133  	}
   134  
   135  	m.rateLimitQueueSize++
   136  	return m
   137  }
   138  
   139  // Start creates initial clients for accepting connections
   140  func (m *Mux) Start() *Mux {
   141  	if m.hasAPIKeys() && m.privateClient == nil {
   142  		m.addPrivateClient()
   143  	}
   144  
   145  	m.watchRateLimit()
   146  	m.addPublicClient()
   147  	return m
   148  }
   149  
   150  // Listen accepts a callback func that will get called each time mux
   151  // receives a message from any of its clients/subscriptions. It
   152  // should be called last, after all setup calls are made
   153  func (m *Mux) Listen(cb func(interface{}, error)) error {
   154  	if m.Err != nil {
   155  		return m.Err
   156  	}
   157  
   158  	m.online = true
   159  	for {
   160  		select {
   161  		case ms, ok := <-m.publicChan:
   162  			if !ok {
   163  				return errors.New("public channel has closed unexpectedly")
   164  			}
   165  			if ms.Err != nil {
   166  				cb(nil, fmt.Errorf("conn:%d has failed | err:%s | reconnecting", ms.CID, ms.Err))
   167  				m.resetPublicClient(ms.CID)
   168  				continue
   169  			}
   170  			// return raw payload data if transform is off
   171  			if !m.transform {
   172  				cb(ms.Data, nil)
   173  				continue
   174  			}
   175  			// handle event type message
   176  			if ms.IsEvent() {
   177  				cb(m.recordEvent(ms.ProcessEvent()))
   178  				continue
   179  			}
   180  			// handle data type message
   181  			if ms.IsRaw() {
   182  				raw, pld, chID, _, err := ms.PreprocessRaw()
   183  				if err != nil {
   184  					cb(nil, err)
   185  					continue
   186  				}
   187  
   188  				inf, ok := m.subInfo[chID]
   189  				if !ok {
   190  					cb(nil, fmt.Errorf("unrecognized chanId:%d", chID))
   191  					continue
   192  				}
   193  				cb(ms.ProcessPublic(raw, pld, chID, inf))
   194  				continue
   195  			}
   196  			cb(nil, fmt.Errorf("unrecognized msg signature: %s", ms.Data))
   197  		case ms, ok := <-m.privateChan:
   198  			if !ok {
   199  				return errors.New("private channel has closed unexpectedly")
   200  			}
   201  			if ms.Err != nil {
   202  				cb(nil, fmt.Errorf("err: %s | reconnecting", ms.Err))
   203  				m.resetPrivateClient()
   204  				continue
   205  			}
   206  			// return raw payload data if transform is off
   207  			if !m.transform {
   208  				cb(ms.Data, nil)
   209  				continue
   210  			}
   211  			// handle event type message
   212  			if ms.IsEvent() {
   213  				cb(m.recordEvent(ms.ProcessEvent()))
   214  				continue
   215  			}
   216  			// handle data type message
   217  			if ms.IsRaw() {
   218  				raw, pld, chID, msgType, err := ms.PreprocessRaw()
   219  				if err != nil {
   220  					cb(nil, err)
   221  					continue
   222  				}
   223  				cb(ms.ProcessPrivate(raw, pld, chID, msgType))
   224  				continue
   225  			}
   226  			cb(nil, fmt.Errorf("unrecognized msg signature: %s", ms.Data))
   227  		case <-m.closeChan:
   228  			m.mtx.Lock()
   229  			defer m.mtx.Unlock()
   230  
   231  			for _, v := range m.publicClients {
   232  				if v == nil {
   233  					continue
   234  				}
   235  				if err := v.Close(); err != nil {
   236  					log.Printf("failed closing public client: %s\n", err)
   237  				}
   238  			}
   239  
   240  			if m.privateClient != nil {
   241  				if err := m.privateClient.Close(); err != nil {
   242  					log.Printf("failed closing private client: %s\n", err)
   243  				}
   244  			}
   245  
   246  			m.online = false
   247  			return nil
   248  		}
   249  	}
   250  }
   251  
   252  // Send meant for authenticated input, takes payload in form of interface
   253  // and calls client with it
   254  func (m *Mux) Send(pld interface{}) error {
   255  	if !m.authenticated || m.privateClient == nil {
   256  		return errors.New("not authorized")
   257  	}
   258  	return m.privateClient.Send(pld)
   259  }
   260  
   261  func (m *Mux) hasAPIKeys() bool {
   262  	return len(m.apikey) != 0 && len(m.apisec) != 0
   263  }
   264  
   265  func (m *Mux) recordEvent(i event.Info, err error) (event.Info, error) {
   266  	switch i.Event {
   267  	case "subscribed":
   268  		m.subInfo[i.ChanID] = i
   269  	case "auth":
   270  		if i.Status == "OK" {
   271  			m.subInfo[i.ChanID] = i
   272  			m.authenticated = true
   273  		}
   274  	}
   275  	// add more cases if/when needed
   276  	return i, err
   277  }
   278  
   279  func (m *Mux) resetPublicClient(cid int) {
   280  	// pull old client subscriptions
   281  	subs := m.publicClients[cid].GetAllSubs()
   282  	// add fresh client
   283  	m.addPublicClient()
   284  	// resubscribe old events
   285  	for _, sub := range subs {
   286  		log.Printf("resubscribing: %+v\n", sub)
   287  		m.Subscribe(sub)
   288  	}
   289  	// remove old, closed channel from the list
   290  	delete(m.publicClients, cid)
   291  }
   292  
   293  func (m *Mux) resetPrivateClient() {
   294  	m.authenticated = false
   295  	m.privateClient = nil
   296  	m.addPrivateClient()
   297  }
   298  
   299  func (m *Mux) addPublicClient() *Mux {
   300  	// adding new client so making sure we increment cid
   301  	m.cid++
   302  	// create new public client and pass error to mux if any
   303  	c, err := client.
   304  		New().
   305  		WithID(m.cid).
   306  		WithSubsLimit(30).
   307  		Public(m.publicURL)
   308  	if err != nil {
   309  		m.Err = err
   310  		return m
   311  	}
   312  	// add new client to list for later reference
   313  	m.publicClients[m.cid] = c
   314  	// start listening for incoming client messages
   315  	go c.Read(m.publicChan)
   316  	return m
   317  }
   318  
   319  func (m *Mux) addPrivateClient() *Mux {
   320  	// create new private client and pass error to mux if any
   321  	c, err := client.New().Private(m.apikey, m.apisec, m.authURL, m.dms)
   322  	if err != nil {
   323  		m.Err = err
   324  		return m
   325  	}
   326  
   327  	m.privateClient = c
   328  	go c.Read(m.privateChan)
   329  	return m
   330  }
   331  
   332  // watchRateLimit will run once every rateLimitDuration
   333  // and free up the queue
   334  func (m *Mux) watchRateLimit() {
   335  	go func() {
   336  		for {
   337  			if m.rateLimitQueueSize > 0 {
   338  				m.rateLimitQueueSize--
   339  			}
   340  
   341  			time.Sleep(rateLimitDuration)
   342  		}
   343  	}()
   344  }