launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/utils/fslock/fslock.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // On-disk mutex protecting a resource
     5  //
     6  // A lock is represented on disk by a directory of a particular name,
     7  // containing an information file.  Taking a lock is done by renaming a
     8  // temporary directory into place.  We use temporary directories because for
     9  // all filesystems we believe that exactly one attempt to claim the lock will
    10  // succeed and the others will fail.
    11  package fslock
    12  
    13  import (
    14  	"bytes"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"launchpad.net/errgo/errors"
    18  	"os"
    19  	"path"
    20  	"regexp"
    21  	"time"
    22  
    23  	"github.com/loggo/loggo"
    24  
    25  	"launchpad.net/juju-core/utils"
    26  )
    27  
    28  const (
    29  	// NameRegexp specifies the regular expression used to identify valid lock names.
    30  	NameRegexp      = "^[a-z]+[a-z0-9.-]*$"
    31  	heldFilename    = "held"
    32  	messageFilename = "message"
    33  )
    34  
    35  var (
    36  	mask           = errors.Mask
    37  	logger         = loggo.GetLogger("juju.utils.fslock")
    38  	ErrLockNotHeld = errors.New("lock not held")
    39  	ErrTimeout     = errors.New("lock timeout exceeded")
    40  
    41  	validName = regexp.MustCompile(NameRegexp)
    42  
    43  	LockWaitDelay = 1 * time.Second
    44  )
    45  
    46  type Lock struct {
    47  	name   string
    48  	parent string
    49  	nonce  []byte
    50  }
    51  
    52  // NewLock returns a new lock with the given name within the given lock
    53  // directory, without acquiring it. The lock name must match the regular
    54  // expression defined by NameRegexp.
    55  func NewLock(lockDir, name string) (*Lock, error) {
    56  	if !validName.MatchString(name) {
    57  		return nil, errors.Newf("Invalid lock name %q.  Names must match %q", name, NameRegexp)
    58  	}
    59  	nonce, err := utils.NewUUID()
    60  	if err != nil {
    61  		return nil, mask(err)
    62  	}
    63  	lock := &Lock{
    64  		name:   name,
    65  		parent: lockDir,
    66  		nonce:  nonce[:],
    67  	}
    68  	// Ensure the parent exists.
    69  	if err := os.MkdirAll(lock.parent, 0755); err != nil {
    70  		return nil, mask(err)
    71  	}
    72  	return lock, nil
    73  }
    74  
    75  func (lock *Lock) lockDir() string {
    76  	return path.Join(lock.parent, lock.name)
    77  }
    78  
    79  func (lock *Lock) heldFile() string {
    80  	return path.Join(lock.lockDir(), "held")
    81  }
    82  
    83  func (lock *Lock) messageFile() string {
    84  	return path.Join(lock.lockDir(), "message")
    85  }
    86  
    87  // If message is set, it will write the message to the lock directory as the
    88  // lock is taken.
    89  func (lock *Lock) acquire(message string) (bool, error) {
    90  	// If the lockDir exists, then the lock is held by someone else.
    91  	_, err := os.Stat(lock.lockDir())
    92  	if err == nil {
    93  		return false, nil
    94  	}
    95  	if !os.IsNotExist(err) {
    96  		return false, err
    97  	}
    98  	// Create a temporary directory (in the parent dir), and then move it to
    99  	// the right name.  Using the same directory to make sure the directories
   100  	// are on the same filesystem.  Use a directory name starting with "." as
   101  	// it isn't a valid lock name.
   102  	tempLockName := fmt.Sprintf(".%x", lock.nonce)
   103  	tempDirName, err := ioutil.TempDir(lock.parent, tempLockName)
   104  	if err != nil {
   105  		return false, mask(err) // this shouldn't really fail...
   106  
   107  	}
   108  	// write nonce into the temp dir
   109  	err = ioutil.WriteFile(path.Join(tempDirName, heldFilename), lock.nonce, 0755)
   110  	if err != nil {
   111  		return false, mask(err)
   112  	}
   113  	if message != "" {
   114  		err = ioutil.WriteFile(path.Join(tempDirName, messageFilename), []byte(message), 0755)
   115  		if err != nil {
   116  			return false, mask(err)
   117  		}
   118  	}
   119  	// Now move the temp directory to the lock directory.
   120  	err = utils.ReplaceFile(tempDirName, lock.lockDir())
   121  	if err != nil {
   122  		// Any error on rename means we failed.
   123  		// Beaten to it, clean up temporary directory.
   124  		os.RemoveAll(tempDirName)
   125  		return false, nil
   126  	}
   127  	// We now have the lock.
   128  	return true, nil
   129  }
   130  
   131  // lockLoop tries to acquire the lock. If the acquisition fails, the
   132  // continueFunc is run to see if the function should continue waiting.
   133  func (lock *Lock) lockLoop(message string, continueFunc func() error) error {
   134  	var heldMessage = ""
   135  	for {
   136  		acquired, err := lock.acquire(message)
   137  		if err != nil {
   138  			return mask(err)
   139  		}
   140  		if acquired {
   141  			return nil
   142  		}
   143  		if err = continueFunc(); err != nil {
   144  			return mask(err, errors.Any)
   145  		}
   146  		currMessage := lock.Message()
   147  		if currMessage != heldMessage {
   148  			logger.Infof("attempted lock failed %q, %s, currently held: %s", lock.name, message, currMessage)
   149  			heldMessage = currMessage
   150  		}
   151  		time.Sleep(LockWaitDelay)
   152  	}
   153  }
   154  
   155  // Lock blocks until it is able to acquire the lock.  Since we are dealing
   156  // with sharing and locking using the filesystem, it is good behaviour to
   157  // provide a message that is saved with the lock.  This is output in debugging
   158  // information, and can be queried by any other Lock dealing with the same
   159  // lock name and lock directory.
   160  func (lock *Lock) Lock(message string) error {
   161  	// The continueFunc is effectively a no-op, causing continual looping
   162  	// until the lock is acquired.
   163  	continueFunc := func() error { return nil }
   164  	return lock.lockLoop(message, continueFunc)
   165  }
   166  
   167  // LockWithTimeout tries to acquire the lock. If it cannot acquire the lock
   168  // within the given duration, it returns ErrTimeout.  See `Lock` for
   169  // information about the message.
   170  func (lock *Lock) LockWithTimeout(duration time.Duration, message string) error {
   171  	deadline := time.Now().Add(duration)
   172  	continueFunc := func() error {
   173  		if time.Now().After(deadline) {
   174  			return ErrTimeout
   175  		}
   176  		return nil
   177  	}
   178  	return lock.lockLoop(message, continueFunc)
   179  }
   180  
   181  // Lock blocks until it is able to acquire the lock.  If the lock is failed to
   182  // be acquired, the continueFunc is called prior to the sleeping.  If the
   183  // continueFunc returns an error, that error is returned from LockWithFunc.
   184  func (lock *Lock) LockWithFunc(message string, continueFunc func() error) error {
   185  	return lock.lockLoop(message, continueFunc)
   186  }
   187  
   188  // IsHeld returns whether the lock is currently held by the receiver.
   189  func (lock *Lock) IsLockHeld() bool {
   190  	heldNonce, err := ioutil.ReadFile(lock.heldFile())
   191  	if err != nil {
   192  		return false
   193  	}
   194  	return bytes.Equal(heldNonce, lock.nonce)
   195  }
   196  
   197  // Unlock releases a held lock.  If the lock is not held ErrLockNotHeld is
   198  // returned.
   199  func (lock *Lock) Unlock() error {
   200  	if !lock.IsLockHeld() {
   201  		return ErrLockNotHeld
   202  	}
   203  	// To ensure reasonable unlocking, we should rename to a temp name, and delete that.
   204  	tempLockName := fmt.Sprintf(".%s.%x", lock.name, lock.nonce)
   205  	tempDirName := path.Join(lock.parent, tempLockName)
   206  	// Now move the lock directory to the temp directory to release the lock.
   207  	if err := utils.ReplaceFile(lock.lockDir(), tempDirName); err != nil {
   208  		return mask(err)
   209  	}
   210  
   211  	// And now cleanup.
   212  	return os.RemoveAll(tempDirName)
   213  }
   214  
   215  // IsLocked returns true if the lock is currently held by anyone.
   216  func (lock *Lock) IsLocked() bool {
   217  	_, err := os.Stat(lock.heldFile())
   218  	return err == nil
   219  }
   220  
   221  // BreakLock forcably breaks the lock that is currently being held.
   222  func (lock *Lock) BreakLock() error {
   223  	return os.RemoveAll(lock.lockDir())
   224  }
   225  
   226  // Message returns the saved message, or the empty string if there is no
   227  // saved message.
   228  func (lock *Lock) Message() string {
   229  	message, err := ioutil.ReadFile(lock.messageFile())
   230  	if err != nil {
   231  		return ""
   232  	}
   233  	return string(message)
   234  }