vitess.io/vitess@v0.16.2/go/vt/topo/consultopo/lock.go (about) 1 /* 2 Copyright 2019 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 consultopo 18 19 import ( 20 "context" 21 "fmt" 22 "path" 23 24 "github.com/hashicorp/consul/api" 25 26 "vitess.io/vitess/go/vt/proto/vtrpc" 27 "vitess.io/vitess/go/vt/vterrors" 28 29 "vitess.io/vitess/go/vt/log" 30 "vitess.io/vitess/go/vt/topo" 31 ) 32 33 // consulLockDescriptor implements topo.LockDescriptor. 34 type consulLockDescriptor struct { 35 s *Server 36 lockPath string 37 lost <-chan struct{} 38 } 39 40 // Lock is part of the topo.Conn interface. 41 func (s *Server) Lock(ctx context.Context, dirPath, contents string) (topo.LockDescriptor, error) { 42 // We list the directory first to make sure it exists. 43 if _, err := s.ListDir(ctx, dirPath, false /*full*/); err != nil { 44 // We need to return the right error codes, like 45 // topo.ErrNoNode and topo.ErrInterrupted, and the 46 // easiest way to do this is to return convertError(err). 47 // It may lose some of the context, if this is an issue, 48 // maybe logging the error would work here. 49 return nil, convertError(err, dirPath) 50 } 51 52 return s.lock(ctx, dirPath, contents) 53 } 54 55 // TryLock is part of the topo.Conn interface. 56 func (s *Server) TryLock(ctx context.Context, dirPath, contents string) (topo.LockDescriptor, error) { 57 // We list all the entries under dirPath 58 entries, err := s.ListDir(ctx, dirPath, true) 59 if err != nil { 60 // We need to return the right error codes, like 61 // topo.ErrNoNode and topo.ErrInterrupted, and the 62 // easiest way to do this is to return convertError(err). 63 // It may lose some of the context, if this is an issue, 64 // maybe logging the error would work here. 65 return nil, convertError(err, dirPath) 66 } 67 68 // If there is a file 'lock' in it then we can assume that someone else already has a lock. 69 // Throw error in this case 70 for _, e := range entries { 71 if e.Name == locksFilename && e.Type == topo.TypeFile && e.Ephemeral { 72 return nil, topo.NewError(topo.NodeExists, fmt.Sprintf("lock already exists at path %s", dirPath)) 73 } 74 } 75 76 // everything is good let's acquire the lock. 77 return s.lock(ctx, dirPath, contents) 78 } 79 80 // Lock is part of the topo.Conn interface. 81 func (s *Server) lock(ctx context.Context, dirPath, contents string) (topo.LockDescriptor, error) { 82 lockPath := path.Join(s.root, dirPath, locksFilename) 83 84 lockOpts := &api.LockOptions{ 85 Key: lockPath, 86 Value: []byte(contents), 87 SessionOpts: &api.SessionEntry{ 88 Name: api.DefaultLockSessionName, 89 TTL: api.DefaultLockSessionTTL, 90 }, 91 } 92 lockOpts.SessionOpts.Checks = s.lockChecks 93 if s.lockDelay > 0 { 94 lockOpts.SessionOpts.LockDelay = s.lockDelay 95 } 96 if s.lockTTL != "" { 97 lockOpts.SessionOpts.TTL = s.lockTTL 98 } 99 // Build the lock structure. 100 l, err := s.client.LockOpts(lockOpts) 101 if err != nil { 102 return nil, err 103 } 104 105 // Wait until we are the only ones in this client trying to 106 // lock that path. 107 s.mu.Lock() 108 li, ok := s.locks[lockPath] 109 for ok { 110 // Unlock, wait for something to change. 111 s.mu.Unlock() 112 select { 113 case <-ctx.Done(): 114 return nil, convertError(ctx.Err(), dirPath) 115 case <-li.done: 116 } 117 118 // The original locker is gone, try to get it again 119 s.mu.Lock() 120 li, ok = s.locks[lockPath] 121 } 122 li = &lockInstance{ 123 lock: l, 124 done: make(chan struct{}), 125 } 126 s.locks[lockPath] = li 127 s.mu.Unlock() 128 129 // We are the only ones trying to lock now. 130 lost, err := l.Lock(ctx.Done()) 131 if err != nil || lost == nil { 132 // Failed to lock, give up our slot in locks map. 133 // Close the channel to unblock anyone else. 134 s.mu.Lock() 135 delete(s.locks, lockPath) 136 s.mu.Unlock() 137 close(li.done) 138 // Consul will return empty leaderCh with nil error if we cannot get lock before the timeout 139 // therefore we return a timeout error here 140 if lost == nil { 141 return nil, topo.NewError(topo.Timeout, lockPath) 142 } 143 return nil, err 144 } 145 146 // We got the lock, we're good. 147 return &consulLockDescriptor{ 148 s: s, 149 lockPath: lockPath, 150 lost: lost, 151 }, nil 152 } 153 154 // Check is part of the topo.LockDescriptor interface. 155 func (ld *consulLockDescriptor) Check(ctx context.Context) error { 156 select { 157 case <-ld.lost: 158 return vterrors.Errorf(vtrpc.Code_INTERNAL, "lost channel closed") 159 default: 160 } 161 return nil 162 } 163 164 // Unlock is part of the topo.LockDescriptor interface. 165 func (ld *consulLockDescriptor) Unlock(ctx context.Context) error { 166 return ld.s.unlock(ctx, ld.lockPath) 167 } 168 169 // unlock releases a lock acquired by Lock() on the given directory. 170 func (s *Server) unlock(ctx context.Context, lockPath string) error { 171 s.mu.Lock() 172 li, ok := s.locks[lockPath] 173 s.mu.Unlock() 174 if !ok { 175 return vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "unlock: lock %v not held", lockPath) 176 } 177 178 // Try to unlock our lock. We will clean up our entry anyway. 179 unlockErr := li.lock.Unlock() 180 181 s.mu.Lock() 182 delete(s.locks, lockPath) 183 s.mu.Unlock() 184 close(li.done) 185 186 // Then try to remove the lock entirely. This will only work if 187 // no one else has the lock. 188 if err := li.lock.Destroy(); err != nil { 189 // If someone else has the lock, we can't remove it, 190 // but we don't need to. 191 if err != api.ErrLockInUse { 192 log.Warningf("failed to clean up lock file %v: %v", lockPath, err) 193 } 194 } 195 196 return unlockErr 197 }