package eventlog import ( "time" ) // checkPrune checks whether the log has exceeded its boundaries of size or // age, and if so prunes the log and updates the head. func (lg *Log) checkPrune(head *logEntry, size int, age time.Duration) error { // To avoid potentially re-pruning for every event, don't trigger an age // prune until we're at least this far beyond the designated size. const windowSlop = 30 * time.Second if age < (lg.windowSize+windowSlop) && (lg.maxItems <= 0 || size <= lg.maxItems) { lg.numItemsGauge.Set(float64(lg.numItems)) return nil // no pruning is needed } var newState logState var err error switch { case lg.maxItems > 0 && size > lg.maxItems: // We exceeded the size cap. In this case, age does not matter: count off // the newest items and drop the unconsumed tail. Note that we prune by a // fraction rather than an absolute amount so that we only have to prune // for size occasionally. // TODO(creachadair): We may want to spill dropped events to secondary // storage rather than dropping them. The size cap is meant as a safety // valve against unexpected extremes, but if a network has "expected" // spikes that nevertheless exceed any safe buffer size (e.g., Osmosis // epochs), we may want to have a fallback so that we don't lose events // that would otherwise fall within the window. newSize := 3 * size / 4 newState, err = lg.pruneSize(head, newSize) default: // We did not exceed the size cap, but some items are too old. newState = lg.pruneAge(head) } // Note that when we update the head after pruning, we do not need to signal // any waiters; pruning never adds new material to the log so anyone waiting // should continue doing so until a subsequent Add occurs. lg.mu.Lock() defer lg.mu.Unlock() lg.numItems = newState.size lg.numItemsGauge.Set(float64(newState.size)) lg.oldestCursor = newState.oldest lg.head = newState.head return err } // pruneSize returns a new log state by pruning head to newSize. // Precondition: newSize ≤ len(head). func (lg *Log) pruneSize(head *logEntry, newSize int) (logState, error) { // Special case for size 0 to simplify the logic below. if newSize == 0 { return logState{}, ErrLogPruned // drop everything } // Initialize: New head has the same item as the old head. first := &logEntry{item: head.item} // new head last := first // new tail (last copied cons) cur := head.next for i := 1; i < newSize; i++ { cp := &logEntry{item: cur.item} last.next = cp last = cp cur = cur.next } var err error if head.item.Cursor.Diff(last.item.Cursor) <= lg.windowSize { err = ErrLogPruned } return logState{ oldest: last.item.Cursor, newest: first.item.Cursor, size: newSize, head: first, }, err } // pruneAge returns a new log state by pruning items older than the window // prior to the head element. func (lg *Log) pruneAge(head *logEntry) logState { first := &logEntry{item: head.item} last := first size := 1 for cur := head.next; cur != nil; cur = cur.next { diff := head.item.Cursor.Diff(cur.item.Cursor) if diff > lg.windowSize { break // all remaining items are older than the window } cp := &logEntry{item: cur.item} last.next = cp last = cp size++ } return logState{ oldest: last.item.Cursor, newest: first.item.Cursor, size: size, head: first, } }