|
// 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
|
|
}
|