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 }