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  }