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  }