github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/memdb/revisions.go (about) 1 package memdb 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/authzed/spicedb/internal/datastore/revisions" 9 "github.com/authzed/spicedb/pkg/datastore" 10 ) 11 12 var ParseRevisionString = revisions.RevisionParser(revisions.Timestamp) 13 14 func nowRevision() revisions.TimestampRevision { 15 return revisions.NewForTime(time.Now().UTC()) 16 } 17 18 func (mdb *memdbDatastore) newRevisionID() revisions.TimestampRevision { 19 mdb.Lock() 20 defer mdb.Unlock() 21 22 existing := mdb.revisions[len(mdb.revisions)-1].revision 23 created := nowRevision() 24 25 // NOTE: The time.Now().UTC() only appears to have *microsecond* level 26 // precision on macOS Monterey in Go 1.19.1. This means that HeadRevision 27 // and the result of a ReadWriteTx could return the *same* transaction ID 28 // if both are executed in sequence without any other forms of delay on 29 // macOS. We therefore check if the created transaction ID matches that 30 // previously created and, if not, add to it. 31 // 32 // See: https://github.com/golang/go/issues/22037 which appeared to fix 33 // this in Go 1.9.2, but there appears to have been a reversion with either 34 // the new version of macOS or Go. 35 if created.Equal(existing) { 36 return revisions.NewForTimestamp(created.TimestampNanoSec() + 1) 37 } 38 39 return created 40 } 41 42 func (mdb *memdbDatastore) HeadRevision(_ context.Context) (datastore.Revision, error) { 43 mdb.RLock() 44 defer mdb.RUnlock() 45 if mdb.db == nil { 46 return nil, fmt.Errorf("datastore has been closed") 47 } 48 49 return mdb.headRevisionNoLock(), nil 50 } 51 52 func (mdb *memdbDatastore) SquashRevisionsForTesting() { 53 mdb.revisions = []snapshot{ 54 { 55 revision: nowRevision(), 56 db: mdb.db, 57 }, 58 } 59 } 60 61 func (mdb *memdbDatastore) headRevisionNoLock() revisions.TimestampRevision { 62 return mdb.revisions[len(mdb.revisions)-1].revision 63 } 64 65 func (mdb *memdbDatastore) OptimizedRevision(_ context.Context) (datastore.Revision, error) { 66 mdb.RLock() 67 defer mdb.RUnlock() 68 if mdb.db == nil { 69 return nil, fmt.Errorf("datastore has been closed") 70 } 71 72 now := nowRevision() 73 return revisions.NewForTimestamp(now.TimestampNanoSec() - now.TimestampNanoSec()%mdb.quantizationPeriod), nil 74 } 75 76 func (mdb *memdbDatastore) CheckRevision(_ context.Context, dr datastore.Revision) error { 77 mdb.RLock() 78 defer mdb.RUnlock() 79 if mdb.db == nil { 80 return fmt.Errorf("datastore has been closed") 81 } 82 83 return mdb.checkRevisionLocalCallerMustLock(dr) 84 } 85 86 func (mdb *memdbDatastore) checkRevisionLocalCallerMustLock(dr datastore.Revision) error { 87 now := nowRevision() 88 89 // Ensure the revision has not fallen outside of the GC window. If it has, it is considered 90 // invalid. 91 if mdb.revisionOutsideGCWindow(now, dr) { 92 return datastore.NewInvalidRevisionErr(dr, datastore.RevisionStale) 93 } 94 95 // If the revision <= now and later than the GC window, it is assumed to be valid, even if 96 // HEAD revision is behind it. 97 if dr.GreaterThan(now) { 98 // If the revision is in the "future", then check to ensure that it is <= of HEAD to handle 99 // the microsecond granularity on macos (see comment above in newRevisionID) 100 headRevision := mdb.headRevisionNoLock() 101 if dr.LessThan(headRevision) || dr.Equal(headRevision) { 102 return nil 103 } 104 105 return datastore.NewInvalidRevisionErr(dr, datastore.CouldNotDetermineRevision) 106 } 107 108 return nil 109 } 110 111 func (mdb *memdbDatastore) revisionOutsideGCWindow(now revisions.TimestampRevision, revisionRaw datastore.Revision) bool { 112 // make an exception for head revision - it will be acceptable even if outside GC Window 113 if revisionRaw.Equal(mdb.headRevisionNoLock()) { 114 return false 115 } 116 117 oldest := revisions.NewForTimestamp(now.TimestampNanoSec() + mdb.negativeGCWindow) 118 return revisionRaw.LessThan(oldest) 119 }