github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/proxy/schemacaching/intervaltracker_test.go (about) 1 package schemacaching 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/stretchr/testify/require" 8 9 "github.com/authzed/spicedb/internal/datastore/revisions" 10 "github.com/authzed/spicedb/pkg/datastore" 11 ) 12 13 func rev(value string) datastore.Revision { 14 dd := revisions.CommonDecoder{ 15 Kind: revisions.HybridLogicalClock, 16 } 17 rev, _ := dd.RevisionFromString(value) 18 return rev 19 } 20 21 func TestIntervalTrackerBasic(t *testing.T) { 22 tracker := newIntervalTracker[string]() 23 24 // Perform lookup on an empty tracker. 25 _, found := tracker.lookup(rev("1"), rev("0")) 26 require.False(t, found) 27 28 // Add an entry at revision. 29 tracker.add("first", rev("1")) 30 validate(t, tracker) 31 32 // Ensure the entry is found at its own revision. 33 value, found := tracker.lookup(rev("1"), rev("2")) 34 require.True(t, found) 35 require.Equal(t, "first", value) 36 37 // Ensure the entry is found after that revision based on the tracking revision. 38 value, found = tracker.lookup(rev("2"), rev("2")) 39 require.True(t, found) 40 require.Equal(t, "first", value) 41 42 // Ensure the entry is not found if the tracking revision is less than that specified. 43 _, found = tracker.lookup(rev("3"), rev("2")) 44 require.False(t, found) 45 46 // Add another entry at revision 4. 47 tracker.add("second", rev("4")) 48 validate(t, tracker) 49 50 // Ensure that revisions 1-3 find the first. 51 for _, rv := range []string{"1", "1.0000000001", "2", "2.0000000006", "3", "3.0000000009", "3.0000000010"} { 52 value, found = tracker.lookup(rev(rv), rev("5")) 53 require.True(t, found) 54 require.Equal(t, "first", value) 55 56 value, found = tracker.lookup(rev(rv), rev("12")) 57 require.True(t, found) 58 require.Equal(t, "first", value) 59 } 60 61 // Ensure that revision 4+ finds the second. 62 value, found = tracker.lookup(rev("4"), rev("5")) 63 require.True(t, found) 64 require.Equal(t, "second", value) 65 66 value, found = tracker.lookup(rev("5"), rev("5")) 67 require.True(t, found) 68 require.Equal(t, "second", value) 69 70 // Ensure the entry is not found if the tracking revision is less than that specified. 71 _, found = tracker.lookup(rev("5.0000000001"), rev("5")) 72 require.False(t, found) 73 } 74 75 func TestIntervalTrackerBeginningGap(t *testing.T) { 76 tracker := newIntervalTracker[string]() 77 78 // Add an entry at revision 4. 79 tracker.add("first", rev("4")) 80 validate(t, tracker) 81 82 // Ensure the value is found at revision 4. 83 value, found := tracker.lookup(rev("4"), rev("4")) 84 require.True(t, found) 85 require.Equal(t, "first", value) 86 87 // Ensure the value is *not* found at revision 4.1 with max tracked 4. 88 _, found = tracker.lookup(rev("4.0000000001"), rev("4")) 89 require.False(t, found) 90 91 // Ensure the value is found at revision 4.1 when maxed tracked is 5. 92 value, found = tracker.lookup(rev("4"), rev("5")) 93 require.True(t, found) 94 require.Equal(t, "first", value) 95 96 // Make sure a request for revision 1-3 (exclusive) with the tracking revision less (since that "update" hasn't "arrived" yet) 97 for _, rv := range []string{"1", "1.0000000001", "2", "2.0000000006", "2.0000000009"} { 98 _, found = tracker.lookup(rev(rv), rev("3")) 99 require.False(t, found) 100 } 101 } 102 103 func TestIntervalTrackerOutOfOrderInsertion(t *testing.T) { 104 tracker := newIntervalTracker[string]() 105 106 // Add an entry at revision 1. 107 require.True(t, tracker.add("first", rev("1"))) 108 validate(t, tracker) 109 110 // Add an entry at revision 2. 111 tracker.add("second", rev("2")) 112 validate(t, tracker) 113 114 // Add an entry at revision 4. 115 tracker.add("four", rev("4")) 116 validate(t, tracker) 117 118 // Add an entry at revision 3. 119 require.False(t, tracker.add("third", rev("3"))) 120 validate(t, tracker) 121 122 // Make sure a request for revision 1-2 (exclusive) refer to 'first'. 123 for _, rv := range []string{"1", "1.0000000001", "1.0000000009"} { 124 value, found := tracker.lookup(rev(rv), rev("4.0000000001")) 125 require.True(t, found) 126 require.Equal(t, "first", value) 127 } 128 129 // Make sure a request for revision 2-4 (exclusive) refer to 'second'. 130 for _, rv := range []string{"2", "2.0000000005", "3.0000000009"} { 131 value, found := tracker.lookup(rev(rv), rev("4.0000000001")) 132 require.True(t, found) 133 require.Equal(t, "second", value) 134 } 135 136 // Make sure a request for revision 4+ refers to 'four' 137 for _, rv := range []string{"4", "4.0000000001", "4.0000000005", "4.0000000009"} { 138 value, found := tracker.lookup(rev(rv), rev("4.0000000009")) 139 require.True(t, found) 140 require.Equal(t, "four", value) 141 } 142 143 // Add an entry at revision 8. 144 tracker.add("eight", rev("8")) 145 validate(t, tracker) 146 147 // Make sure a request for revision 4-8 (exclusive) refers to 'four' 148 for _, rv := range []string{"4", "4.0000000001", "4.0000000005", "4.0000000001", "7.0000000009"} { 149 value, found := tracker.lookup(rev(rv), rev("8.0000000001")) 150 require.True(t, found) 151 require.Equal(t, "four", value) 152 } 153 154 // Make sure a request for revision 8+ refers to 'eight' 155 for _, rv := range []string{"8", "8.0000000001", "8.0000000005", "8.0000000009"} { 156 value, found := tracker.lookup(rev(rv), rev("8.0000000009")) 157 require.True(t, found) 158 require.Equal(t, "eight", value) 159 } 160 } 161 162 func TestIntervalTrackerGC(t *testing.T) { 163 tracker := newIntervalTracker[string]() 164 165 // Add an entry at revision 2. 166 tracker.add("second", rev("2")) 167 validate(t, tracker) 168 169 // Add an entry at revision 4. 170 tracker.add("four", rev("4")) 171 validate(t, tracker) 172 173 // Add an entry at revision 1. 174 require.False(t, tracker.add("first", rev("1"))) 175 validate(t, tracker) 176 177 // Wait 10ms 178 time.Sleep(10 * time.Millisecond) 179 180 // GC anything older than 5s, which shouldn't change anything. 181 result := tracker.removeStaleIntervals(5 * time.Second) 182 require.False(t, false, result) 183 require.Equal(t, 2, len(tracker.sortedEntries)) 184 185 // GC anything older than 5ms. There should still be a single entry because it is unbounded. 186 result = tracker.removeStaleIntervals(5 * time.Millisecond) 187 require.False(t, false, result) 188 require.Equal(t, 1, len(tracker.sortedEntries)) 189 } 190 191 func TestIntervalTrackerAnotherBasicTest(t *testing.T) { 192 tracker := newIntervalTracker[string]() 193 194 // Add an entry at revision. 195 tracker.add("first", rev("1")) 196 validate(t, tracker) 197 198 // Ensure the entry is found at its own revision. 199 value, found := tracker.lookup(rev("1"), rev("1")) 200 require.True(t, found) 201 require.Equal(t, "first", value) 202 203 // Add another entry at revision 2. 204 tracker.add("second", rev("2")) 205 validate(t, tracker) 206 207 // Ensure the entry is found at its own revision. 208 value, found = tracker.lookup(rev("2"), rev("2")) 209 require.True(t, found) 210 require.Equal(t, "second", value) 211 212 // Ensure entry 1 is found at its own revision. 213 value, found = tracker.lookup(rev("1"), rev("2")) 214 require.True(t, found) 215 require.Equal(t, "first", value) 216 217 // Ensure entry 1 is found at a parent revision. 218 value, found = tracker.lookup(rev("1"), rev("3")) 219 require.True(t, found) 220 require.Equal(t, "first", value) 221 222 // Ensure entry 2 is found at a parent revision. 223 value, found = tracker.lookup(rev("2"), rev("3")) 224 require.True(t, found) 225 require.Equal(t, "second", value) 226 227 value, found = tracker.lookup(rev("3"), rev("3")) 228 require.True(t, found) 229 require.Equal(t, "second", value) 230 231 // Check to ensure not found for revision 3. 232 _, found = tracker.lookup(rev("3"), rev("2")) 233 require.False(t, found) 234 235 // Ensure entry 2 is found even if last revision is lower. 236 value, found = tracker.lookup(rev("2"), rev("1")) 237 require.True(t, found) 238 require.Equal(t, "second", value) 239 } 240 241 func TestIntervalTrackerWithNoLastRevision(t *testing.T) { 242 tracker := newIntervalTracker[string]() 243 244 // Add an entry at revision. 245 tracker.add("first", rev("1")) 246 validate(t, tracker) 247 248 // Ensure the entry is found at its own revision. 249 value, found := tracker.lookup(rev("1"), rev("1")) 250 require.True(t, found) 251 require.Equal(t, "first", value) 252 253 // Ensure the entry is found at its own revision. 254 value, found = tracker.lookup(rev("1"), nil) 255 require.True(t, found) 256 require.Equal(t, "first", value) 257 258 // Add another entry at revision 2. 259 tracker.add("second", rev("2")) 260 validate(t, tracker) 261 262 // Ensure the entry is found at its own revision. 263 value, found = tracker.lookup(rev("2"), nil) 264 require.True(t, found) 265 require.Equal(t, "second", value) 266 267 // Ensure another revision is not found. 268 _, found = tracker.lookup(rev("3"), nil) 269 require.False(t, found) 270 271 // Ensure another revision is not found. 272 _, found = tracker.lookup(rev("0"), nil) 273 require.False(t, found) 274 } 275 276 func TestIntervalTrackerRealWorldUsage(t *testing.T) { 277 tracker := newIntervalTracker[string]() 278 tracker.add("notfound1", rev("1")) 279 validate(t, tracker) 280 281 tracker.add("real2", rev("2")) 282 validate(t, tracker) 283 284 tracker.add("real2-again", rev("3")) 285 validate(t, tracker) 286 287 tracker.add("notfound5", rev("5")) 288 validate(t, tracker) 289 290 value, found := tracker.lookup(rev("5"), rev("5")) 291 require.True(t, found) 292 require.Equal(t, "notfound5", value) 293 294 value, found = tracker.lookup(rev("5"), rev("3.0000000005")) 295 require.True(t, found) 296 require.Equal(t, "notfound5", value) 297 298 value, found = tracker.lookup(rev("2"), rev("5")) 299 require.True(t, found) 300 require.Equal(t, "real2", value) 301 302 value, found = tracker.lookup(rev("2"), rev("3.0000000005")) 303 require.True(t, found) 304 require.Equal(t, "real2", value) 305 306 value, found = tracker.lookup(rev("3.0000000005"), rev("5")) 307 require.True(t, found) 308 require.Equal(t, "real2-again", value) 309 310 value, found = tracker.lookup(rev("3.0000000005"), rev("3.0000000005")) 311 require.True(t, found) 312 require.Equal(t, "real2-again", value) 313 } 314 315 func validate(t *testing.T, tracker *intervalTracker[string]) { 316 for index, entry := range tracker.sortedEntries { 317 if index > 0 { 318 require.NotNil(t, entry.endingRevisionOrNil, "found nil ending revision for entry %d", index) 319 require.True(t, entry.endingRevisionOrNil.LessThan(tracker.sortedEntries[index-1].startingRevision) || entry.endingRevisionOrNil.Equal(tracker.sortedEntries[index-1].startingRevision), "found entry %v->%v after entry %v->%v", entry.startingRevision, entry.endingRevisionOrNil, tracker.sortedEntries[index-1].startingRevision, tracker.sortedEntries[index-1].endingRevisionOrNil) 320 require.True(t, entry.startingRevision.LessThan(entry.endingRevisionOrNil) || entry.startingRevision.Equal(entry.endingRevisionOrNil)) 321 } 322 } 323 }