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  }