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  }