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  }