code.vegaprotocol.io/vega@v0.79.0/datanode/candlesv2/candle_updates.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package candlesv2
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/datanode/entities"
    27  	"code.vegaprotocol.io/vega/logging"
    28  )
    29  
    30  var ErrNewSubscriberNotReady = errors.New("new subscriber was not ready to receive the last candle data")
    31  
    32  type candleSource interface {
    33  	GetCandleDataForTimeSpan(ctx context.Context, candleID string, from *time.Time, to *time.Time,
    34  		p entities.CursorPagination) ([]entities.Candle, entities.PageInfo, error)
    35  }
    36  
    37  type subscriptionMsg struct {
    38  	subscribe bool
    39  	id        string
    40  	out       chan entities.Candle
    41  }
    42  
    43  func (m subscriptionMsg) String() string {
    44  	if m.subscribe {
    45  		return fmt.Sprintf("unsubscribe, subscription id:%s", m.id)
    46  	}
    47  
    48  	return "subscribe"
    49  }
    50  
    51  type CandleUpdates struct {
    52  	log                *logging.Logger
    53  	candleSource       candleSource
    54  	candleID           string
    55  	subscriptionMsgCh  chan subscriptionMsg
    56  	nextSubscriptionID atomic.Uint64
    57  	config             CandleUpdatesConfig
    58  	subs               map[string]chan entities.Candle
    59  	mu                 *sync.RWMutex
    60  	lastCandle         *entities.Candle
    61  }
    62  
    63  func NewCandleUpdates(ctx context.Context, log *logging.Logger, candleID string, candleSource candleSource,
    64  	config CandleUpdatesConfig,
    65  ) *CandleUpdates {
    66  	ces := &CandleUpdates{
    67  		log:               log,
    68  		candleSource:      candleSource,
    69  		candleID:          candleID,
    70  		config:            config,
    71  		subscriptionMsgCh: make(chan subscriptionMsg, config.CandleUpdatesStreamSubscriptionMsgBufferSize),
    72  		subs:              map[string]chan entities.Candle{},
    73  		mu:                &sync.RWMutex{},
    74  	}
    75  
    76  	go ces.run(ctx)
    77  
    78  	return ces
    79  }
    80  
    81  func (s *CandleUpdates) run(ctx context.Context) {
    82  	defer s.closeAllSubscriptions()
    83  
    84  	ticker := time.NewTicker(s.config.CandleUpdatesStreamInterval.Duration)
    85  	defer ticker.Stop()
    86  
    87  	candleUpdatesFailed := false
    88  	updateCandles := func(now time.Time) *entities.Candle {
    89  		// no subscriptions, don't update candles and remove last candle.
    90  		if len(s.subs) == 0 {
    91  			return nil
    92  		}
    93  		candles, err := s.getCandleUpdates(ctx, now)
    94  		if err != nil {
    95  			if !candleUpdatesFailed {
    96  				s.log.Error("Failed to get candles for candle id", logging.String("candle", s.candleID), logging.Error(err))
    97  			}
    98  			candleUpdatesFailed = true
    99  			return s.lastCandle // keep last candle we successfully obtained
   100  		}
   101  		if candleUpdatesFailed {
   102  			s.log.Info("Successfully got candles for candle id", logging.String("candle", s.candleID))
   103  			candleUpdatesFailed = false
   104  		}
   105  		if len(candles) == 0 {
   106  			return s.lastCandle // no new data, just keep the reference to the last candle we had
   107  		}
   108  		// send the new data to all subscribers.
   109  		_ = s.sendCandlesToSubscribers(candles, s.subs)
   110  		// find the most recent, non zero candle as the last candle we know exists
   111  		for i := len(candles) - 1; i >= 0; i-- {
   112  			last := candles[i]
   113  			if !last.High.IsZero() && !last.Low.IsZero() {
   114  				return &last
   115  			}
   116  		}
   117  		// if no last candle was found, the last candle remains whatever s.lastCandle was
   118  		return s.lastCandle
   119  	}
   120  	for {
   121  		select {
   122  		case <-ctx.Done():
   123  			return
   124  		case subscriptionMsg := <-s.subscriptionMsgCh:
   125  			s.mu.Lock()
   126  			s.handleSubscription(subscriptionMsg)
   127  			s.mu.Unlock()
   128  		case now := <-ticker.C:
   129  			s.mu.RLock()
   130  			s.lastCandle = updateCandles(now)
   131  			s.mu.RUnlock()
   132  		}
   133  	}
   134  }
   135  
   136  func (s *CandleUpdates) handleSubscription(subscription subscriptionMsg) {
   137  	if subscription.subscribe {
   138  		s.addSubscription(subscription)
   139  		return
   140  	}
   141  	s.removeSubscription(subscription.id)
   142  }
   143  
   144  func (s *CandleUpdates) addSubscription(subscription subscriptionMsg) {
   145  	if s.lastCandle == nil {
   146  		s.subs[subscription.id] = subscription.out
   147  		return
   148  	}
   149  	newSub := map[string]chan entities.Candle{
   150  		subscription.id: subscription.out,
   151  	}
   152  	if rm := s.sendCandlesToSubscribers([]entities.Candle{*s.lastCandle}, newSub); len(rm) == 0 {
   153  		s.subs[subscription.id] = subscription.out
   154  	}
   155  }
   156  
   157  func (s *CandleUpdates) removeSubscription(id string) {
   158  	// no lock acquired, the map HAS to be locked when this function is called.
   159  	if ch, ok := s.subs[id]; ok {
   160  		close(ch)
   161  		delete(s.subs, id)
   162  	}
   163  }
   164  
   165  func (s *CandleUpdates) closeAllSubscriptions() {
   166  	s.mu.Lock()
   167  	s.lastCandle = nil
   168  	for _, subscriber := range s.subs {
   169  		close(subscriber)
   170  	}
   171  	s.mu.Unlock()
   172  }
   173  
   174  // Subscribe returns a unique subscription id and channel on which updates will be sent.
   175  func (s *CandleUpdates) Subscribe() (string, <-chan entities.Candle, error) {
   176  	out := make(chan entities.Candle, s.config.CandleUpdatesStreamBufferSize)
   177  
   178  	nextID := s.nextSubscriptionID.Add(1)
   179  	id := fmt.Sprintf("%s-%d", s.candleID, nextID)
   180  	var err error
   181  
   182  	if s.config.CandleUpdatesStreamSubscriptionMsgBufferSize == 0 {
   183  		// immediately add, acquire the lock and add to the map.
   184  		s.mu.Lock()
   185  		defer s.mu.Unlock()
   186  		s.subs[id] = out
   187  		// we have some data to send, then try this immediately
   188  		if s.lastCandle != nil {
   189  			newSub := map[string]chan entities.Candle{
   190  				id: out,
   191  			}
   192  			// try to send the last candle to the new subscriber, this will remove the last sub
   193  			// and close the channel if the send fails.
   194  			if rm := s.sendCandlesToSubscribers([]entities.Candle{*s.lastCandle}, newSub); len(rm) != 0 {
   195  				// if rm is not empty, the new subscriber was removed, and the channel was closed.
   196  				return "", nil, ErrNewSubscriberNotReady
   197  			}
   198  		}
   199  		return id, out, nil
   200  	}
   201  	msg := subscriptionMsg{
   202  		subscribe: true,
   203  		id:        id,
   204  		out:       out,
   205  	}
   206  
   207  	err = s.sendSubscriptionMessage(msg)
   208  	if err != nil {
   209  		return "", nil, err
   210  	}
   211  
   212  	return id, out, nil
   213  }
   214  
   215  func (s *CandleUpdates) Unsubscribe(id string) error {
   216  	if s.config.CandleUpdatesStreamSubscriptionMsgBufferSize == 0 {
   217  		// instantly unsubscribe, acquire the lock and remove from the map
   218  		s.mu.Lock()
   219  		if ch, ok := s.subs[id]; ok {
   220  			close(ch)
   221  			delete(s.subs, id)
   222  		}
   223  		s.mu.Unlock()
   224  		return nil
   225  	}
   226  	msg := subscriptionMsg{
   227  		subscribe: false,
   228  		id:        id,
   229  	}
   230  
   231  	return s.sendSubscriptionMessage(msg)
   232  }
   233  
   234  func (s *CandleUpdates) sendSubscriptionMessage(msg subscriptionMsg) error {
   235  	select {
   236  	case s.subscriptionMsgCh <- msg:
   237  		return nil
   238  	default:
   239  		return fmt.Errorf("failed to send subscription message \"%s\", subscription message buffer is full, try again later", msg)
   240  	}
   241  }
   242  
   243  func (s *CandleUpdates) getCandleUpdates(ctx context.Context, now time.Time) ([]entities.Candle, error) {
   244  	ctx, cancelFn := context.WithTimeout(ctx, s.config.CandlesFetchTimeout.Duration)
   245  	defer cancelFn()
   246  
   247  	var updates []entities.Candle
   248  	var err error
   249  	if s.lastCandle != nil {
   250  		start := s.lastCandle.PeriodStart
   251  		var candles []entities.Candle
   252  		candles, _, err = s.candleSource.GetCandleDataForTimeSpan(ctx, s.candleID, &start, &now, entities.CursorPagination{})
   253  
   254  		if err != nil {
   255  			return nil, fmt.Errorf("getting candle updates:%w", err)
   256  		}
   257  
   258  		// allocate slice rather than doubling cap as we go.
   259  		updates = make([]entities.Candle, 0, len(candles)+1)
   260  		for _, candle := range candles {
   261  			// last candle or newer should be considered an update.
   262  			if !candle.PeriodStart.Before(s.lastCandle.PeriodStart) {
   263  				updates = append(updates, candle)
   264  			}
   265  		}
   266  		return updates, nil
   267  	}
   268  	updates, _, err = s.candleSource.GetCandleDataForTimeSpan(ctx, s.candleID, nil, &now, entities.CursorPagination{})
   269  
   270  	if err != nil {
   271  		return nil, fmt.Errorf("getting candle updates:%w", err)
   272  	}
   273  
   274  	return updates, nil
   275  }
   276  
   277  func (s *CandleUpdates) sendCandlesToSubscribers(candles []entities.Candle, subscriptions map[string]chan entities.Candle) []string {
   278  	rm := make([]string, 0, len(subscriptions))
   279  	for id, outCh := range subscriptions {
   280  	loop:
   281  		for _, candle := range candles {
   282  			select {
   283  			case outCh <- candle:
   284  			default:
   285  				rm = append(rm, id)
   286  				s.removeSubscription(id)
   287  				break loop
   288  			}
   289  		}
   290  	}
   291  	return rm
   292  }