github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/proxy/hedging_test.go (about) 1 package proxy 2 3 import ( 4 "context" 5 "errors" 6 "runtime" 7 "testing" 8 "time" 9 10 "github.com/benbjohnson/clock" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "go.uber.org/goleak" 14 15 "github.com/authzed/spicedb/internal/datastore/common" 16 "github.com/authzed/spicedb/internal/datastore/proxy/proxy_test" 17 "github.com/authzed/spicedb/internal/datastore/revisions" 18 "github.com/authzed/spicedb/pkg/datastore" 19 "github.com/authzed/spicedb/pkg/datastore/options" 20 core "github.com/authzed/spicedb/pkg/proto/core/v1" 21 ) 22 23 var ( 24 slowQueryTime = 5 * time.Millisecond 25 maxSampleCount = uint64(1_000_000) 26 quantile = 0.95 27 28 errKnown = errors.New("known error") 29 errAnotherKnown = errors.New("another known error") 30 nsKnown = "namespace_name" 31 revisionKnown = revisions.NewForTransactionID(1) 32 anotherRevisionKnown = revisions.NewForTransactionID(2) 33 34 emptyIterator = common.NewSliceRelationshipIterator(nil, options.Unsorted) 35 ) 36 37 type testFunc func(t *testing.T, proxy datastore.Datastore, expectFirst bool) 38 39 func TestDatastoreRequestHedging(t *testing.T) { 40 testCases := []struct { 41 methodName string 42 useSnapshotReader bool 43 arguments []interface{} 44 firstCallResults []interface{} 45 secondCallResults []interface{} 46 f testFunc 47 }{ 48 { 49 "ReadNamespaceByName", 50 true, 51 []interface{}{nsKnown}, 52 []interface{}{&core.NamespaceDefinition{}, revisionKnown, errKnown}, 53 []interface{}{&core.NamespaceDefinition{}, anotherRevisionKnown, errKnown}, 54 func(t *testing.T, proxy datastore.Datastore, expectFirst bool) { 55 require := require.New(t) 56 _, rev, err := proxy.SnapshotReader(datastore.NoRevision).ReadNamespaceByName(context.Background(), nsKnown) 57 require.ErrorIs(errKnown, err) 58 if expectFirst { 59 require.Equal(revisionKnown, rev) 60 } else { 61 require.Equal(anotherRevisionKnown, rev) 62 } 63 }, 64 }, 65 { 66 "OptimizedRevision", 67 false, 68 []interface{}{mock.Anything, mock.Anything}, 69 []interface{}{revisionKnown, errKnown}, 70 []interface{}{anotherRevisionKnown, errKnown}, 71 func(t *testing.T, proxy datastore.Datastore, expectFirst bool) { 72 require := require.New(t) 73 rev, err := proxy.OptimizedRevision(context.Background()) 74 require.ErrorIs(errKnown, err) 75 if expectFirst { 76 require.Equal(revisionKnown, rev) 77 } else { 78 require.Equal(anotherRevisionKnown, rev) 79 } 80 }, 81 }, 82 { 83 "HeadRevision", 84 false, 85 []interface{}{mock.Anything}, 86 []interface{}{revisionKnown, errKnown}, 87 []interface{}{anotherRevisionKnown, errKnown}, 88 func(t *testing.T, proxy datastore.Datastore, expectFirst bool) { 89 require := require.New(t) 90 rev, err := proxy.HeadRevision(context.Background()) 91 require.ErrorIs(errKnown, err) 92 if expectFirst { 93 require.Equal(revisionKnown, rev) 94 } else { 95 require.Equal(anotherRevisionKnown, rev) 96 } 97 }, 98 }, 99 { 100 "QueryRelationships", 101 true, 102 []interface{}{mock.Anything, mock.Anything}, 103 []interface{}{emptyIterator, errKnown}, 104 []interface{}{emptyIterator, errAnotherKnown}, 105 func(t *testing.T, proxy datastore.Datastore, expectFirst bool) { 106 require := require.New(t) 107 _, err := proxy. 108 SnapshotReader(datastore.NoRevision). 109 QueryRelationships(context.Background(), datastore.RelationshipsFilter{}) 110 if expectFirst { 111 require.ErrorIs(errKnown, err) 112 } else { 113 require.ErrorIs(errAnotherKnown, err) 114 } 115 }, 116 }, 117 { 118 "ReverseQueryRelationships", 119 true, 120 []interface{}{mock.Anything, mock.Anything}, 121 []interface{}{emptyIterator, errKnown}, 122 []interface{}{emptyIterator, errAnotherKnown}, 123 func(t *testing.T, proxy datastore.Datastore, expectFirst bool) { 124 require := require.New(t) 125 _, err := proxy. 126 SnapshotReader(datastore.NoRevision). 127 ReverseQueryRelationships(context.Background(), datastore.SubjectsFilter{}) 128 if expectFirst { 129 require.ErrorIs(errKnown, err) 130 } else { 131 require.ErrorIs(errAnotherKnown, err) 132 } 133 }, 134 }, 135 } 136 137 for _, tc := range testCases { 138 tc := tc 139 t.Run(tc.methodName, func(t *testing.T) { 140 defer goleak.VerifyNone(t, goleak.IgnoreTopFunction("github.com/authzed/spicedb/internal/datastore/proxy.autoAdvance.func1"), goleak.IgnoreCurrent()) 141 mockTime := clock.NewMock() 142 delegateDS := &proxy_test.MockDatastore{} 143 proxy, err := newHedgingProxyWithTimeSource( 144 delegateDS, slowQueryTime, maxSampleCount, quantile, mockTime, 145 ) 146 require.NoError(t, err) 147 148 delegate := &delegateDS.Mock 149 150 if tc.useSnapshotReader { 151 readerMock := &proxy_test.MockReader{} 152 delegate.On("SnapshotReader", mock.Anything).Return(readerMock) 153 delegate = &readerMock.Mock 154 } 155 156 delegate. 157 On(tc.methodName, tc.arguments...). 158 Return(tc.firstCallResults...). 159 Once() 160 161 tc.f(t, proxy, true) 162 delegate.AssertExpectations(t) 163 164 delegate. 165 On(tc.methodName, tc.arguments...). 166 WaitUntil(mockTime.After(5 * slowQueryTime)). 167 Return(tc.firstCallResults...). 168 Once() 169 delegate. 170 On(tc.methodName, tc.arguments...). 171 Return(tc.secondCallResults...). 172 Once() 173 174 done := autoAdvance(mockTime, slowQueryTime, 6*slowQueryTime) 175 176 tc.f(t, proxy, false) 177 delegate.AssertExpectations(t) 178 179 <-done 180 181 delegate. 182 On(tc.methodName, tc.arguments...). 183 WaitUntil(mockTime.After(7 * slowQueryTime)). 184 Return(tc.firstCallResults...). 185 Once() 186 delegate. 187 On(tc.methodName, tc.arguments...). 188 WaitUntil(mockTime.After(8 * slowQueryTime)). 189 Return(tc.secondCallResults...). 190 Once() 191 192 autoAdvance(mockTime, slowQueryTime, 9*slowQueryTime) 193 194 tc.f(t, proxy, true) 195 delegate.AssertExpectations(t) 196 }) 197 } 198 } 199 200 func TestDigestRollover(t *testing.T) { 201 require := require.New(t) 202 203 delegate := &proxy_test.MockDatastore{} 204 mockTime := clock.NewMock() 205 proxy := &hedgingProxy{ 206 Datastore: delegate, 207 headRevisionHedger: newHedger(mockTime, slowQueryTime, 100, 0.9999999999), 208 } 209 210 // Simulate a request that starts off fast enough 211 delegate. 212 On("HeadRevision", mock.Anything). 213 WaitUntil(mockTime.After(slowQueryTime/2)). 214 Return(datastore.NoRevision, errKnown). 215 Once() 216 217 done := autoAdvance(mockTime, slowQueryTime/4, slowQueryTime*2) 218 219 _, err := proxy.HeadRevision(context.Background()) 220 require.ErrorIs(err, errKnown) 221 delegate.AssertExpectations(t) 222 223 <-done 224 225 for i := time.Duration(0); i < 205; i++ { 226 delegate. 227 On("HeadRevision", mock.Anything). 228 WaitUntil(mockTime.After(i*100*time.Microsecond)). 229 Return(datastore.NoRevision, errKnown). 230 Once() 231 } 232 233 done = autoAdvance(mockTime, 100*time.Microsecond, 205*100*time.Microsecond) 234 235 for i := 0; i < 200; i++ { 236 _, err := proxy.HeadRevision(context.Background()) 237 require.ErrorIs(err, errKnown) 238 } 239 <-done 240 241 delegate.ExpectedCalls = nil 242 243 // Now run a request which previously would have been fast enough and ensure that the 244 // request is hedged 245 delegate. 246 On("HeadRevision", mock.Anything). 247 WaitUntil(mockTime.After(slowQueryTime/2)). 248 Return(datastore.NoRevision, errKnown). 249 Once() 250 delegate. 251 On("HeadRevision", mock.Anything). 252 WaitUntil(mockTime.After(100*time.Microsecond)). 253 Return(datastore.NoRevision, errAnotherKnown). 254 Once() 255 256 autoAdvance(mockTime, 100*time.Microsecond, slowQueryTime) 257 258 _, err = proxy.HeadRevision(context.Background()) 259 require.ErrorIs(errAnotherKnown, err) 260 delegate.AssertExpectations(t) 261 } 262 263 func TestBadArgs(t *testing.T) { 264 require := require.New(t) 265 delegate := &proxy_test.MockDatastore{} 266 267 _, err := NewHedgingProxy(delegate, -1*time.Millisecond, maxSampleCount, quantile) 268 require.Error(err) 269 270 _, err = NewHedgingProxy(delegate, 10*time.Millisecond, 10, quantile) 271 require.Error(err) 272 273 _, err = NewHedgingProxy(delegate, 10*time.Millisecond, 1000, 0.0) 274 require.Error(err) 275 276 _, err = NewHedgingProxy(delegate, 10*time.Millisecond, 1000, 1.0) 277 require.Error(err) 278 } 279 280 func TestDatastoreE2E(t *testing.T) { 281 require := require.New(t) 282 283 delegateDatastore := &proxy_test.MockDatastore{} 284 delegateReader := &proxy_test.MockReader{} 285 mockTime := clock.NewMock() 286 287 proxy, err := newHedgingProxyWithTimeSource( 288 delegateDatastore, slowQueryTime, maxSampleCount, quantile, mockTime, 289 ) 290 require.NoError(err) 291 292 expectedTuples := []*core.RelationTuple{ 293 { 294 ResourceAndRelation: &core.ObjectAndRelation{ 295 Namespace: "test", 296 ObjectId: "test", 297 Relation: "test", 298 }, 299 Subject: &core.ObjectAndRelation{ 300 Namespace: "test", 301 ObjectId: "test", 302 Relation: "test", 303 }, 304 }, 305 } 306 307 delegateDatastore.On("SnapshotReader", mock.Anything).Return(delegateReader) 308 309 delegateReader. 310 On("QueryRelationships", mock.Anything, mock.Anything). 311 Return(common.NewSliceRelationshipIterator(expectedTuples, options.Unsorted), nil). 312 WaitUntil(mockTime.After(2 * slowQueryTime)). 313 Once() 314 delegateReader. 315 On("QueryRelationships", mock.Anything, mock.Anything). 316 Return(common.NewSliceRelationshipIterator(expectedTuples, options.Unsorted), nil). 317 Once() 318 319 autoAdvance(mockTime, slowQueryTime/2, 2*slowQueryTime) 320 321 it, err := proxy.SnapshotReader(revisionKnown).QueryRelationships( 322 context.Background(), datastore.RelationshipsFilter{ 323 OptionalResourceType: "test", 324 }, 325 ) 326 require.NoError(err) 327 defer it.Close() 328 329 only := it.Next() 330 require.Equal(expectedTuples[0], only) 331 332 require.Nil(it.Next()) 333 require.NoError(it.Err()) 334 335 delegateDatastore.AssertExpectations(t) 336 delegateReader.AssertExpectations(t) 337 } 338 339 func TestContextCancellation(t *testing.T) { 340 require := require.New(t) 341 342 delegate := &proxy_test.MockDatastore{} 343 mockTime := clock.NewMock() 344 proxy, err := newHedgingProxyWithTimeSource( 345 delegate, slowQueryTime, maxSampleCount, quantile, mockTime, 346 ) 347 require.NoError(err) 348 349 delegate. 350 On("HeadRevision", mock.Anything). 351 Return(datastore.NoRevision, errKnown). 352 WaitUntil(mockTime.After(500 * time.Microsecond)). 353 Once() 354 355 ctx, cancel := context.WithCancel(context.Background()) 356 go func() { 357 mockTime.Sleep(100 * time.Microsecond) 358 cancel() 359 }() 360 361 autoAdvance(mockTime, 150*time.Microsecond, 1*time.Millisecond) 362 363 _, err = proxy.HeadRevision(ctx) 364 require.Error(err) 365 } 366 367 func autoAdvance(timeSource *clock.Mock, step time.Duration, totalTime time.Duration) <-chan time.Time { 368 done := make(chan time.Time) 369 370 go func() { 371 defer close(done) 372 373 endTime := timeSource.Now().Add(totalTime) 374 375 runtime.Gosched() 376 for now := timeSource.Now(); now.Before(endTime); now = timeSource.Now() { 377 timeSource.Add(step) 378 runtime.Gosched() 379 } 380 381 done <- timeSource.Now() 382 }() 383 384 return done 385 }