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 }