// Package eventlog defines a reverse time-ordered log of events over a sliding // window of time before the most recent item in the log. // // New items are added to the head of the log (the newest end), and items that // fall outside the designated window are pruned from its tail (the oldest). // Items within the log are indexed by lexicographically-ordered cursors. package eventlog import ( "context" "errors" "sync" "time" "github.com/tendermint/tendermint/internal/eventlog/cursor" "github.com/tendermint/tendermint/types" ) // A Log is a reverse time-ordered log of events in a sliding window of time // before the newest item. Use Add to add new items to the front (head) of the // log, and Scan or WaitScan to traverse the current contents of the log. // // After construction, a *Log is safe for concurrent access by one writer and // any number of readers. type Log struct { // These values do not change after construction. windowSize time.Duration maxItems int numItemsGauge gauge // Protects access to the fields below. Lock to modify the values of these // fields, or to read or snapshot the values. mu sync.Mutex numItems int // total number of items in the log oldestCursor cursor.Cursor // cursor of the oldest item head *logEntry // pointer to the newest item ready chan struct{} // closed when head changes source cursor.Source // generator of cursors } // New constructs a new empty log with the given settings. func New(opts LogSettings) (*Log, error) { if opts.WindowSize <= 0 { return nil, errors.New("window size must be positive") } lg := &Log{ windowSize: opts.WindowSize, maxItems: opts.MaxItems, numItemsGauge: discard{}, ready: make(chan struct{}), source: opts.Source, } if opts.Metrics != nil { lg.numItemsGauge = opts.Metrics.numItemsGauge } return lg, nil } // Add adds a new item to the front of the log. If necessary, the log is pruned // to fit its constraints on size and age. Add blocks until both steps are done. // // Any error reported by Add arises from pruning; the new item was added to the // log regardless whether an error occurs. func (lg *Log) Add(etype string, data types.EventData) error { lg.mu.Lock() head := &logEntry{ item: newItem(lg.source.Cursor(), etype, data), next: lg.head, } lg.numItems++ lg.updateHead(head) size := lg.numItems age := head.item.Cursor.Diff(lg.oldestCursor) // If the log requires pruning, do the pruning step outside the lock. This // permits readers to continue to make progress while we're working. lg.mu.Unlock() return lg.checkPrune(head, size, age) } // Scan scans the current contents of the log, calling f with each item until // all items are visited or f reports an error. If f returns ErrStopScan, Scan // returns nil, otherwise it returns the error reported by f. // // The Info value returned is valid even if Scan reports an error. func (lg *Log) Scan(f func(*Item) error) (Info, error) { return lg.scanState(lg.state(), f) } // WaitScan blocks until the cursor of the frontmost log item is different from // c, then executes a Scan on the contents of the log. If ctx ends before the // head is updated, WaitScan returns an error without calling f. // // The Info value returned is valid even if WaitScan reports an error. func (lg *Log) WaitScan(ctx context.Context, c cursor.Cursor, f func(*Item) error) (Info, error) { st := lg.state() for st.head == nil || st.head.item.Cursor == c { var err error st, err = lg.waitStateChange(ctx) if err != nil { return st.info(), err } } return lg.scanState(st, f) } // Info returns the current state of the log. func (lg *Log) Info() Info { return lg.state().info() } // ErrStopScan is returned by a Scan callback to signal that scanning should be // terminated without error. var ErrStopScan = errors.New("stop scanning") // ErrLogPruned is returned by Add to signal that at least some events within // the time window were discarded by pruning in excess of the size limit. // This error may be wrapped, use errors.Is to test for it. var ErrLogPruned = errors.New("log pruned") // LogSettings configure the construction of an event log. type LogSettings struct { // The size of the time window measured in time before the newest item. // This value must be positive. WindowSize time.Duration // The maximum number of items that will be retained in memory within the // designated time window. A value ≤ 0 imposes no limit, otherwise items in // excess of this number will be dropped from the log. MaxItems int // The cursor source to use for log entries. If not set, use wallclock time. Source cursor.Source // If non-nil, exported metrics to update. If nil, metrics are discarded. Metrics *Metrics } // Info records the current state of the log at the time of a scan operation. type Info struct { Oldest cursor.Cursor // the cursor of the oldest item in the log Newest cursor.Cursor // the cursor of the newest item in the log Size int // the number of items in the log } // logState is a snapshot of the state of the log. type logState struct { oldest cursor.Cursor newest cursor.Cursor size int head *logEntry } func (st logState) info() Info { return Info{Oldest: st.oldest, Newest: st.newest, Size: st.size} } // state returns a snapshot of the current log contents. The caller may freely // traverse the internal structure of the list without locking, provided it // does not modify either the entries or their items. func (lg *Log) state() logState { lg.mu.Lock() defer lg.mu.Unlock() if lg.head == nil { return logState{} // empty } return logState{ oldest: lg.oldestCursor, newest: lg.head.item.Cursor, size: lg.numItems, head: lg.head, } } // waitStateChange blocks until either ctx ends or the head of the log is // modified, then returns the state of the log. An error is reported only if // ctx terminates before head changes. func (lg *Log) waitStateChange(ctx context.Context) (logState, error) { lg.mu.Lock() ch := lg.ready // capture lg.mu.Unlock() select { case <-ctx.Done(): return lg.state(), ctx.Err() case <-ch: return lg.state(), nil } } // scanState scans the contents of the log at st. See the Scan method for a // description of the callback semantics. func (lg *Log) scanState(st logState, f func(*Item) error) (Info, error) { info := Info{Oldest: st.oldest, Newest: st.newest, Size: st.size} for cur := st.head; cur != nil; cur = cur.next { if err := f(cur.item); err != nil { if errors.Is(err, ErrStopScan) { return info, nil } return info, err } } return info, nil } // updateHead replaces the current head with newHead, signals any waiters, and // resets the wait signal. The caller must hold log.mu exclusively. func (lg *Log) updateHead(newHead *logEntry) { lg.head = newHead close(lg.ready) // signal lg.ready = make(chan struct{}) } // A logEntry is the backbone of the event log queue. Entries are not mutated // after construction, so it is safe to read item and next without locking. type logEntry struct { item *Item next *logEntry }