github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/cursors_test.go (about)

     1  package graph
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/authzed/spicedb/internal/dispatch"
    11  	"github.com/authzed/spicedb/pkg/datastore/options"
    12  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    13  	"github.com/authzed/spicedb/pkg/tuple"
    14  )
    15  
    16  func TestCursorProduction(t *testing.T) {
    17  	limits := newLimitTracker(10)
    18  
    19  	ci, err := newCursorInformation(&v1.Cursor{
    20  		DispatchVersion: 42,
    21  		Sections:        []string{"1", "2", "3"},
    22  	}, limits, 42)
    23  	require.NoError(t, err)
    24  
    25  	cursor := ci.responsePartialCursor()
    26  	require.Equal(t, uint32(42), cursor.DispatchVersion)
    27  	require.Empty(t, cursor.Sections)
    28  
    29  	cci, err := ci.withOutgoingSection("4")
    30  	require.NoError(t, err)
    31  
    32  	ccursor := cci.responsePartialCursor()
    33  
    34  	require.Equal(t, uint32(42), ccursor.DispatchVersion)
    35  	require.Equal(t, []string{"4"}, ccursor.Sections)
    36  }
    37  
    38  func TestCursorDifferentDispatchVersion(t *testing.T) {
    39  	limits := newLimitTracker(10)
    40  
    41  	_, err := newCursorInformation(&v1.Cursor{
    42  		DispatchVersion: 2,
    43  		Sections:        []string{},
    44  	}, limits, 1)
    45  	require.Error(t, err)
    46  }
    47  
    48  func TestCursorHasHeadSectionOnEmpty(t *testing.T) {
    49  	limits := newLimitTracker(10)
    50  
    51  	ci, err := newCursorInformation(&v1.Cursor{
    52  		DispatchVersion: 1,
    53  		Sections:        []string{},
    54  	}, limits, 1)
    55  	require.NoError(t, err)
    56  
    57  	value, ok := ci.headSectionValue()
    58  	require.False(t, ok)
    59  	require.Equal(t, "", value)
    60  }
    61  
    62  func TestCursorWithClonedLimits(t *testing.T) {
    63  	limits := newLimitTracker(10)
    64  
    65  	ci, err := newCursorInformation(&v1.Cursor{
    66  		DispatchVersion: 1,
    67  		Sections:        []string{},
    68  	}, limits, 1)
    69  	require.NoError(t, err)
    70  
    71  	require.Equal(t, uint32(10), ci.limits.currentLimit)
    72  	require.Equal(t, uint32(1), ci.dispatchCursorVersion)
    73  
    74  	cloned := ci.withClonedLimits()
    75  	require.Equal(t, uint32(10), cloned.limits.currentLimit)
    76  	require.Equal(t, uint32(1), cloned.dispatchCursorVersion)
    77  
    78  	require.True(t, limits.prepareForPublishing())
    79  
    80  	require.Equal(t, uint32(9), ci.limits.currentLimit)
    81  	require.Equal(t, uint32(1), ci.dispatchCursorVersion)
    82  
    83  	require.Equal(t, uint32(10), cloned.limits.currentLimit)
    84  	require.Equal(t, uint32(1), cloned.dispatchCursorVersion)
    85  }
    86  
    87  func TestCursorSections(t *testing.T) {
    88  	limits := newLimitTracker(10)
    89  
    90  	ci, err := newCursorInformation(&v1.Cursor{
    91  		DispatchVersion: 1,
    92  		Sections:        []string{"1", "two"},
    93  	}, limits, 1)
    94  	require.NoError(t, err)
    95  	require.Equal(t, uint32(1), ci.dispatchCursorVersion)
    96  
    97  	value, ok := ci.headSectionValue()
    98  	require.True(t, ok)
    99  	require.Equal(t, value, "1")
   100  
   101  	ivalue, err := ci.integerSectionValue()
   102  	require.NoError(t, err)
   103  	require.Equal(t, ivalue, 1)
   104  }
   105  
   106  func TestCursorNonIntSection(t *testing.T) {
   107  	limits := newLimitTracker(10)
   108  
   109  	ci, err := newCursorInformation(&v1.Cursor{
   110  		DispatchVersion: 1,
   111  		Sections:        []string{"one", "two"},
   112  	}, limits, 1)
   113  	require.NoError(t, err)
   114  
   115  	value, ok := ci.headSectionValue()
   116  	require.True(t, ok)
   117  	require.Equal(t, value, "one")
   118  
   119  	_, err = ci.integerSectionValue()
   120  	require.Error(t, err)
   121  }
   122  
   123  func TestWithSubsetInCursor(t *testing.T) {
   124  	limits := newLimitTracker(10)
   125  
   126  	ci, err := newCursorInformation(&v1.Cursor{
   127  		DispatchVersion: 1,
   128  		Sections:        []string{"100"},
   129  	}, limits, 1)
   130  	require.NoError(t, err)
   131  
   132  	handlerCalled := false
   133  	nextCalled := false
   134  	err = withSubsetInCursor(ci,
   135  		func(currentOffset int, nextCursorWith afterResponseCursor) error {
   136  			require.Equal(t, 100, currentOffset)
   137  			handlerCalled = true
   138  			return nil
   139  		},
   140  		func(c cursorInformation) error {
   141  			nextCalled = true
   142  			return nil
   143  		})
   144  	require.NoError(t, err)
   145  	require.True(t, handlerCalled)
   146  	require.True(t, nextCalled)
   147  }
   148  
   149  func TestCombineCursors(t *testing.T) {
   150  	cursor1 := &v1.Cursor{
   151  		DispatchVersion: 1,
   152  		Sections:        []string{"a", "b", "c"},
   153  	}
   154  	cursor2 := &v1.Cursor{
   155  		DispatchVersion: 1,
   156  		Sections:        []string{"d", "e", "f"},
   157  	}
   158  
   159  	combined, err := combineCursors(cursor1, cursor2)
   160  	require.NoError(t, err)
   161  	require.Equal(t, []string{"a", "b", "c", "d", "e", "f"}, combined.Sections)
   162  }
   163  
   164  func TestCombineCursorsWithNil(t *testing.T) {
   165  	cursor2 := &v1.Cursor{
   166  		DispatchVersion: 1,
   167  		Sections:        []string{"d", "e", "f"},
   168  	}
   169  
   170  	combined, err := combineCursors(nil, cursor2)
   171  	require.NoError(t, err)
   172  	require.Equal(t, []string{"d", "e", "f"}, combined.Sections)
   173  }
   174  
   175  func TestWithParallelizedStreamingIterableInCursor(t *testing.T) {
   176  	limits := newLimitTracker(50)
   177  
   178  	ci, err := newCursorInformation(&v1.Cursor{
   179  		DispatchVersion: 1,
   180  		Sections:        []string{},
   181  	}, limits, 1)
   182  	require.NoError(t, err)
   183  
   184  	items := []int{10, 20, 30, 40, 50}
   185  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   186  	err = withParallelizedStreamingIterableInCursor[int, int](
   187  		context.Background(),
   188  		ci,
   189  		items,
   190  		parentStream,
   191  		2,
   192  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   193  			err := stream.Publish(item * 10)
   194  			if err != nil {
   195  				return err
   196  			}
   197  
   198  			return stream.Publish((item * 10) + 1)
   199  		})
   200  
   201  	require.NoError(t, err)
   202  	require.Equal(t, []int{100, 101, 200, 201, 300, 301, 400, 401, 500, 501}, parentStream.Results())
   203  }
   204  
   205  func TestWithParallelizedStreamingIterableInCursorWithExistingCursor(t *testing.T) {
   206  	limits := newLimitTracker(50)
   207  
   208  	ci, err := newCursorInformation(&v1.Cursor{
   209  		DispatchVersion: 1,
   210  		Sections:        []string{"2"},
   211  	}, limits, 1)
   212  	require.NoError(t, err)
   213  
   214  	items := []int{10, 20, 30, 40, 50}
   215  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   216  	err = withParallelizedStreamingIterableInCursor[int, int](
   217  		context.Background(),
   218  		ci,
   219  		items,
   220  		parentStream,
   221  		2,
   222  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   223  			err := stream.Publish(item * 10)
   224  			if err != nil {
   225  				return err
   226  			}
   227  
   228  			return stream.Publish((item * 10) + 1)
   229  		})
   230  
   231  	require.NoError(t, err)
   232  	require.Equal(t, []int{300, 301, 400, 401, 500, 501}, parentStream.Results())
   233  }
   234  
   235  func TestWithParallelizedStreamingIterableInCursorWithLimit(t *testing.T) {
   236  	limits := newLimitTracker(5)
   237  
   238  	ci, err := newCursorInformation(&v1.Cursor{
   239  		DispatchVersion: 1,
   240  		Sections:        []string{},
   241  	}, limits, 1)
   242  	require.NoError(t, err)
   243  
   244  	items := []int{10, 20, 30, 40, 50}
   245  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   246  	err = withParallelizedStreamingIterableInCursor[int, int](
   247  		context.Background(),
   248  		ci,
   249  		items,
   250  		parentStream,
   251  		2,
   252  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   253  			err := stream.Publish(item * 10)
   254  			if err != nil {
   255  				return err
   256  			}
   257  
   258  			return stream.Publish((item * 10) + 1)
   259  		})
   260  
   261  	require.NoError(t, err)
   262  	require.Equal(t, []int{100, 101, 200, 201, 300}, parentStream.Results())
   263  }
   264  
   265  func TestWithParallelizedStreamingIterableInCursorEnsureParallelism(t *testing.T) {
   266  	limits := newLimitTracker(500)
   267  
   268  	ci, err := newCursorInformation(&v1.Cursor{
   269  		DispatchVersion: 1,
   270  		Sections:        []string{},
   271  	}, limits, 1)
   272  	require.NoError(t, err)
   273  
   274  	items := []int{}
   275  	expected := []int{}
   276  	for i := 0; i < 500; i++ {
   277  		items = append(items, i)
   278  		expected = append(expected, i*10)
   279  	}
   280  
   281  	encountered := []int{}
   282  	lock := sync.Mutex{}
   283  
   284  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   285  	err = withParallelizedStreamingIterableInCursor[int, int](
   286  		context.Background(),
   287  		ci,
   288  		items,
   289  		parentStream,
   290  		5,
   291  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   292  			lock.Lock()
   293  			encountered = append(encountered, item)
   294  			lock.Unlock()
   295  
   296  			return stream.Publish(item * 10)
   297  		})
   298  
   299  	require.Equal(t, len(expected), len(encountered))
   300  	require.NotEqual(t, encountered, expected)
   301  
   302  	require.NoError(t, err)
   303  	require.Equal(t, expected, parentStream.Results())
   304  }
   305  
   306  func TestWithDatastoreCursorInCursor(t *testing.T) {
   307  	limits := newLimitTracker(500)
   308  
   309  	ci, err := newCursorInformation(&v1.Cursor{
   310  		DispatchVersion: 1,
   311  		Sections:        []string{},
   312  	}, limits, 1)
   313  	require.NoError(t, err)
   314  
   315  	encountered := []int{}
   316  	lock := sync.Mutex{}
   317  
   318  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   319  	err = withDatastoreCursorInCursor[int, int](
   320  		context.Background(),
   321  		ci,
   322  		parentStream,
   323  		5,
   324  		func(queryCursor options.Cursor) ([]itemAndPostCursor[int], error) {
   325  			return []itemAndPostCursor[int]{
   326  				{1, tuple.MustParse("document:foo#viewer@user:tom")},
   327  				{2, tuple.MustParse("document:foo#viewer@user:sarah")},
   328  				{3, tuple.MustParse("document:foo#viewer@user:fred")},
   329  			}, nil
   330  		},
   331  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   332  			lock.Lock()
   333  			encountered = append(encountered, item)
   334  			lock.Unlock()
   335  
   336  			return stream.Publish(item * 10)
   337  		})
   338  
   339  	expected := []int{10, 20, 30}
   340  
   341  	require.Equal(t, len(expected), len(encountered))
   342  	require.NotEqual(t, encountered, expected)
   343  
   344  	require.NoError(t, err)
   345  	require.Equal(t, expected, parentStream.Results())
   346  }
   347  
   348  func TestWithDatastoreCursorInCursorWithStartingCursor(t *testing.T) {
   349  	limits := newLimitTracker(500)
   350  
   351  	ci, err := newCursorInformation(&v1.Cursor{
   352  		DispatchVersion: 1,
   353  		Sections:        []string{"", "42"},
   354  	}, limits, 1)
   355  	require.NoError(t, err)
   356  
   357  	encountered := []int{}
   358  	lock := sync.Mutex{}
   359  
   360  	parentStream := dispatch.NewCollectingDispatchStream[int](context.Background())
   361  	err = withDatastoreCursorInCursor[int, int](
   362  		context.Background(),
   363  		ci,
   364  		parentStream,
   365  		5,
   366  		func(queryCursor options.Cursor) ([]itemAndPostCursor[int], error) {
   367  			require.Equal(t, "", tuple.MustString(queryCursor))
   368  
   369  			return []itemAndPostCursor[int]{
   370  				{2, tuple.MustParse("document:foo#viewer@user:sarah")},
   371  				{3, tuple.MustParse("document:foo#viewer@user:fred")},
   372  			}, nil
   373  		},
   374  		func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error {
   375  			lock.Lock()
   376  			encountered = append(encountered, item)
   377  			lock.Unlock()
   378  
   379  			if v, _ := cc.headSectionValue(); v != "" {
   380  				value, _ := cc.integerSectionValue()
   381  				item = item + value
   382  			}
   383  
   384  			return stream.Publish(item * 10)
   385  		})
   386  
   387  	require.NoError(t, err)
   388  
   389  	expected := []int{440, 30}
   390  	require.Equal(t, len(expected), len(encountered))
   391  	require.Equal(t, expected, parentStream.Results())
   392  }