github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/revisions/remoteclock.go (about) 1 package revisions 2 3 import ( 4 "context" 5 "time" 6 7 log "github.com/authzed/spicedb/internal/logging" 8 "github.com/authzed/spicedb/pkg/datastore" 9 "github.com/authzed/spicedb/pkg/spiceerrors" 10 ) 11 12 // RemoteNowFunction queries the datastore to get a current revision. 13 type RemoteNowFunction func(context.Context) (datastore.Revision, error) 14 15 // RemoteClockRevisions handles revision calculation for datastores that provide 16 // their own clocks. 17 type RemoteClockRevisions struct { 18 *CachedOptimizedRevisions 19 20 gcWindowNanos int64 21 nowFunc RemoteNowFunction 22 followerReadDelayNanos int64 23 quantizationNanos int64 24 } 25 26 // NewRemoteClockRevisions returns a RemoteClockRevisions for the given configuration 27 func NewRemoteClockRevisions(gcWindow, maxRevisionStaleness, followerReadDelay, quantization time.Duration) *RemoteClockRevisions { 28 // Ensure the max revision staleness never exceeds the GC window. 29 if maxRevisionStaleness > gcWindow { 30 log.Warn(). 31 Dur("maxRevisionStaleness", maxRevisionStaleness). 32 Dur("gcWindow", gcWindow). 33 Msg("the configured maximum revision staleness exceeds the configured gc window, so capping to gcWindow") 34 maxRevisionStaleness = gcWindow - 1 35 } 36 37 revisions := &RemoteClockRevisions{ 38 CachedOptimizedRevisions: NewCachedOptimizedRevisions( 39 maxRevisionStaleness, 40 ), 41 gcWindowNanos: gcWindow.Nanoseconds(), 42 followerReadDelayNanos: followerReadDelay.Nanoseconds(), 43 quantizationNanos: quantization.Nanoseconds(), 44 } 45 46 revisions.SetOptimizedRevisionFunc(revisions.optimizedRevisionFunc) 47 48 return revisions 49 } 50 51 func (rcr *RemoteClockRevisions) optimizedRevisionFunc(ctx context.Context) (datastore.Revision, time.Duration, error) { 52 nowRev, err := rcr.nowFunc(ctx) 53 if err != nil { 54 return datastore.NoRevision, 0, err 55 } 56 57 if nowRev == datastore.NoRevision { 58 return datastore.NoRevision, 0, datastore.NewInvalidRevisionErr(nowRev, datastore.CouldNotDetermineRevision) 59 } 60 61 nowTS, ok := nowRev.(WithTimestampRevision) 62 if !ok { 63 return datastore.NoRevision, 0, spiceerrors.MustBugf("expected with-timestamp revision, got %T", nowRev) 64 } 65 66 delayedNow := nowTS.TimestampNanoSec() - rcr.followerReadDelayNanos 67 quantized := delayedNow 68 validForNanos := int64(0) 69 if rcr.quantizationNanos > 0 { 70 afterLastQuantization := delayedNow % rcr.quantizationNanos 71 quantized -= afterLastQuantization 72 validForNanos = rcr.quantizationNanos - afterLastQuantization 73 } 74 log.Ctx(ctx).Debug(). 75 Time("quantized", time.Unix(0, quantized)). 76 Int64("readSkew", rcr.followerReadDelayNanos). 77 Int64("totalSkew", nowTS.TimestampNanoSec()-quantized). 78 Msg("revision skews") 79 80 return nowTS.ConstructForTimestamp(quantized), time.Duration(validForNanos) * time.Nanosecond, nil 81 } 82 83 // SetNowFunc sets the function used to determine the head revision 84 func (rcr *RemoteClockRevisions) SetNowFunc(nowFunc RemoteNowFunction) { 85 rcr.nowFunc = nowFunc 86 } 87 88 func (rcr *RemoteClockRevisions) CheckRevision(ctx context.Context, dsRevision datastore.Revision) error { 89 if dsRevision == datastore.NoRevision { 90 return datastore.NewInvalidRevisionErr(dsRevision, datastore.CouldNotDetermineRevision) 91 } 92 93 revision := dsRevision.(WithTimestampRevision) 94 95 ctx, span := tracer.Start(ctx, "CheckRevision") 96 defer span.End() 97 98 // Make sure the system time indicated is within the software GC window 99 now, err := rcr.nowFunc(ctx) 100 if err != nil { 101 return err 102 } 103 104 nowTS, ok := now.(WithTimestampRevision) 105 if !ok { 106 return spiceerrors.MustBugf("expected HLC revision, got %T", now) 107 } 108 109 nowNanos := nowTS.TimestampNanoSec() 110 revisionNanos := revision.TimestampNanoSec() 111 112 isStale := revisionNanos < (nowNanos - rcr.gcWindowNanos) 113 if isStale { 114 log.Ctx(ctx).Debug().Stringer("now", now).Stringer("revision", revision).Msg("stale revision") 115 return datastore.NewInvalidRevisionErr(revision, datastore.RevisionStale) 116 } 117 118 isUnknown := revisionNanos > nowNanos 119 if isUnknown { 120 log.Ctx(ctx).Debug().Stringer("now", now).Stringer("revision", revision).Msg("unknown revision") 121 return datastore.NewInvalidRevisionErr(revision, datastore.CouldNotDetermineRevision) 122 } 123 124 return nil 125 }