github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/proxy/schemacaching/intervaltracker.go (about) 1 package schemacaching 2 3 import ( 4 "slices" 5 "sync" 6 "time" 7 8 "github.com/authzed/spicedb/pkg/datastore" 9 ) 10 11 // intervalTracker is a specialized type for tracking a value over a set of 12 // revision-based time intervals. 13 type intervalTracker[T any] struct { 14 // sortedEntries are the entries in the interval tracker, sorted with the *latest* being *first*. 15 sortedEntries []intervalTrackerEntry[T] 16 17 // lock is the sync lock for the tracker. 18 lock sync.RWMutex 19 } 20 21 // intervalTrackerEntry is a single entry in the interval tracker, over a 22 // period of revisions. 23 type intervalTrackerEntry[T any] struct { 24 // created is the point at which the entry was created. 25 created time.Time 26 27 // value is the value for the tracked interval. 28 value T 29 30 // startingRevision at which the interval begins. 31 startingRevision datastore.Revision 32 33 // endingRevision is the *exclusive* revision at which the interval ends. If not specified, 34 // then the interval is open until the last checkpoint revision. 35 endingRevisionOrNil datastore.Revision 36 } 37 38 // newIntervalTracker creates a new interval tracker. 39 func newIntervalTracker[T any]() *intervalTracker[T] { 40 return &intervalTracker[T]{ 41 sortedEntries: make([]intervalTrackerEntry[T], 0, 1), 42 } 43 } 44 45 // removeStaleIntervals removes all fully-defined intervals that were created at least window-time ago. 46 // Returns true if *all* intervals were removed, and the tracker is now empty. 47 func (it *intervalTracker[T]) removeStaleIntervals(window time.Duration) bool { 48 threshold := time.Now().Add(-window) 49 50 it.lock.Lock() 51 defer it.lock.Unlock() 52 53 it.sortedEntries = slices.DeleteFunc(it.sortedEntries, func(entry intervalTrackerEntry[T]) bool { 54 // The open-ended entry always remains. 55 if entry.endingRevisionOrNil == nil { 56 return false 57 } 58 59 return entry.created.Before(threshold) 60 }) 61 62 return len(it.sortedEntries) == 0 63 } 64 65 // lookup performs lookup of the value in the tracker at the specified revision. lastCheckpointRevision is the 66 // bound to use for the ending revision for the unbounded entry in the tracker (if any). 67 func (it *intervalTracker[T]) lookup(revision datastore.Revision, lastCheckpointRevision datastore.Revision) (T, bool) { 68 it.lock.RLock() 69 defer it.lock.RUnlock() 70 71 // NOTE: The sortedEntries slice is sorted from latest to least recent, which is opposite that expected 72 // by BinarySearchFunc, so all the returned values below are "inverted". 73 index, ok := slices.BinarySearchFunc( 74 it.sortedEntries, 75 revision, 76 func(entry intervalTrackerEntry[T], rev datastore.Revision) int { 77 // If the entry's starting revision exactly matches the revision, then we know we've found 78 // the correct entry. 79 if entry.startingRevision.Equal(rev) { 80 return 0 81 } 82 83 // If the entry starts after the revision, then it precedes the revision in the slice. 84 if entry.startingRevision.GreaterThan(rev) { 85 return -1 86 } 87 88 // Check if the revision is found within the entry. 89 if entry.endingRevisionOrNil != nil { 90 // If the revision is less than the ending revision (exclusively), then we've found 91 // the correct entry. 92 if rev.LessThan(entry.endingRevisionOrNil) { 93 return 0 94 } 95 96 // Otherwise, the revision is later that ending the entry, which means the revision 97 // precedes the entry in the slice. 98 return 1 99 } 100 101 // If the last checkpoint revision is nil, then the entry's ending revision is closed to 102 // anything beyond the entry's starting revision. 103 endingRevisionInclusive := lastCheckpointRevision 104 if lastCheckpointRevision == nil { 105 endingRevisionInclusive = entry.startingRevision 106 } 107 108 // If the entry has no ending revision, then the supplied last checkpoint revision is the *inclusive* 109 // revision for the ending. 110 if rev.LessThan(endingRevisionInclusive) || rev.Equal(endingRevisionInclusive) { 111 return 0 112 } 113 114 if rev.GreaterThan(endingRevisionInclusive) { 115 return -1 116 } 117 118 return 1 119 }) 120 if !ok { 121 return *new(T), false 122 } 123 124 return it.sortedEntries[index].value, true 125 } 126 127 // add adds an entry into the tracker, indicating it becomes alive at the given revision. 128 // Returns whether the entry was successfully added. An entry can only be added if it is 129 // the latest entry: any attempt to add an entry at a revision before the latest found will 130 // return false and no-op. 131 func (it *intervalTracker[T]) add(entry T, revision datastore.Revision) bool { 132 now := time.Now() 133 134 it.lock.Lock() 135 defer it.lock.Unlock() 136 137 if len(it.sortedEntries) == 0 { 138 it.sortedEntries = append(it.sortedEntries, intervalTrackerEntry[T]{ 139 created: now, 140 value: entry, 141 startingRevision: revision, 142 endingRevisionOrNil: nil, 143 }) 144 return true 145 } 146 147 if revision.LessThan(it.sortedEntries[0].startingRevision) { 148 return false 149 } 150 151 // If given the same revision as the top entry (which can happen from some datastores), ignore. 152 if revision.Equal(it.sortedEntries[0].startingRevision) { 153 return true 154 } 155 156 it.sortedEntries[0].endingRevisionOrNil = revision 157 it.sortedEntries = append([]intervalTrackerEntry[T]{ 158 { 159 created: now, 160 value: entry, 161 startingRevision: revision, 162 endingRevisionOrNil: nil, 163 }, 164 }, it.sortedEntries...) 165 return true 166 }