github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/revisions/remoteclock_test.go (about) 1 package revisions 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/benbjohnson/clock" 9 "github.com/stretchr/testify/require" 10 11 log "github.com/authzed/spicedb/internal/logging" 12 "github.com/authzed/spicedb/pkg/datastore" 13 ) 14 15 func TestRemoteClockOptimizedRevisions(t *testing.T) { 16 type timeAndExpectedRevision struct { 17 unixTime int64 18 expected int64 19 } 20 21 testCases := []struct { 22 name string 23 followerReadDelay time.Duration 24 quantization time.Duration 25 times []timeAndExpectedRevision 26 }{ 27 { 28 "direct", 0, 0, 29 []timeAndExpectedRevision{ 30 {1230, 1230}, 31 {1231, 1231}, 32 {1232, 1232}, 33 {1233, 1233}, 34 {1234, 1234}, 35 {1235, 1235}, 36 }, 37 }, 38 { 39 "simple quantized", 0, 5 * time.Second, 40 []timeAndExpectedRevision{ 41 {1230, 1230}, 42 {1231, 1230}, 43 {1232, 1230}, 44 {1233, 1230}, 45 {1234, 1230}, 46 {1235, 1235}, 47 }, 48 }, 49 { 50 "simple with skew", 5 * time.Second, 5 * time.Second, 51 []timeAndExpectedRevision{ 52 {1230, 1225}, 53 {1231, 1225}, 54 {1232, 1225}, 55 {1233, 1225}, 56 {1234, 1225}, 57 {1235, 1230}, 58 }, 59 }, 60 { 61 "skew no quantization", 5 * time.Second, 0, 62 []timeAndExpectedRevision{ 63 {1230, 1225}, 64 {1231, 1226}, 65 {1232, 1227}, 66 {1233, 1228}, 67 {1234, 1229}, 68 {1235, 1230}, 69 }, 70 }, 71 } 72 73 for _, tc := range testCases { 74 tc := tc 75 t.Run(tc.name, func(t *testing.T) { 76 require := require.New(t) 77 78 rcr := NewRemoteClockRevisions(1*time.Hour, 0, tc.followerReadDelay, tc.quantization) 79 80 remoteClock := clock.NewMock() 81 rcr.clockFn = remoteClock 82 rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) { 83 log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time") 84 return NewForTime(remoteClock.Now()), nil 85 }) 86 87 for _, timeAndExpected := range tc.times { 88 remoteClock.Set(time.Unix(timeAndExpected.unixTime, 0)) 89 90 expected := NewForTimestamp(timeAndExpected.expected * 1_000_000_000) 91 optimized, err := rcr.OptimizedRevision(context.Background()) 92 require.NoError(err) 93 require.True( 94 expected.Equal(optimized), 95 "optimized revision does not match expected: %s != %s", 96 expected, 97 optimized, 98 ) 99 } 100 }) 101 } 102 } 103 104 func TestRemoteClockCheckRevisions(t *testing.T) { 105 testCases := []struct { 106 name string 107 gcWindow time.Duration 108 currentTime int64 109 testRevisionSeconds int64 110 expectError bool 111 }{ 112 {"now is valid", 1 * time.Hour, 12345, 12345, false}, 113 {"near future", 1 * time.Hour, 12345, 12346, true}, 114 {"far future", 1 * time.Hour, 12345, 1650599916, true}, 115 {"recent past", 1 * time.Hour, 12345, 12344, false}, 116 {"expired", 1 * time.Second, 12345, 12343, true}, 117 {"very old", 1 * time.Hour, 12345, 8744, true}, 118 } 119 120 for _, tc := range testCases { 121 tc := tc 122 t.Run(tc.name, func(t *testing.T) { 123 require := require.New(t) 124 125 rcr := NewRemoteClockRevisions(tc.gcWindow, 0, 0, 0) 126 127 remoteClock := clock.NewMock() 128 rcr.clockFn = remoteClock 129 rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) { 130 log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time") 131 return NewForTime(remoteClock.Now()), nil 132 }) 133 134 remoteClock.Set(time.Unix(tc.currentTime, 0)) 135 136 testRevision := NewForTimestamp(tc.testRevisionSeconds * 1_000_000_000) 137 err := rcr.CheckRevision(context.Background(), testRevision) 138 if tc.expectError { 139 require.Error(err) 140 } else { 141 require.NoError(err) 142 } 143 }) 144 } 145 } 146 147 func TestRemoteClockStalenessBeyondGC(t *testing.T) { 148 // Set a GC window of 1 hour. 149 gcWindow := 1 * time.Hour 150 151 // Set a max revision staleness of 100 hours, well in excess of the GC window. 152 maxRevisionStaleness := 100 * time.Hour 153 154 rcr := NewRemoteClockRevisions(gcWindow, maxRevisionStaleness, 0, 0) 155 156 remoteClock := clock.NewMock() 157 rcr.clockFn = remoteClock 158 rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) { 159 log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time") 160 return NewForTime(remoteClock.Now()), nil 161 }) 162 163 // Set the current time to 1. 164 currentTime := int64(1) 165 remoteClock.Set(time.Unix(currentTime, 0)) 166 167 // Call optimized revision. 168 optimized, err := rcr.OptimizedRevision(context.Background()) 169 require.NoError(t, err) 170 171 // Ensure the optimized revision is not past the GC window. 172 err = rcr.CheckRevision(context.Background(), optimized) 173 require.NoError(t, err) 174 175 // Set the current time to 100001 to ensure the optimized revision is past the GC window. 176 remoteClock.Set(time.Unix(100001, 0)) 177 178 newOptimized, err := rcr.OptimizedRevision(context.Background()) 179 require.NoError(t, err) 180 181 // Ensure the new optimized revision is not past the GC window. 182 err = rcr.CheckRevision(context.Background(), newOptimized) 183 require.NoError(t, err) 184 }