github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/test/revisions.go (about)

     1  package test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/authzed/spicedb/internal/datastore/common"
    13  	"github.com/authzed/spicedb/pkg/datastore"
    14  	ns "github.com/authzed/spicedb/pkg/namespace"
    15  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    16  	dispatch "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    17  )
    18  
    19  // RevisionQuantizationTest tests whether or not the requirements for revisions hold
    20  // for a particular datastore.
    21  func RevisionQuantizationTest(t *testing.T, tester DatastoreTester) {
    22  	testCases := []struct {
    23  		quantizationRange        time.Duration
    24  		expectFindLowerRevisions bool
    25  	}{
    26  		{0 * time.Second, false},
    27  		{100 * time.Millisecond, true},
    28  	}
    29  
    30  	for _, tc := range testCases {
    31  		tc := tc
    32  		t.Run(fmt.Sprintf("quantization%s", tc.quantizationRange), func(t *testing.T) {
    33  			require := require.New(t)
    34  
    35  			ds, err := tester.New(tc.quantizationRange, veryLargeGCInterval, veryLargeGCWindow, 1)
    36  			require.NoError(err)
    37  
    38  			ctx := context.Background()
    39  			veryFirstRevision, err := ds.OptimizedRevision(ctx)
    40  			require.NoError(err)
    41  
    42  			postSetupRevision := setupDatastore(ds, require)
    43  			require.True(postSetupRevision.GreaterThan(veryFirstRevision))
    44  
    45  			// Create some revisions
    46  			var writtenAt datastore.Revision
    47  			tpl := makeTestTuple("first", "owner")
    48  			for i := 0; i < 10; i++ {
    49  				writtenAt, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl)
    50  				require.NoError(err)
    51  			}
    52  			require.True(writtenAt.GreaterThan(postSetupRevision))
    53  
    54  			// Get the new now revision
    55  			nowRevision, err := ds.HeadRevision(ctx)
    56  			require.NoError(err)
    57  
    58  			// Let the quantization window expire
    59  			time.Sleep(tc.quantizationRange)
    60  
    61  			// Now we should ONLY get revisions later than the now revision
    62  			for start := time.Now(); time.Since(start) < 10*time.Millisecond; {
    63  				testRevision, err := ds.OptimizedRevision(ctx)
    64  				require.NoError(err)
    65  				require.True(nowRevision.LessThan(testRevision) || nowRevision.Equal(testRevision))
    66  			}
    67  		})
    68  	}
    69  }
    70  
    71  // RevisionSerializationTest tests whether the revisions generated by this datastore can
    72  // be serialized and sent through the dispatch layer.
    73  func RevisionSerializationTest(t *testing.T, tester DatastoreTester) {
    74  	require := require.New(t)
    75  
    76  	ds, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1)
    77  	require.NoError(err)
    78  
    79  	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    80  	defer cancel()
    81  	revToTest, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
    82  		return rwt.WriteNamespaces(ctx, testNamespace)
    83  	})
    84  	require.NoError(err)
    85  
    86  	meta := dispatch.ResolverMeta{
    87  		AtRevision:     revToTest.String(),
    88  		DepthRemaining: 50,
    89  		TraversalBloom: dispatch.MustNewTraversalBloomFilter(50),
    90  	}
    91  	require.NoError(meta.Validate())
    92  }
    93  
    94  // RevisionGCTest makes sure revision GC takes place, revisions out-side of the GC window
    95  // are invalid, and revisions inside the GC window are valid.
    96  func RevisionGCTest(t *testing.T, tester DatastoreTester) {
    97  	require := require.New(t)
    98  
    99  	ds, err := tester.New(0, 10*time.Millisecond, 300*time.Millisecond, 1)
   100  	require.NoError(err)
   101  
   102  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   103  	defer cancel()
   104  
   105  	testCaveat := createCoreCaveat(t)
   106  	_, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   107  		if err := rwt.WriteNamespaces(ctx, ns.Namespace("foo/createdtxgc")); err != nil {
   108  			return err
   109  		}
   110  		return rwt.WriteCaveats(ctx, []*core.CaveatDefinition{
   111  			testCaveat,
   112  		})
   113  	})
   114  	require.NoError(err)
   115  
   116  	previousRev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   117  		return rwt.WriteNamespaces(ctx, testNamespace)
   118  	})
   119  	require.NoError(err)
   120  
   121  	require.NoError(ds.CheckRevision(ctx, previousRev), "expected latest write revision to be within GC window")
   122  
   123  	head, err := ds.HeadRevision(ctx)
   124  	require.NoError(err)
   125  	require.NoError(ds.CheckRevision(ctx, head), "expected head revision to be valid in GC Window")
   126  
   127  	// Make sure GC kicks in after the window.
   128  	time.Sleep(300 * time.Millisecond)
   129  
   130  	gcable, ok := ds.(common.GarbageCollector)
   131  	if ok {
   132  		gcable.ResetGCCompleted()
   133  		require.Eventually(func() bool { return gcable.HasGCRun() }, 5*time.Second, 50*time.Millisecond, "GC was never run as expected")
   134  	}
   135  
   136  	// FIXME currently the various datastores behave differently when a revision was requested and GC Window elapses.
   137  	// this is due to the fact MySQL and PostgreSQL implement revisions as a snapshot, while CRDB, Spanner and MemDB
   138  	// implement it as a timestamp.
   139  	//
   140  	// previous head revision is not valid if outside GC Window
   141  	// require.Error(ds.CheckRevision(ctx, head), "expected head revision to be valid if out of GC window")
   142  	//
   143  	// latest state of the system is invalid if head revision is out of GC window
   144  	//_, _, err = ds.SnapshotReader(head).ReadNamespaceByName(ctx, "foo/bar")
   145  	// require.Error(err, "expected previously written schema to exist at out-of-GC window head")
   146  
   147  	// check freshly fetched head revision is valid after GC window elapsed
   148  	head, err = ds.HeadRevision(ctx)
   149  	require.NoError(err)
   150  
   151  	// check that we can read a caveat whose revision has been garbage collectged
   152  	_, _, err = ds.SnapshotReader(head).ReadCaveatByName(ctx, testCaveat.Name)
   153  	require.NoError(err, "expected previously written caveat should exist at head")
   154  
   155  	// check that we can read the namespace which had its revision garbage collected
   156  	_, _, err = ds.SnapshotReader(head).ReadNamespaceByName(ctx, "foo/createdtxgc")
   157  	require.NoError(err, "expected previously written namespace should exist at head")
   158  
   159  	// state of the system is also consistent at a recent call to head
   160  	_, _, err = ds.SnapshotReader(head).ReadNamespaceByName(ctx, "foo/bar")
   161  	require.NoError(err, "expected previously written schema to exist at head")
   162  
   163  	// and that recent call to head revision is also valid, even after a GC window cycle without writes elapsed
   164  	require.NoError(ds.CheckRevision(ctx, head), "expected freshly obtained head revision to be valid")
   165  
   166  	// write happens, we get a new head revision
   167  	newerRev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   168  		return rwt.WriteNamespaces(ctx, testNamespace)
   169  	})
   170  	require.NoError(err)
   171  	require.NoError(ds.CheckRevision(ctx, newerRev), "expected newer head revision to be within GC Window")
   172  	require.Error(ds.CheckRevision(ctx, previousRev), "expected revision head-1 to be outside GC Window")
   173  }
   174  
   175  func SequentialRevisionsTest(t *testing.T, tester DatastoreTester) {
   176  	require := require.New(t)
   177  
   178  	ds, err := tester.New(0, 10*time.Second, 300*time.Minute, 1)
   179  	require.NoError(err)
   180  
   181  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   182  	defer cancel()
   183  
   184  	var previous datastore.Revision
   185  	for i := 0; i < 50; i++ {
   186  		head, err := ds.HeadRevision(ctx)
   187  		require.NoError(err)
   188  		require.NoError(ds.CheckRevision(ctx, head), "expected head revision to be valid in GC Window")
   189  
   190  		if previous != nil {
   191  			require.True(head.GreaterThan(previous) || head.Equal(previous))
   192  		}
   193  
   194  		previous = head
   195  	}
   196  }
   197  
   198  func ConcurrentRevisionsTest(t *testing.T, tester DatastoreTester) {
   199  	require := require.New(t)
   200  
   201  	ds, err := tester.New(0, 10*time.Second, 300*time.Minute, 1)
   202  	require.NoError(err)
   203  
   204  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   205  	defer cancel()
   206  
   207  	var wg sync.WaitGroup
   208  	wg.Add(10)
   209  
   210  	startingRev, err := ds.HeadRevision(ctx)
   211  	require.NoError(err)
   212  
   213  	for i := 0; i < 10; i++ {
   214  		go func() {
   215  			defer wg.Done()
   216  
   217  			for i := 0; i < 5; i++ {
   218  				head, err := ds.HeadRevision(ctx)
   219  				require.NoError(err)
   220  				require.NoError(ds.CheckRevision(ctx, head), "expected head revision to be valid in GC Window")
   221  				require.True(head.GreaterThan(startingRev) || head.Equal(startingRev))
   222  			}
   223  		}()
   224  	}
   225  
   226  	wg.Wait()
   227  }