github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/revisions/remoteclock_test.go (about)

     1  package revisions
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/benbjohnson/clock"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	log "github.com/authzed/spicedb/internal/logging"
    12  	"github.com/authzed/spicedb/pkg/datastore"
    13  )
    14  
    15  func TestRemoteClockOptimizedRevisions(t *testing.T) {
    16  	type timeAndExpectedRevision struct {
    17  		unixTime int64
    18  		expected int64
    19  	}
    20  
    21  	testCases := []struct {
    22  		name              string
    23  		followerReadDelay time.Duration
    24  		quantization      time.Duration
    25  		times             []timeAndExpectedRevision
    26  	}{
    27  		{
    28  			"direct", 0, 0,
    29  			[]timeAndExpectedRevision{
    30  				{1230, 1230},
    31  				{1231, 1231},
    32  				{1232, 1232},
    33  				{1233, 1233},
    34  				{1234, 1234},
    35  				{1235, 1235},
    36  			},
    37  		},
    38  		{
    39  			"simple quantized", 0, 5 * time.Second,
    40  			[]timeAndExpectedRevision{
    41  				{1230, 1230},
    42  				{1231, 1230},
    43  				{1232, 1230},
    44  				{1233, 1230},
    45  				{1234, 1230},
    46  				{1235, 1235},
    47  			},
    48  		},
    49  		{
    50  			"simple with skew", 5 * time.Second, 5 * time.Second,
    51  			[]timeAndExpectedRevision{
    52  				{1230, 1225},
    53  				{1231, 1225},
    54  				{1232, 1225},
    55  				{1233, 1225},
    56  				{1234, 1225},
    57  				{1235, 1230},
    58  			},
    59  		},
    60  		{
    61  			"skew no quantization", 5 * time.Second, 0,
    62  			[]timeAndExpectedRevision{
    63  				{1230, 1225},
    64  				{1231, 1226},
    65  				{1232, 1227},
    66  				{1233, 1228},
    67  				{1234, 1229},
    68  				{1235, 1230},
    69  			},
    70  		},
    71  	}
    72  
    73  	for _, tc := range testCases {
    74  		tc := tc
    75  		t.Run(tc.name, func(t *testing.T) {
    76  			require := require.New(t)
    77  
    78  			rcr := NewRemoteClockRevisions(1*time.Hour, 0, tc.followerReadDelay, tc.quantization)
    79  
    80  			remoteClock := clock.NewMock()
    81  			rcr.clockFn = remoteClock
    82  			rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) {
    83  				log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time")
    84  				return NewForTime(remoteClock.Now()), nil
    85  			})
    86  
    87  			for _, timeAndExpected := range tc.times {
    88  				remoteClock.Set(time.Unix(timeAndExpected.unixTime, 0))
    89  
    90  				expected := NewForTimestamp(timeAndExpected.expected * 1_000_000_000)
    91  				optimized, err := rcr.OptimizedRevision(context.Background())
    92  				require.NoError(err)
    93  				require.True(
    94  					expected.Equal(optimized),
    95  					"optimized revision does not match expected: %s != %s",
    96  					expected,
    97  					optimized,
    98  				)
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  func TestRemoteClockCheckRevisions(t *testing.T) {
   105  	testCases := []struct {
   106  		name                string
   107  		gcWindow            time.Duration
   108  		currentTime         int64
   109  		testRevisionSeconds int64
   110  		expectError         bool
   111  	}{
   112  		{"now is valid", 1 * time.Hour, 12345, 12345, false},
   113  		{"near future", 1 * time.Hour, 12345, 12346, true},
   114  		{"far future", 1 * time.Hour, 12345, 1650599916, true},
   115  		{"recent past", 1 * time.Hour, 12345, 12344, false},
   116  		{"expired", 1 * time.Second, 12345, 12343, true},
   117  		{"very old", 1 * time.Hour, 12345, 8744, true},
   118  	}
   119  
   120  	for _, tc := range testCases {
   121  		tc := tc
   122  		t.Run(tc.name, func(t *testing.T) {
   123  			require := require.New(t)
   124  
   125  			rcr := NewRemoteClockRevisions(tc.gcWindow, 0, 0, 0)
   126  
   127  			remoteClock := clock.NewMock()
   128  			rcr.clockFn = remoteClock
   129  			rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) {
   130  				log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time")
   131  				return NewForTime(remoteClock.Now()), nil
   132  			})
   133  
   134  			remoteClock.Set(time.Unix(tc.currentTime, 0))
   135  
   136  			testRevision := NewForTimestamp(tc.testRevisionSeconds * 1_000_000_000)
   137  			err := rcr.CheckRevision(context.Background(), testRevision)
   138  			if tc.expectError {
   139  				require.Error(err)
   140  			} else {
   141  				require.NoError(err)
   142  			}
   143  		})
   144  	}
   145  }
   146  
   147  func TestRemoteClockStalenessBeyondGC(t *testing.T) {
   148  	// Set a GC window of 1 hour.
   149  	gcWindow := 1 * time.Hour
   150  
   151  	// Set a max revision staleness of 100 hours, well in excess of the GC window.
   152  	maxRevisionStaleness := 100 * time.Hour
   153  
   154  	rcr := NewRemoteClockRevisions(gcWindow, maxRevisionStaleness, 0, 0)
   155  
   156  	remoteClock := clock.NewMock()
   157  	rcr.clockFn = remoteClock
   158  	rcr.SetNowFunc(func(ctx context.Context) (datastore.Revision, error) {
   159  		log.Debug().Stringer("now", remoteClock.Now()).Msg("current remote time")
   160  		return NewForTime(remoteClock.Now()), nil
   161  	})
   162  
   163  	// Set the current time to 1.
   164  	currentTime := int64(1)
   165  	remoteClock.Set(time.Unix(currentTime, 0))
   166  
   167  	// Call optimized revision.
   168  	optimized, err := rcr.OptimizedRevision(context.Background())
   169  	require.NoError(t, err)
   170  
   171  	// Ensure the optimized revision is not past the GC window.
   172  	err = rcr.CheckRevision(context.Background(), optimized)
   173  	require.NoError(t, err)
   174  
   175  	// Set the current time to 100001 to ensure the optimized revision is past the GC window.
   176  	remoteClock.Set(time.Unix(100001, 0))
   177  
   178  	newOptimized, err := rcr.OptimizedRevision(context.Background())
   179  	require.NoError(t, err)
   180  
   181  	// Ensure the new optimized revision is not past the GC window.
   182  	err = rcr.CheckRevision(context.Background(), newOptimized)
   183  	require.NoError(t, err)
   184  }