github.com/mavryk-network/mvgo@v1.19.9/rpc/observer.go (about) 1 // Copyright (c) 2020-2023 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 4 package rpc 5 6 import ( 7 "context" 8 "sync" 9 "time" 10 11 "github.com/mavryk-network/mvgo/mavryk" 12 ) 13 14 // WIP: interface may change 15 // 16 // TODO: 17 // - support AdressObserver with address subscription filter 18 // - disable events/polling when no subscriber exists 19 // - handle reorgs (inclusion may switch to a different block hash) 20 21 type ObserverCallback func(*BlockHeaderLogEntry, int64, int, int, bool) bool 22 23 type observerSubscription struct { 24 id int 25 cb ObserverCallback 26 oh mavryk.OpHash 27 matched bool 28 } 29 30 type Observer struct { 31 subs map[int]*observerSubscription 32 watched map[mavryk.OpHash][]int 33 recent map[mavryk.OpHash][3]int64 34 seq int 35 once sync.Once 36 mu sync.Mutex 37 ctx context.Context 38 cancel context.CancelFunc 39 c *Client 40 minDelay time.Duration 41 head *BlockHeaderLogEntry 42 } 43 44 func NewObserver() *Observer { 45 ctx, cancel := context.WithCancel(context.Background()) 46 m := &Observer{ 47 subs: make(map[int]*observerSubscription), 48 watched: make(map[mavryk.OpHash][]int), 49 recent: make(map[mavryk.OpHash][3]int64), 50 minDelay: mavryk.DefaultParams.MinimalBlockDelay, 51 ctx: ctx, 52 cancel: cancel, 53 head: &BlockHeaderLogEntry{ 54 Level: -1, 55 }, 56 } 57 return m 58 } 59 60 func (m *Observer) Head() *BlockHeaderLogEntry { 61 return m.head 62 } 63 64 func (m *Observer) WithDelay(minDelay time.Duration) *Observer { 65 m.minDelay = minDelay 66 return m 67 } 68 69 func (m *Observer) Close() { 70 m.mu.Lock() 71 defer m.mu.Unlock() 72 m.cancel() 73 m.subs = make(map[int]*observerSubscription) 74 m.watched = make(map[mavryk.OpHash][]int) 75 m.recent = make(map[mavryk.OpHash][3]int64) 76 } 77 78 func (m *Observer) Subscribe(oh mavryk.OpHash, cb ObserverCallback) int { 79 m.mu.Lock() 80 defer m.mu.Unlock() 81 m.seq++ 82 seq := m.seq 83 m.subs[seq] = &observerSubscription{ 84 id: seq, 85 cb: cb, 86 oh: oh, 87 } 88 if pos, ok := m.recent[oh]; ok { 89 match := m.subs[seq] 90 m.c.Log.Debugf("monitor: %03d direct match %s", seq, oh) 91 if remove := match.cb(m.head, pos[0], int(pos[1]), int(pos[2]), false); remove { 92 delete(m.subs, match.id) 93 } 94 } 95 m.c.Log.Debugf("monitor: %03d subscribed %s", seq, oh) 96 m.watched[oh] = append(m.watched[oh], seq) 97 return seq 98 } 99 100 func (m *Observer) Unsubscribe(id int) { 101 m.mu.Lock() 102 defer m.mu.Unlock() 103 req, ok := m.subs[id] 104 if ok { 105 m.removeWatcher(req.oh, id) 106 delete(m.subs, id) 107 m.c.Log.Debugf("monitor: %03d unsubscribed %s", id, req.oh) 108 } 109 } 110 111 func (m *Observer) removeWatcher(oh mavryk.OpHash, id int) { 112 n := 0 113 for _, v := range m.watched[oh] { 114 if v != id { 115 m.watched[oh][n] = v 116 n++ 117 } 118 } 119 if n == 0 { 120 delete(m.watched, oh) 121 } else { 122 m.watched[oh] = m.watched[oh][:n] 123 } 124 } 125 126 func (m *Observer) Listen(cli *Client) { 127 m.once.Do(func() { 128 m.c = cli 129 if m.c.Params != nil { 130 m.minDelay = m.c.Params.MinimalBlockDelay 131 } 132 go m.listenBlocks() 133 }) 134 } 135 136 func (m *Observer) ListenMempool(cli *Client) { 137 m.once.Do(func() { 138 m.c = cli 139 if m.c.Params != nil { 140 m.minDelay = m.c.Params.MinimalBlockDelay 141 } 142 go m.listenMempool() 143 }) 144 } 145 146 func (m *Observer) listenMempool() { 147 // TODO 148 } 149 150 func (m *Observer) listenBlocks() { 151 var ( 152 mon *BlockHeaderMonitor 153 useEvents bool = true 154 firstLoop bool = true 155 ) 156 defer func() { 157 if mon != nil { 158 mon.Close() 159 } 160 }() 161 162 for { 163 // handle close request 164 select { 165 case <-m.ctx.Done(): 166 return 167 default: 168 } 169 170 // (re)connect 171 if mon == nil && useEvents { 172 mon = NewBlockHeaderMonitor() 173 if err := m.c.MonitorBlockHeader(m.ctx, mon); err != nil { 174 mon.Close() 175 mon = nil 176 if ErrorStatus(err) == 404 { 177 m.c.Log.Debug("monitor: event mode unsupported, falling back to poll mode.") 178 useEvents = false 179 } else { 180 // wait 5 sec, but also return on close 181 select { 182 case <-m.ctx.Done(): 183 return 184 case <-time.After(5 * time.Second): 185 } 186 } 187 continue 188 } 189 } 190 191 var head *BlockHeaderLogEntry 192 if mon != nil && useEvents && !firstLoop { 193 // event mode: wait for next block message 194 var err error 195 head, err = mon.Recv(m.ctx) 196 // reconnect on error unless context was cancelled 197 if err != nil { 198 mon.Close() 199 mon = nil 200 continue 201 } 202 // m.c.Log.Debugf("monitor: new head %s", head.Hash) 203 } else { 204 // poll mode: check every n sec 205 h, err := m.c.GetTipHeader(m.ctx) 206 if err != nil { 207 // wait 5 sec, but also return on close 208 select { 209 case <-m.ctx.Done(): 210 return 211 case <-time.After(5 * time.Second): 212 } 213 continue 214 } 215 head = h.LogEntry() 216 } 217 firstLoop = false 218 219 // skip already processed blocks 220 if head.Hash.Equal(m.head.Hash) && !useEvents { 221 // wait minDelay/2 for late blocks 222 if !useEvents { 223 select { 224 case <-m.ctx.Done(): 225 return 226 case <-time.After(m.minDelay / 2): 227 } 228 } 229 continue 230 } 231 m.c.Log.Debugf("monitor: new block %d %s", head.Level, head.Hash) 232 233 // TODO: check for reorg and gaps 234 235 // handle block watchers 236 m.mu.Lock() 237 for _, id := range m.watched[mavryk.ZeroOpHash] { 238 sub, ok := m.subs[id] 239 if !ok { 240 m.removeWatcher(mavryk.ZeroOpHash, id) 241 continue 242 } 243 if remove := sub.cb(head, head.Level, -1, -1, false); remove { 244 delete(m.subs, id) 245 m.removeWatcher(mavryk.ZeroOpHash, id) 246 } 247 } 248 249 // callback for all previous matches who have not yet unregistered (i.e. waiting for 250 // additional confirmations) 251 for _, v := range m.subs { 252 if v.matched { 253 m.c.Log.Debugf("monitor: signal n-th match for %d %s", v.id, v.oh) 254 if remove := v.cb(head, head.Level, -1, -1, false); remove { 255 delete(m.subs, v.id) 256 m.removeWatcher(v.oh, v.id) 257 } 258 } 259 } 260 // clear recent op hashes 261 for n := range m.recent { 262 delete(m.recent, n) 263 } 264 265 numSubs := len(m.subs) 266 m.mu.Unlock() 267 268 // pull block ops when subs exist 269 var ( 270 ohs [][]mavryk.OpHash 271 err error 272 ) 273 if numSubs > 0 { 274 ohs, err = m.c.GetBlockOperationHashes(m.ctx, head.Hash) 275 if err != nil { 276 m.c.Log.Warnf("monitor: cannot fetch block ops: %v", err) 277 continue 278 } 279 } 280 281 // fan-out matches 282 m.mu.Lock() 283 for l, list := range ohs { 284 for n, h := range list { 285 // keep as recent 286 m.recent[h] = [3]int64{head.Level, int64(l), int64(n)} 287 288 // match op hash against subs 289 ids, ok := m.watched[h] 290 if !ok { 291 m.c.Log.Debugf("monitor: --- !! %s", h) 292 continue 293 } 294 295 // handle all subscriptions for this op hash 296 var removed []*observerSubscription 297 for _, id := range ids { 298 sub, ok := m.subs[id] 299 if !ok { 300 m.c.Log.Debugf("monitor: --- !! %s", h) 301 continue 302 } 303 304 m.c.Log.Debugf("monitor: matched %d %s", sub.id, sub.oh) 305 306 // callback 307 if remove := sub.cb(head, head.Level, l, n, false); remove { 308 delete(m.subs, sub.id) 309 removed = append(removed, sub) 310 } else { 311 sub.matched = true 312 } 313 } 314 315 // remove deleted subs from watch list 316 for _, sub := range removed { 317 m.removeWatcher(sub.oh, sub.id) 318 } 319 } 320 } 321 322 // update monitor state 323 m.head = head 324 m.mu.Unlock() 325 326 // wait in poll mode 327 if !useEvents { 328 select { 329 case <-m.ctx.Done(): 330 return 331 case <-time.After(m.minDelay): 332 } 333 } 334 } 335 }