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 }