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