@@ -2,11 +2,14 @@ package store
2
2
3
3
import (
4
4
"context"
5
+ "database/sql"
5
6
"strings"
6
7
"testing"
8
+ "time"
7
9
8
10
"github.com/google/go-cmp/cmp"
9
11
"github.com/keegancsmith/sqlf"
12
+ "golang.org/x/sync/errgroup"
10
13
11
14
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
12
15
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
@@ -425,6 +428,159 @@ func TestDown(t *testing.T) {
425
428
})
426
429
}
427
430
431
+ func TestIndexStatus (t * testing.T ) {
432
+ db := dbtest .NewDB (t )
433
+ store := testStore (db )
434
+ ctx := context .Background ()
435
+
436
+ if _ , err := db .ExecContext (ctx , "CREATE TABLE tbl (id text, name text);" ); err != nil {
437
+ t .Fatalf ("unexpected error: %s" , err )
438
+ }
439
+
440
+ // Index does not (yet) exist
441
+ if _ , ok , err := store .IndexStatus (ctx , "tbl" , "idx" ); err != nil {
442
+ t .Fatalf ("unexpected error: %s" , err )
443
+ } else if ok {
444
+ t .Fatalf ("unexpected index status" )
445
+ }
446
+
447
+ // Wrap context in a small timeout; we do tight for-loops here to determine
448
+ // when we can continue on to/unblock the next operation, but none of the
449
+ // steps should take any significant time.
450
+ ctx , cancel := context .WithTimeout (ctx , time .Second * 10 )
451
+ group , groupCtx := errgroup .WithContext (ctx )
452
+ defer cancel ()
453
+
454
+ whileEmpty := func (ctx context.Context , conn dbutil.DB , query string ) error {
455
+ for {
456
+ rows , err := conn .QueryContext (ctx , query )
457
+ if err != nil {
458
+ return err
459
+ }
460
+
461
+ lockVisible := rows .Next ()
462
+
463
+ if err := basestore .CloseRows (rows , nil ); err != nil {
464
+ return err
465
+ }
466
+
467
+ if lockVisible {
468
+ return nil
469
+ }
470
+ }
471
+ }
472
+
473
+ // Create separate connections to precise control contention of resources
474
+ // so we can examine what this method returns while an index is being created.
475
+
476
+ conns := make ([]* sql.Conn , 3 )
477
+ for i := 0 ; i < 3 ; i ++ {
478
+ conn , err := db .Conn (ctx )
479
+ if err != nil {
480
+ t .Fatalf ("failed to open new connection: %s" , err )
481
+ }
482
+ t .Cleanup (func () { conn .Close () })
483
+
484
+ conns [i ] = conn
485
+ }
486
+ connA , connB , connC := conns [0 ], conns [1 ], conns [2 ]
487
+
488
+ lockQuery := `SELECT pg_advisory_lock(10, 10)`
489
+ unlockQuery := `SELECT pg_advisory_unlock(10, 10)`
490
+ createIndexQuery := `CREATE INDEX CONCURRENTLY idx ON tbl(id)`
491
+
492
+ // Session A
493
+ // Successfully take and hold advisory lock
494
+ if _ , err := connA .ExecContext (ctx , lockQuery ); err != nil {
495
+ t .Fatalf ("unexpected error: %s" , err )
496
+ }
497
+
498
+ // Session B
499
+ // Try to take advisory lock; blocked by Session A
500
+ group .Go (func () error {
501
+ _ , err := connB .ExecContext (groupCtx , lockQuery )
502
+ return err
503
+ })
504
+
505
+ // Session C
506
+ // try to create index concurrently; blocked by session B waiting on session A
507
+ group .Go (func () error {
508
+ // Wait until we can see Session B's lock before attempting to create index
509
+ if err := whileEmpty (groupCtx , connC , "SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND NOT granted" ); err != nil {
510
+ t .Fatalf ("unexpected error: %s" , err )
511
+ }
512
+
513
+ _ , err := connC .ExecContext (groupCtx , createIndexQuery )
514
+ return err
515
+ })
516
+
517
+ // Wait until we can see Session C's lock before querying index status
518
+ if err := whileEmpty (ctx , db , "SELECT 1 FROM pg_locks WHERE locktype = 'relation' AND mode = 'ShareUpdateExclusiveLock'" ); err != nil {
519
+ t .Fatalf ("unexpected error: %s" , err )
520
+ }
521
+
522
+ // "waiting for old snapshots" will be the phase that is blocked by the concurrent
523
+ // sessions holding advisory locks. We may happen to hit one of the earlier phases
524
+ // if we're quick enough, so we'll keep polling progress until we hit the target.
525
+ blockingPhase := "waiting for old snapshots"
526
+ nonblockingPhasePrefixes := make ([]string , 0 , len (CreateIndexConcurrentlyPhases ))
527
+ for _ , prefix := range CreateIndexConcurrentlyPhases {
528
+ if prefix == blockingPhase {
529
+ break
530
+ }
531
+
532
+ nonblockingPhasePrefixes = append (nonblockingPhasePrefixes , prefix )
533
+ }
534
+ compareWithPrefix := func (value , prefix string ) bool {
535
+ return value == prefix || strings .HasPrefix (value , prefix + ":" )
536
+ }
537
+
538
+ retryLoop:
539
+ for {
540
+ if status , ok , err := store .IndexStatus (ctx , "tbl" , "idx" ); err != nil {
541
+ t .Fatalf ("unexpected error: %s" , err )
542
+ } else if ! ok {
543
+ t .Fatalf ("expected index status" )
544
+ } else if status .Phase == nil {
545
+ t .Fatalf ("unexpected phase. want=%q have=nil" , blockingPhase )
546
+ } else if * status .Phase == blockingPhase {
547
+ break
548
+ } else {
549
+ for _ , prefix := range nonblockingPhasePrefixes {
550
+ if compareWithPrefix (* status .Phase , prefix ) {
551
+ continue retryLoop
552
+ }
553
+ }
554
+
555
+ t .Fatalf ("unexpected phase. want=%q have=%q" , blockingPhase , * status .Phase )
556
+ }
557
+ }
558
+
559
+ // Session A
560
+ // Unlock, unblocking both Session B and Session C
561
+ if _ , err := connA .ExecContext (ctx , unlockQuery ); err != nil {
562
+ t .Fatalf ("unexpected error: %s" , err )
563
+ }
564
+
565
+ // Wait for index creation to complete
566
+ if err := group .Wait (); err != nil {
567
+ t .Fatalf ("unexpected error: %s" , err )
568
+ }
569
+
570
+ if status , ok , err := store .IndexStatus (ctx , "tbl" , "idx" ); err != nil {
571
+ t .Fatalf ("unexpected error: %s" , err )
572
+ } else if ! ok {
573
+ t .Fatalf ("expected index status" )
574
+ } else {
575
+ if ! status .IsValid {
576
+ t .Fatalf ("unexpected isvalid. want=%v have=%v" , true , status .IsValid )
577
+ }
578
+ if status .Phase != nil {
579
+ t .Fatalf ("unexpected phase. want=%v have=%v" , nil , status .Phase )
580
+ }
581
+ }
582
+ }
583
+
428
584
func testStore (db dbutil.DB ) * Store {
429
585
return NewWithDB (db , "test_migrations_table" , NewOperations (& observation .TestContext ))
430
586
}
0 commit comments