vitess.io/vitess@v0.16.2/go/vt/topo/test/trylock.go (about)

     1  /*
     2  Copyright 2022 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package test
    18  
    19  import (
    20  	"context"
    21  	"path"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/stretchr/testify/require"
    26  
    27  	"vitess.io/vitess/go/vt/topo"
    28  
    29  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    30  )
    31  
    32  // checkTryLock checks if we can lock / unlock as expected. It's using a keyspace
    33  // as the lock target.
    34  func checkTryLock(t *testing.T, ts *topo.Server) {
    35  	ctx := context.Background()
    36  	if err := ts.CreateKeyspace(ctx, "test_keyspace", &topodatapb.Keyspace{}); err != nil {
    37  		require.Fail(t, "CreateKeyspace fail", err.Error())
    38  	}
    39  
    40  	conn, err := ts.ConnForCell(context.Background(), topo.GlobalCell)
    41  	if err != nil {
    42  		require.Fail(t, "ConnForCell(global) failed", err.Error())
    43  	}
    44  
    45  	t.Log("===      checkTryLockTimeout")
    46  	checkTryLockTimeout(ctx, t, conn)
    47  
    48  	t.Log("===      checkTryLockMissing")
    49  	checkTryLockMissing(ctx, t, conn)
    50  
    51  	t.Log("===      checkTryLockUnblocks")
    52  	checkTryLockUnblocks(ctx, t, conn)
    53  }
    54  
    55  // checkTryLockTimeout test the fail-fast nature of TryLock
    56  func checkTryLockTimeout(ctx context.Context, t *testing.T, conn topo.Conn) {
    57  	keyspacePath := path.Join(topo.KeyspacesPath, "test_keyspace")
    58  	lockDescriptor, err := conn.TryLock(ctx, keyspacePath, "")
    59  	if err != nil {
    60  		require.Fail(t, "TryLock failed", err.Error())
    61  	}
    62  
    63  	// We have the lock, list the keyspace directory.
    64  	// It should not contain anything, except Ephemeral files.
    65  	entries, err := conn.ListDir(ctx, keyspacePath, true /*full*/)
    66  	if err != nil {
    67  		require.Fail(t, "ListDir failed: %v", err.Error())
    68  	}
    69  	for _, e := range entries {
    70  		if e.Name == "Keyspace" {
    71  			continue
    72  		}
    73  		if e.Ephemeral {
    74  			t.Logf("skipping ephemeral node %v in %v", e, keyspacePath)
    75  			continue
    76  		}
    77  		// Non-ephemeral entries better have only ephemeral children.
    78  		p := path.Join(keyspacePath, e.Name)
    79  		entries, err := conn.ListDir(ctx, p, true /*full*/)
    80  		if err != nil {
    81  			require.Fail(t, "ListDir failed", err.Error())
    82  		}
    83  		for _, e := range entries {
    84  			if e.Ephemeral {
    85  				t.Logf("skipping ephemeral node %v in %v", e, p)
    86  			} else {
    87  				require.Fail(t, "non-ephemeral DirEntry")
    88  			}
    89  		}
    90  	}
    91  
    92  	// We should not be able to take the lock again. It should throw `NodeExists` error.
    93  	fastCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    94  	if _, err := conn.TryLock(fastCtx, keyspacePath, "again"); !topo.IsErrType(err, topo.NodeExists) {
    95  		require.Fail(t, "TryLock failed", err.Error())
    96  	}
    97  	cancel()
    98  
    99  	// test we can interrupt taking the lock
   100  	interruptCtx, cancel := context.WithCancel(ctx)
   101  	finished := make(chan struct{})
   102  
   103  	// go routine to cancel the context.
   104  	go func() {
   105  		<-finished
   106  		cancel()
   107  	}()
   108  
   109  	waitUntil := time.Now().Add(10 * time.Second)
   110  	var firstTime = true
   111  	// after attempting the `TryLock` and getting an error `NodeExists`, we will cancel the context deliberately
   112  	// and expect `context canceled` error in next iteration of `for` loop.
   113  	for {
   114  		if time.Now().After(waitUntil) {
   115  			t.Fatalf("Unlock(test_keyspace) timed out")
   116  		}
   117  		// we expect context to fail with `context canceled` error
   118  		if interruptCtx.Err() != nil {
   119  			require.ErrorContains(t, interruptCtx.Err(), "context canceled")
   120  			break
   121  		}
   122  		if _, err := conn.TryLock(interruptCtx, keyspacePath, "interrupted"); !topo.IsErrType(err, topo.NodeExists) {
   123  			require.Fail(t, "TryLock failed", err.Error())
   124  		}
   125  		if firstTime {
   126  			close(finished)
   127  			firstTime = false
   128  		}
   129  		time.Sleep(1 * time.Second)
   130  	}
   131  
   132  	if err := lockDescriptor.Check(ctx); err != nil {
   133  		t.Errorf("Check(): %v", err)
   134  	}
   135  
   136  	if err := lockDescriptor.Unlock(ctx); err != nil {
   137  		require.Fail(t, "Unlock failed", err.Error())
   138  	}
   139  
   140  	// test we can't unlock again
   141  	if err := lockDescriptor.Unlock(ctx); err == nil {
   142  		require.Fail(t, "Unlock failed", err.Error())
   143  	}
   144  }
   145  
   146  // checkTryLockMissing makes sure we can't lock a non-existing directory.
   147  func checkTryLockMissing(ctx context.Context, t *testing.T, conn topo.Conn) {
   148  	keyspacePath := path.Join(topo.KeyspacesPath, "test_keyspace_666")
   149  	if _, err := conn.TryLock(ctx, keyspacePath, "missing"); err == nil {
   150  		require.Fail(t, "TryLock(test_keyspace_666) worked for non-existing keyspace")
   151  	}
   152  }
   153  
   154  // unlike 'checkLockUnblocks', checkTryLockUnblocks will not block on other client but instead
   155  // keep retrying until it gets the lock.
   156  func checkTryLockUnblocks(ctx context.Context, t *testing.T, conn topo.Conn) {
   157  	keyspacePath := path.Join(topo.KeyspacesPath, "test_keyspace")
   158  	unblock := make(chan struct{})
   159  	finished := make(chan struct{})
   160  
   161  	duration := 10 * time.Second
   162  	waitUntil := time.Now().Add(duration)
   163  	// TryLock will keep getting NodeExists until lockDescriptor2 unlock itself.
   164  	// It will not wait but immediately return with NodeExists error.
   165  	go func() {
   166  		<-unblock
   167  		for time.Now().Before(waitUntil) {
   168  			lockDescriptor, err := conn.TryLock(ctx, keyspacePath, "unblocks")
   169  			if err != nil {
   170  				if !topo.IsErrType(err, topo.NodeExists) {
   171  					require.Fail(t, "expected node exists during trylock", err.Error())
   172  				}
   173  				time.Sleep(1 * time.Second)
   174  			} else {
   175  				if err = lockDescriptor.Unlock(ctx); err != nil {
   176  					require.Fail(t, "Unlock(test_keyspace) failed", err.Error())
   177  				}
   178  				close(finished)
   179  				break
   180  			}
   181  		}
   182  	}()
   183  
   184  	// Lock the keyspace.
   185  	lockDescriptor2, err := conn.TryLock(ctx, keyspacePath, "")
   186  	if err != nil {
   187  		require.Fail(t, "Lock(test_keyspace) failed", err.Error())
   188  	}
   189  
   190  	// unblock the go routine so it starts waiting
   191  	close(unblock)
   192  
   193  	if err = lockDescriptor2.Unlock(ctx); err != nil {
   194  		require.Fail(t, "Unlock(test_keyspace) failed", err.Error())
   195  	}
   196  
   197  	timeout := time.After(2 * duration)
   198  	select {
   199  	case <-finished:
   200  	case <-timeout:
   201  		require.Fail(t, "Unlock(test_keyspace) timed out")
   202  	}
   203  }