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 }