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  }