You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
3.3 KiB

  1. package eventlog
  2. import (
  3. "time"
  4. )
  5. // checkPrune checks whether the log has exceeded its boundaries of size or
  6. // age, and if so prunes the log and updates the head.
  7. func (lg *Log) checkPrune(head *logEntry, size int, age time.Duration) error {
  8. // To avoid potentially re-pruning for every event, don't trigger an age
  9. // prune until we're at least this far beyond the designated size.
  10. const windowSlop = 30 * time.Second
  11. if age < (lg.windowSize+windowSlop) && (lg.maxItems <= 0 || size <= lg.maxItems) {
  12. lg.numItemsGauge.Set(float64(lg.numItems))
  13. return nil // no pruning is needed
  14. }
  15. var newState logState
  16. var err error
  17. switch {
  18. case lg.maxItems > 0 && size > lg.maxItems:
  19. // We exceeded the size cap. In this case, age does not matter: count off
  20. // the newest items and drop the unconsumed tail. Note that we prune by a
  21. // fraction rather than an absolute amount so that we only have to prune
  22. // for size occasionally.
  23. // TODO(creachadair): We may want to spill dropped events to secondary
  24. // storage rather than dropping them. The size cap is meant as a safety
  25. // valve against unexpected extremes, but if a network has "expected"
  26. // spikes that nevertheless exceed any safe buffer size (e.g., Osmosis
  27. // epochs), we may want to have a fallback so that we don't lose events
  28. // that would otherwise fall within the window.
  29. newSize := 3 * size / 4
  30. newState, err = lg.pruneSize(head, newSize)
  31. default:
  32. // We did not exceed the size cap, but some items are too old.
  33. newState = lg.pruneAge(head)
  34. }
  35. // Note that when we update the head after pruning, we do not need to signal
  36. // any waiters; pruning never adds new material to the log so anyone waiting
  37. // should continue doing so until a subsequent Add occurs.
  38. lg.mu.Lock()
  39. defer lg.mu.Unlock()
  40. lg.numItems = newState.size
  41. lg.numItemsGauge.Set(float64(newState.size))
  42. lg.oldestCursor = newState.oldest
  43. lg.head = newState.head
  44. return err
  45. }
  46. // pruneSize returns a new log state by pruning head to newSize.
  47. // Precondition: newSize ≤ len(head).
  48. func (lg *Log) pruneSize(head *logEntry, newSize int) (logState, error) {
  49. // Special case for size 0 to simplify the logic below.
  50. if newSize == 0 {
  51. return logState{}, ErrLogPruned // drop everything
  52. }
  53. // Initialize: New head has the same item as the old head.
  54. first := &logEntry{item: head.item} // new head
  55. last := first // new tail (last copied cons)
  56. cur := head.next
  57. for i := 1; i < newSize; i++ {
  58. cp := &logEntry{item: cur.item}
  59. last.next = cp
  60. last = cp
  61. cur = cur.next
  62. }
  63. var err error
  64. if head.item.Cursor.Diff(last.item.Cursor) <= lg.windowSize {
  65. err = ErrLogPruned
  66. }
  67. return logState{
  68. oldest: last.item.Cursor,
  69. newest: first.item.Cursor,
  70. size: newSize,
  71. head: first,
  72. }, err
  73. }
  74. // pruneAge returns a new log state by pruning items older than the window
  75. // prior to the head element.
  76. func (lg *Log) pruneAge(head *logEntry) logState {
  77. first := &logEntry{item: head.item}
  78. last := first
  79. size := 1
  80. for cur := head.next; cur != nil; cur = cur.next {
  81. diff := head.item.Cursor.Diff(cur.item.Cursor)
  82. if diff > lg.windowSize {
  83. break // all remaining items are older than the window
  84. }
  85. cp := &logEntry{item: cur.item}
  86. last.next = cp
  87. last = cp
  88. size++
  89. }
  90. return logState{
  91. oldest: last.item.Cursor,
  92. newest: first.item.Cursor,
  93. size: size,
  94. head: first,
  95. }
  96. }